Привет! Меня зовут Александр Петровский, я инженер в DINS. Я работаю в команде, которая участвует в разработке сервисов облачной телефонии и видеоконференций для RingCentral. Каждый из них состоит из большого количества микросервисов.
Когда мы мигрировали один из наших микросервисов с CentOS 7 с ядром 4.19 на Oracle Linux 7 с ядром 5.4, мы заметили рост утилизации процессора на наших stress/performance-тестах. В статье я расскажу, как мы исследовали причины роста утилизации процессора сначала в user-space, а потом и в kernel-space и о том, к какому результату это нас привело.
Проблема
Для начала немного о том, что представляет собой наш микросервис: это in-house L3/L4 balancing router. Ядро сервиса состоит из eBPF/XDP [1] приложения, которое загружается в ядро Linux («живет» в SOFTIRQ) и решает задачи балансировки/роутинга сетевых пакетов до конечных бэкендов — это наш data plane. Cервис похож на katran [2] у Facebook, maglev [3] у Google, unimog [4] у Cloudflare и glb [5] у Github, но за небольшим исключением, что выгодно отличает нас от остальных. Серверы, на которых расположен микросервис (далее по тексту «ноды»), соединены между собой в кластер, а также соединены по BGP с L3 Juniper роутерами и получают от них сетевые пакеты с помощью ECMP. Сами кластеры располагаются в разных дата-центрах и соединены между собой, каждая нода в кластере знает состояние всех соединений во всех присоединенных кластерах. За это отвечает приложение на Erlang совместно с приложением на Golang как адаптером для работы с eBPF/XDP — это наш control plane. Таким образом, каждая нода готова балансировать/роутить пакеты в кластере, если все остальные ноды в кластере выйдут из строя. Каждый кластер в свою очередь готов балансировать/роутить пакеты других присоединенных кластеров, если они выйдут из строя.
В ходе миграции на наших stress/performance-тестах, которые генерируют высокий PPS, мы заметили рост утилизации процессора примерно на 10% на Oracle Linux 7 с ядром 5.4.17-2102.200.13.el7uek по сравнению с CentOS 7 с ядром 4.19.125-1:
Тут в первую очередь нам интересен рост утилизации процессора в SOFTIRQ: 13% vs. 26%. SOFTIRQ показывает время, затрачиваемое процессором при обработке некоторых soft deferred задач. Таких как обработка сетевых пакетов (rx/tx), RCU, таймеров и tasklet'ов. Следовательно, нам нужно выяснить, на что больше всего процессорного времени тратится в SOFTIRQ?
Исследование в user-space
Чтобы определить, на какой тип задач (обработка сетевых пакетов (rx/tx), RCU, таймеров и tasklet'ов) процессор тратит больше всего времени в SOFTIRQ, достаточно заглянуть в файл /proc/softirqs. Для нас интересны в первую очередь NET_TX и NET_RX. Пожалуйста, обратите внимание — файл содержит статистику по разным типам задач SOFTIRQ с начала старта системы и в данном случае, нам интересна только скорость роста этих значений:
[root@str01-t01-**r01 /] # cat /proc/softirqs
CPU0 CPU1 CPU2 CPU3
HI: 0 0 0 0
TIMER: 129613942 356589263 129018907 163921988
NET_TX: 2508105 2585781 2487271 2447192
NET_RX: 2044953742 1994883329 1858956128 1847655110
BLOCK: 609965 122879 139668 167503
IRQ_POLL: 0 0 0 0
TASKLET: 73252 57791 163713 154726
SCHED: 55000904 79751919 49854734 56760814
HRTIMER: 2 0 0 1
RCU: 114773995 223570840 114631792 132787573
и
[root@str01-t01-**r02 /] # cat /proc/softirqs
CPU0 CPU1 CPU2 CPU3
HI: 0 0 0 0
TIMER: 132695274 339738125 154810847 117233153
NET_TX: 2166064 2348500 2239794 2108175
NET_RX: 1049349788 1038558391 1003371390 911448878
BLOCK: 97480 194503 149366 106470
IRQ_POLL: 0 0 0 0
TASKLET: 124116 266805 263565 141745
SCHED: 52902419 77032919 54463361 44062142
HRTIMER: 229 0 10 258
RCU: 105711455 206432337 118001592 97389637
Из листингов выше видно, значения NET_TX и NET_RX во времени растут примерно с одинаковой скоростью.
Но все-таки, кто же из них стал работать медленнее и как следствие тратить больше процессорного времени? Чтобы это определить, можно воспользоваться набором скриптов из пакета BCC. Скрипт /usr/share/bcc/tools/softirqs из этого пакета просуммирует время, затрачиваемое каждыми типом задач в SOFTIRQ в течении 10 секунд. Пожалуйста, обратите внимание, оригинальный скрипт был модифицирован для сбора статистики только на 0-м ядре процессора для большей точности и гранулярности:
[root@str01-t01-**r01 /] # /usr/share/bcc/tools/softirqs 10 1
Tracing soft irq event time... Hit Ctrl-C to end.
SOFTIRQ TOTAL_usecs
net_tx 21
block 47
rcu 1472
timer 3735
sched 7419
net_rx 2663715
и
[root@str01-t01-**r02 /] # /usr/share/bcc/tools/softirqs 10 1
Tracing soft irq event time... Hit Ctrl-C to end.
SOFTIRQ TOTAL_usecs
net_tx 14
timer 2408
rcu 2485
sched 9190
net_rx 2710900
После анализа листингов выше видно — основное время затрачивается при обработке входящего трафика — NET_RX. И самое странное, время в обоих случаях почти одинаково — ~266ms (str01-t01-**r01) vs. ~271ms (str01-t01-**r02). Т.е. примерно от ~266ms до ~271ms тратится на NET_RX на 0-м ядре процессора (как и на всех остальных ядрах, очевидно) в каждую секунду времени. Но! в процентном соотношении это не 13% vs. 26% как мы видели на графиках выше. It seems like we need to go deeper... :(
Исследование в kernel-space
Для дальнейшего анализа и понимания проблемы, нужно немного углубиться в то, как работает SOFTIRQ. SOFTIRQ запускается когда:
system call возвращает управление в user-space;
hardware interrupt handler возвращает управление в ядро.
Упрощённо, общий процесс работы NET_RX и обработки сетевых пакетов выглядит следующим образом: драйвер сетевого интерфейса регистрирует свой callback как NAPI poll-функцию — vmxnet3_poll_rx_only в нашем случае. При поступлении очередного пакета драйвер информирует (нотифицирует) NAPI, о том, что один из softirq callback'ов готов к работе. Ядро вызывает _do__softirq функцию, которая вызывает net_rx_action функцию, которая уже в свою очередь вызывает NAPI poll-функцию vmxnet3_poll_rx_only. Далее эта функция в течении некоторого времени вычитывает сетевые пакеты из DMA-памяти сетевого интерфейса. Время работы функции обусловлено временным бюджетом (максимум до 2ms) или количеством пакетов, которые можно вычитать (до 64 пакетов за одну итерацию). В случае, если временной бюджет еще не исчерпан, и в DMA-памяти сетевого интерфейса есть еще пакеты, которые можно вычитать, происходит очередная итерация чтения пакетов. После этого ядро вызывает net_rps_send_ipi функцию (эта функция используется RPS подсистемой). И наконец вызывается функция process_backlog для непосредственной обработки вычитанных пакетов ядром (в контексте этой функции и работает ядро нашего сервиса — eBPF/XDP приложение) [6] [7] [8].
Для дальнейшего анализа нужно собрать stacktrace (с 0-го ядра) процессора на str01-t01-**r01 в течение 10 секунд:
[root@str01-t01-**r01 ~] # perf record -C 0 -g - sleep 10
Для наглядности полученный stacktrace я конвертировал во FlameGraph:
[root@str01-t01-**r01 ~] # perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > perf.data.str01.svg
И то же самое на str01-t01-**r02:
[root@str01-t01-**r02 ~] # perf record -C 0 -g - sleep 10
Полученный stacktrace также сконвертируем во FlameGraph:
[root@str01-t01-**r02 ~] # perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > perf.data.str02.svg
Используя функцию поиска — search во FlameGraph — можно найти все вызовы функции net_rx_action и время (в процентах) затраченное ей на 0-м ядре процессора. Время в обоих случаях почти одинаково — ~23.1% (str01-t01-**r01) vs. ~23.4% (str01-t01-**r02).
подобные stacktrace'ы также можно собрать с помощью скриптов из пакета BCC
[root@str01-t01-**r01 ~] # /usr/share/bcc/tools/profile -C 0 -f 10 | ~/FlameGraph/flamegraph.pl > /root/profile.str01.svg
Следующим шагом для получения более полной картины было проведено инструментирование функциий _do__softirq, net_rx_action, vmxnet3_poll_rx_only, net_rps_send_ipi и process_backlog в течение 60 секунд с помощью bpftrace c использованием скрипта softirqlat.bt [10]:
[root@str01-t01-**r01 /] # bpftrace -c 'sleep 60' /root/softirqlat.bt
Tracing latencies of
__do_softirq
|- net_rx_action
|- vmxnet3_poll_rx_only
|- net_rps_send_ipi
|- process_backlog
CPU CORE 0
__do_softirq (self 19131964 usecs, child 17371519 usecs)
|- net_rx_action (self 17371519 usecs, child 15148558 usecs)
|- vmxnet3_poll_rx_only (self 6036193 usecs, pkts received 5716452)
|- net_rps_send_ipi (self 1491625 usecs)
|- process_backlog (self 7620740 usecs, pkts processes 6445139)
CPU CORE 1
__do_softirq (self 18805067 usecs, child 17022120 usecs)
|- net_rx_action (self 17022120 usecs, child 14840980 usecs)
|- vmxnet3_poll_rx_only (self 6012230 usecs, pkts received 5697282)
|- net_rps_send_ipi (self 1373021 usecs)
|- process_backlog (self 7455729 usecs, pkts processes 6397867)
CPU CORE 2
__do_softirq (self 20225967 usecs, child 18542331 usecs)
|- net_rx_action (self 18542331 usecs, child 16473121 usecs)
|- vmxnet3_poll_rx_only (self 7742061 usecs, pkts received 8581000)
|- net_rps_send_ipi (self 1434057 usecs)
|- process_backlog (self 7297003 usecs, pkts processes 6413051)
CPU CORE 3
__do_softirq (self 18146713 usecs, child 16493474 usecs)
|- net_rx_action (self 16493474 usecs, child 14537792 usecs)
|- vmxnet3_poll_rx_only (self 5981788 usecs, pkts received 5658005)
|- net_rps_send_ipi (self 1147239 usecs)
|- process_backlog (self 7408765 usecs, pkts processes 6384332)
и
[root@str01-t01-**r02 /]# bpftrace -c 'sleep 60' /root/softirqlat.bt
Tracing latencies of
__do_softirq
|- net_rx_action
|- vmxnet3_poll_rx_only
|- net_rps_send_ipi
|- process_backlog
CPU CORE 0
__do_softirq (self 20552213 usecs, child 18688829 usecs)
|- net_rx_action (self 18688829 usecs, child 16270724 usecs)
|- vmxnet3_poll_rx_only (self 6845690 usecs, pkts received 6652314)
|- net_rps_send_ipi (self 1620819 usecs)
|- process_backlog (self 7804215 usecs, pkts processes 6392437)
CPU CORE 1
__do_softirq (self 19829660 usecs, child 17929799 usecs)
|- net_rx_action (self 17929799 usecs, child 15544991 usecs)
|- vmxnet3_poll_rx_only (self 6277612 usecs, pkts received 5687827)
|- net_rps_send_ipi (self 1392258 usecs)
|- process_backlog (self 7875121 usecs, pkts processes 6394496)
CPU CORE 2
__do_softirq (self 17845311 usecs, child 16057312 usecs)
|- net_rx_action (self 16057312 usecs, child 13931757 usecs)
|- vmxnet3_poll_rx_only (self 4932063 usecs, pkts received 3782523)
|- net_rps_send_ipi (self 1046316 usecs)
|- process_backlog (self 7953378 usecs, pkts processes 6407624)
CPU CORE 3
__do_softirq (self 20872098 usecs, child 19241081 usecs)
|- net_rx_action (self 19241081 usecs, child 17237208 usecs)
|- vmxnet3_poll_rx_only (self 8364547 usecs, pkts received 9453994)
|- net_rps_send_ipi (self 1370673 usecs)
|- process_backlog (self 7501988 usecs, pkts processes 6372464)
Обобщая результаты
Среднее время затрачиваемое в секунду на обработку трафика в NET_RX в обоих случаях почти одинаково: ~266ms vs. ~271ms;
Среднее время (в процентах) затрачиваемое в секунду на обработку трафика функцией net_rx_action в обоих случаях почти одинаково: ~23.1% vs. ~23.4%;
Среднее время затрачиваемое в секунду в функциях _do__softirq, net_rx_action, vmxnet3_poll_rx_only, net_rps_send_ipi и process_backlog в обоих случаях почти одинаково. Отклонение времени в vmxnet3_poll_rx_only на некоторых ядрах процессора зависит от количества пакетов полученных на этом ядре (если быть более точным — на rx-queue ассоциированной с соответствующим ядром процессора);
Выглядит так, что проблема где-то в SOFTIRQ аккаунтинге.
Исследование SOFTIRQ accounting'а
После чтения исходных кодов ядра Linux методом пристального вглядывания, стало понятно, что SOFTIRQ accounting производится в разных функциях, одна из них — irqtime_account_process_tick, и для нас она наиболее интересна. Комментарий в этой функции сообщает следующее:
When returning from idle, many ticks can get accounted at once, including some ticks of steal, irq, and softirq time.
Эта функция может быть включена/отключена на уровне конфига ядра с помощью опции CONFIG_IRQ_TIME_ACCOUNTING. Когда она включена, SOFTIRQ accounting производится более аккуратно, и наоборот — когда эта опция отключена, тики процессора могут быть подсчитаны неточно :(
[root@str01-t01-**r01 ~] # cat /boot/config-4.19.125-1.el7.x86_64 | grep "CONFIG_IRQ_TIME_ACCOUNTING"
# CONFIG_IRQ_TIME_ACCOUNTING is not set
и
[root@str01-t01-**r02 ~] # cat /boot/config-5.4.17-2102.200.13.el7uek.x86_64 | grep "CONFIG_IRQ_TIME_ACCOUNTING"
CONFIG_IRQ_TIME_ACCOUNTING=y
Выглядит так, что CentOS 7 с ядром 4.19.125-1 считает тики процессора не точно, а Oracle Linux 7 с ядром 5.4.17-2102.200.13.el7uek показывает более реальную статистику утилизации процессора в SOFTIRQ. Для подтверждения этой гипотезы было собрано новое ядро Oracle Linux 7 5.4.17-2102.200.13.el7uek с отключенной опцией CONFIG_IRQ_TIME_ACCOUNTING (это было сделано на str01-t01-**r03):
Левый график — нагрузка на str01-t01-**r01 с CentOS 7 с ядром 4.19.125-1 с отключенной CONFIG_IRQ_TIME_ACCOUNTING опцией;
Центральный график — нагрузка на str01-t01-**r02 с Oracle Linux 7 с ядром 5.4.17-2102.200.13.el7uek с включенной CONFIG_IRQ_TIME_ACCOUNTING опцией;
Правый график — нагрузка на str01-t01-**r03 с Oracle Linux 7 с пересобранным ядром 5.4.17-2102.200.13.el7uek с отключенной CONFIG_IRQ_TIME_ACCOUNTING опцией.
Заключение
Теперь можно точно сказать — Oracle Linux 7 c ядром 5.4.17-2102.200.13.el7uek и включенной опцией CONFIG_IRQ_TIME_ACCOUNTING более точно производит подсчет тиков процессора и показывает более реальную статистику утилизации процессора, чем CentOS 7 с ядром 4.19.125-1 и выключенной опцией CONFIG_IRQ_TIME_ACCOUNTING.
Спасибо, что прочли до конца, я буду рад вопросам и постараюсь на них ответить в комментариях.
Вместо списка литературы
https://ebpf.io/what-is-ebpf
https://www.iovisor.org/technology/xdphttps://research.google/pubs/pub44824
https://medium.com/martinomburajr/maglev-the-load-balancer-behind-googles-infrastructure-architectural-overview-part-1-3-3b9aab736f40
https://medium.com/martinomburajr/maglev-the-load-balancer-behind-googles-infrastructure-architecture-internals-part-2-3-a426c011f673https://blog.cloudflare.com/unimog-cloudflares-edge-load-balancer
https://github.blog/2018-08-08-glb-director-open-source-load-balancer
https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data
https://blog.packagecloud.io/eng/2016/10/11/monitoring-tuning-linux-networking-stack-receiving-data-illustratedhttps://people.kernel.org/dsahern/the-cpu-cost-of-networking-on-a-host
https://github.blog/2019-11-21-debugging-network-stalls-on-kubernetes
https://github.com/juise/dins-habr-blog-incr-softirq-cpu-utilization
Комментарии (17)
mSnus
19.08.2021 17:31+1Спасибо за статью, интересно. То есть реальная загрузка не выросла? Надо переходить...
Единственное -- всё же утилизация процессора это не его загруженность, а когда его сдают в цветмет и переплавляют <картинка с терминатором Т-800 в расплавленном металле>.
saboteur_kiev
20.08.2021 01:35+1consuming часто переводят как утилизация, это уже состоявшийся термин, наравне с нагрузкой и загрузкой.
Вообще, посчитать загрузку процессора в линукс - всегда было непростой задачей. С другой стороны ядро и работает чуток быстрее. за счет того, что переключение процессов дешевое.
Ion_Storm
20.08.2021 06:51+4Статья, в середине которой хочется воскликнуть "Шайтанама!!!" В очередной раз убеждаюсь, что на самом деле я не знаю линукс.
DerRotBaron
20.08.2021 10:18Доводилось заниматься чем-то подобным на DPDK. Сразу при прочтении мысль, а что если "застрелить" систему до предела по PPS, должно же наверное быть видно, что пропускная способность одинаковая?
juise Автор
22.08.2021 20:10Все верно, это был один из запасных планов. Но тут стоит понимать, что даже если скажем общее capacity по нагрузке не поменялось, то скедулер ядра (я полагаю) может вести себя по разному при утилизации процессора в 80% и в 95% и вероятно это тоже может смазать картину.
Smileedition
30.08.2021 07:47т.е. теоретические эта опция ядра будет влиять не только на отображение информации о cpu, но и давать какие эффекты производительности при высокой нагрузке?
banuchka
20.08.2021 13:23отличная статья, спасибо! Вы разложили по полочкам то, что обычно linux инженером кажется чем-то запредельно тяжелым.
XDP - это сейчас прям такой горячий пирожок, но при этом при первом приближении уровень вхождения кажется высоким. Очень надеюсь, что после Вашей статьи тех, кто в первый или не в первый раз решит посмотреть на XDP еще раз станет чуточку больше.
juise Автор
22.08.2021 20:15Спасибо. Да eBPF/XDP популярная тема, когда мы начинали этим заниматься, такое ощущение, что мы попали на пик популярности и буквально каждые пару недель выходила какая-то интересная статья. Начинали мы кстати с MVP на базе вот этого прекрасного кода от Netronome - https://github.com/Netronome/bpf-samples/ и еще пользовались вот этим - https://github.com/xdp-project/xdp-project
erlyvideo
28.08.2021 17:09+1а можете лучше рассказать, что вообще с его помощью можно сделать?
Не как, а что?
13werwolf13
20.08.2021 14:45Отличная статья, узнал немного нового и как ни странно подтвердил свои закравшиеся где-то в недрах уставшего мозга, но не до конца сформированные, подозрения..
gmlexx
21.08.2021 17:51@juise Александр, скажите пожалуйста в чем была причина перехода с Centos на Oracle Linux?
juise Автор
22.08.2021 20:20+2RedHat анонсировало CentOS 8 как последнюю стабильную версию, которую они поддерживают и предлагают всем сфокусируется на CentOS Stream, нас как компанию это не устраивает.
Nnnnoooo
Вот такие статьи и есть настоящий хабр!
Мой акк не позволяет плюсовать, но люто плюсую просто текстом :)
Кстати как общие ощущения от оракл линукса по сравнению с centos? Лично мне под нагрузкой, чисто субьективно их unbreakable kernel больше нравится, чем ядра от centos.
Nnnnoooo
И очень печально что такие статьи получают так мало плюсов :( Но оно и понятно, учитывая что хабр превратился в нучпоп ресурс от журналистов и копирайтеров
Nnnnoooo
Статья постепенно набирает плюсы!
Это позволяет верить, что в современном сообществе хабра еще есть часть от того старого уютного хабрахабра :)
Минусаторы от копирайтеров можете минусовать, если вам от этого легче. Но лично для меня хабр всегда был ресурсом с техническими статьями из IT тематики, а не научпоп развлекалочкой с кликбейтными заголовками.
P.S. Единственное что могу принять в качестве нерелевантной КПДВ это котики или сиськи/попа
juise Автор
Спасибо за ваш отзыв ) Что касается нашего впечатления об OL по сравнению с CentOS — очень бедные репозитории с пакетами, поэтому мы пытаемся использовать платные RHEL репозитории. Во всем остальном мы (engineering) разницы не заметили (кроме роста CPU :) ), а вот operations наоборот, насколько я понял, довольны регулярным выходом обновлений/патчей, каждый месяц (вроде бы так).
erlyvideo
не очень понятно, как может быть что-то беднее центоса.