由于 system.currentTimeMillis(),系统 CPU 使用率高

High System CPU usage because of system.currentTimeMillis()

我在我们的风暴主管(Wheezy 机器)上调试高系统 CPU 使用率(不是用户 CPU 使用率)。以下是观察结果

相关进程的 perf 输出:

Events: 10K cpu-clock
16.40%  java  [kernel.kallsyms]   [k] system_call_after_swapgs
13.95%  java  [kernel.kallsyms]   [k] pvclock_clocksource_read
12.76%  java  [kernel.kallsyms]   [k] do_gettimeofday
12.61%  java  [vdso]              [.] 0x7ffe0fea898f
 9.02%  java  perf-17609.map      [.] 0x7fcabb8b85dc
 7.16%  java  [kernel.kallsyms]   [k] copy_user_enhanced_fast_string
 4.97%  java  [kernel.kallsyms]   [k] native_read_tsc
 2.88%  java  [kernel.kallsyms]   [k] sys_gettimeofday
 2.82%  java  libjvm.so           [.] os::javaTimeMillis()
 2.39%  java  [kernel.kallsyms]   [k] arch_local_irq_restore

在相关进程的一个线程的 strace 中捕获到这个

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000247           0     64038           gettimeofday
  0.00    0.000000           0         1           rt_sigreturn
  0.00    0.000000           0         1           futex
------ ----------- ----------- --------- --------- ----------------
100.00    0.000247                 64040           total

终于弄明白线程是while(true)中的运行,其中一个调用是System.currentTimeMillis()。我禁用了它,系统 CPU % 从 50% 下降到 3%。很明显,这就是问题所在。我无法理解的是,在存在 vDSO 的情况下,这些内核调用应该只发生在用户地址 space 中。但从 perf 报告中可以清楚地看出,内核调用确实发生在内核 space 中。对此有任何指示吗? 内核版本:3.2.0-4-amd64 Debian 3.2.86-1 x86_64 GNU/Linux
时钟类型:kvm

添加有问题线程的代码。

@RequiredArgsConstructor
public class TestThread implements Runnable {
    private final Queue<String> queue;
    private final Publisher publisher;
    private final int maxBatchSize;

    private long lastPushTime;
    @Override
    public void run() {
        lastPushTime = System.currentTimeMillis();
        List<String> events = new ArrayList<>();
        while (true) {
            try {
                String message = queue.poll();
                long lastPollTime = System.currentTimeMillis();
                if (message != null) {
                    events.add(message);
                    pushEvents(events, false);
                }

                // if event threshold hasn't reached the size, but it's been there for over 10seconds, push it.
                if ((lastPollTime - lastPushTime > 10000) && (events.size() > 0)) {
                    pushEvents(events, true);
                }
            } catch (Exception e) {
                // Log and do something
            }
        }
    }

    private void pushEvents(List<String> events, boolean forcePush) {
        if (events.size() >= maxBatchSize || forcePush) {
            pushToHTTPEndPoint(events);
            events.clear();
            lastPushTime = System.currentTimeMillis();
        }
    }

    private void pushToHTTPEndPoint(List<String> events) {
        publisher.publish(events);
    }
}

循环内没有其他值得注意的东西,所以你在 System.currentTimeMillis()

上旋转

vDSO 将有助于提高 System.currentTimeMillis() 的性能,但它真的会改变 CPU 的分类从 "System" 到 "User" 吗?我不知道,抱歉。

此线程将消耗 100% CPU,是否分类为 "System" 或 "User" 有很大区别吗?

您应该重写此代码以使用非自旋等待,例如 BlockingQueue.poll(timeout)

您的实际问题是什么?

What I fail to understand is, in the presence of vDSO these kernel calls should only happen in the user's address space. But as is clear from perf report, kernel calls are indeed taking place in the kernel space. Any pointers on this?

为什么 CPU 在此自旋锁中花费的时间如何分类很重要?

根据 User CPU time vs System CPU time? "System CPU Time" 是:

System CPU Time: Amount of time the processor worked on operating system's functions connected to that specific program.

根据该定义,花在 System.currentTimeMillis() 上的时间将算作系统时间,即使由于 vDSO 不需要用户到内核模式切换。

通过阅读你的代码,除了publisher.publish(events)queue.poll()之外,没有控制代码来阻止while循环,这意味着这个线程在while循环中很忙,从不休息。

在我看来,你需要限制对 System.currentTimeMillis() 的调用。一个好的选择是使 queue.poll() 阻塞,一些伪代码:

while (!stopWork) {
    try {
        // wait for messages with 10 seconds timeout,if no message or timeout return empty list
        // this is easy to impl with BlockingQueue
        List<String> events = queue.poll(10,TimeUnit.SECOND);
        if (events.isEmpty()) {
            continue;
        }
        new java.util.Timer().schedule( 
            new java.util.TimerTask() {
                @Override
                public void run() {
                    pushEvents(events, true);
                }
            }, 1000*10 );
    } catch (Exception e) {
        // Log and do something
    }
}

What I fail to understand is, in the presence of vDSO these kernel calls should only happen in the user's address space. But as is clear from perf report, kernel calls are indeed taking place in the kernel space. Any pointers on this?

可以在虚拟系统上禁用 vDSO。 KVM 使用 PVClock(你可以在这个 nice article 中阅读更多内容)并且它取决于内核版本。 例如,我们可以看到 here 永远不会覆盖 VCLOCK_MODE。 另一方面,here it is changed vclock_mode - and vclock_mode indicator 也适用于 vDSO。

commit 中引入了此支持,并在 Linux 内核的 3.8 版本中发布。

一般在我的实践中,如果在"while(true)"里面调用了很长时间,总会看到很大的CPU消耗。

当然,在大多数情况下,阻塞队列就足够了,但是如果您需要良好的延迟和性能,您也可以使用自旋,没有线程阻塞,但是您应该限制自旋周期并进行基准测试以衡量此影响优化。元代码可能类似于:

int spin = 100;
while(spin-- > 0) {
    // try to get result
}
// still no result -> execute blocking code

所以我在这里找到了问题所在。为了提供更多上下文,问题更多是关于 vDSO 进行系统调用的事实(如果原始 post 具有误导性,我们深表歉意!)。此内核版本 (kvmclock) 的时钟源不支持虚拟系统调用,因此发生了真正的系统调用。它是在这个提交 https://github.com/torvalds/linux/commit/3dc4f7cfb7441e5e0fed3a02fc81cdaabd28300a#diff-5a34e1e52f50e00cef4b0d0ff3fef8f7 中引入的(感谢 egorlitvinenko 指出这一点。

此外,我确实知道 while(true) 中的任何内容都会消耗 CPU。由于这是在 apache storm 上下文中调用实质上是在进行 HTTP 调用之前批处理事件,因此可以通过使用 apache storm 的滴答元组支持以更好的方式完成此操作。