Высокая загрузка ЦП системы из-за 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
}
}