Высокое использование центрального процессора из-за system.currentTimeMillis()

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

Вывод 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

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

% 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 эти вызовы ядра должны происходить только в адресном пространстве пользователя. Но, как видно из перфорированного отчета, вызовы ядра действительно происходят в пространстве ядра. Любые указатели на это? Версия ядра: 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 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);
    }
}

Ответ 1

Я не понимаю, что при наличии vDSO эти вызовы ядра должны происходить только в адресном пространстве пользователя. Но, как видно из перфорированного отчета, вызовы ядра действительно происходят в пространстве ядра. Любые указатели на это?

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

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

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

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

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

Ответ 2

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

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

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

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

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

Я не понимаю, что при наличии vDSO эти вызовы ядра должны происходить только в адресном пространстве пользователя. Но, как видно из перфорированного отчета, вызовы ядра действительно происходят в пространстве ядра. Любые указатели на это?

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

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

Время центрального процессора:. Время, затраченное процессором на функции операционной системы, подключенные к этой конкретной программе.

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

Ответ 3

прочитав свой код, нет кода управления для блокировки цикла while, кроме publisher.publish(events) и queue.poll(), что означает, что этот поток занят во время цикла, никогда не делайте перерыва.

на мой взгляд, вам нужно ограничить вызовы на 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
    }
}

Ответ 4

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

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