Высокая загрузка ЦП системы из-за system.currentTimeMillis()

Я отлаживал высокую загрузку ЦП системы (не ЦП пользователя) на наших супервизорах (машина Wheezy). Вот наблюдения

Вывод перф для соответствующего процесса:

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

Поймал это в нити соответствующего процесса

% 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(), Я отключил то же самое, и системный процессор% снизился с 50% до 3%. Так ясно, что это была проблема. Что я не понимаю, так это то, что при наличии vDSO эти вызовы ядра должны происходить только в адресном пространстве пользователя. Но, как видно из отчета perf, вызовы ядра действительно происходят в пространстве ядра. Есть какие-нибудь указатели на это? Версия ядра: 3.2.0-4-amd64 Debian 3.2.86-1 x86_64 GNU/Linux
тип часов: квм

Добавление кода проблемной темы.

@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);
    }
}

4 ответа

Решение

Что я не понимаю, так это то, что при наличии vDSO эти вызовы ядра должны происходить только в адресном пространстве пользователя. Но, как видно из отчета perf, вызовы ядра действительно происходят в пространстве ядра. Есть какие-нибудь указатели на это?

vDSO может быть отключен в виртуальной системе. KVM использует PVClock (об этом вы можете прочитать в этой хорошей статье), и это зависит от версии ядра. Например, мы могли видеть здесь, что VCLOCK_MODE никогда не переопределяется. С другой стороны, здесь это изменяется vclock_mode - и индикатор vclock_mode для vDSO тоже.

Эта поддержка была введена в этом коммите и выпущена в 3.8 версии ядра Linux.

Как правило, в моей практике, если вы долго вызываете что-то внутри "while(true)", вы всегда будете видеть большую загрузку процессора.

Конечно, очереди блокирования в большинстве случаев достаточно, но если вам нужны хорошие время ожидания и производительность, вы также можете использовать вращение без блокировки потоков, но вы должны ограничить циклы вращения и сделать эталонные тесты, чтобы измерить влияние этой оптимизации. Мета-код может быть что-то вроде:

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

Внутри петли нет ничего более заметного, так что вы вращаетесь System.currentTimeMillis()

vDSO поможет улучшить производительность System.currentTimeMillis(), но действительно ли это меняет классификацию CPU с "System" на "User"? Я не знаю, прости.

Этот поток будет потреблять 100% ЦП, имеет ли большое значение, классифицируется ли он как "Системный" или "Пользовательский"?

Вы должны переписать этот код, чтобы использовать, например, ожидание без вращения BlockingQueue.poll(timeout)

Какой у вас актуальный вопрос здесь?

Что я не понимаю, так это то, что при наличии vDSO эти вызовы ядра должны происходить только в адресном пространстве пользователя. Но, как видно из отчета perf, вызовы ядра действительно происходят в пространстве ядра. Есть какие-нибудь указатели на это?

Почему имеет значение, как классифицируется время ЦП внутри этой спин-блокировки?

В зависимости от времени ЦП пользователя и времени ЦП системы? "Системное процессорное время":

System CPU Time: время, в течение которого процессор работал над функциями операционной системы, связанными с этой конкретной программой.

По этому определению время, потраченное на вращение System.currentTimeMillis() будет считаться системным временем, даже если для него не требуется переключение режима пользователя в ядро ​​из-за vDSO.

Так что я разобрался с проблемой здесь. Чтобы дать больше контекста, вопрос был больше о том, что vDSO совершает системные вызовы (извиняюсь, если оригинальное сообщение вводило в заблуждение!). Источник синхронизации для этой версии ядра (kvmclock) не поддерживает виртуальные системные вызовы и, следовательно, происходят реальные системные вызовы. Он был представлен в этом коммите https://github.com/torvalds/linux/commit/3dc4f7cfb7441e5e0fed3a02fc81cdaabd28300a (спасибо egorlitvinenko за указание на это.

Кроме того, я понимаю, что все в то время как (истина) будет потреблять процессор. Так как это происходило в контексте Apache Storm, где вызов должен был по сути пакетировать события перед выполнением HTTP-вызова, это можно было бы сделать лучше, используя поддержку тиковых кортежей Apache Storm.

При чтении вашего кода, не существует контрольного кода для блокировки цикла while, кроме publisher.publish(events)а также queue.poll(), это означает, что этот поток занят циклом 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
    }
}
Другие вопросы по тегам