MAATRIX / Блог / ksoftirqd на 100%: все прерывания сети сидели на одном ядре

ksoftirqd на 100%: все прерывания сети сидели на одном ядре

MAATRIX

Однажды на выделенном сервере с восемью ядрами один-единственный процесс ksoftirqd/0 держал целое ядро на 100% нагрузки, пока остальные семь скучали на 5-10%. Снаружи это выглядело как случайные подтормаживания API под нагрузкой: p95 задержки прыгали в три-четыре раза, хотя суммарная загрузка процессора была смешной. Разбирались три дня, перебрали десяток гипотез, а причина оказалась в одной строке конфигурации сетевой карты, которую никто не трогал полгода.

Как это выглядело: первые сигналы

Мониторинг поднял алерт не по CPU в среднем (node_cpu_seconds_total в сумме держался около 25%), а по латентности верхних персентилей на API-эндпоинтах. Заявки от пользователей были расплывчатые: «иногда сайт тупит», «бывает долго грузится корзина». Классическая ситуация, когда агрегированные метрики врут, потому что усредняют разное поведение восьми ядер в одно число.

Первым делом открыли top прямо на сервере:

top - 14:32:07 up 41 days,  3:12,  2 users,  load average: 2.14, 1.98, 2.05
%Cpu0  :  2.0 us,  1.3 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi, 96.7 si,  0.0 st
%Cpu1  :  4.1 us,  2.0 sy,  0.0 ni, 93.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  3.8 us,  1.9 sy,  0.0 ni, 94.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
...
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    9 root      20   0       0      0      0 R  97.3  0.0 612:44.10 ksoftirqd/0

Колонка si (softirq) на CPU0 была почти 97%, на остальных — ноль. ksoftirqd/0 — это не баг и не вредонос, а штатный поток ядра, который обрабатывает софт-прерывания, если их накопилось слишком много, чтобы забрать всё прямо в контексте железного прерывания. Когда он сам жрёт ядро целиком, это значит, что поток входящих пакетов на этом ядре физически не успевает обрабатываться.

Похожий разбор общей методологии поиска причины нагрузки на CPU (не только сетевой) есть в статье как найти причину высокой нагрузки на процессор — но здесь картина была специфическая: один поток ядра, один CPU, остальные простаивают.

Что отбросили в первый день

Первая гипотеза — DDoS или аномальный всплеск трафика. Проверили sar -n DEV 1 и графики Netdata по интерфейсу:

14:32:01       eth0     18452.30      9210.10   21340.55   4820.12      0.00      0.00      1.20

18 тысяч пакетов в секунду на вход — заметно, но не запредельно для сервера с 8 ядрами и 10G-линком. Для сравнения полосы и заявленной скорости порта у нас есть отдельный разбор в статье про канал 1G против 10G, но сути дела он не менял: узким местом была не полоса, а количество пакетов в секунду и то, как они распределялись по ядрам.

Вторая гипотеза — виноват один из сервисов, который жрёт CPU синхронно с сетью (например, TLS-терминация без аппаратного ускорения). Проверили через pidstat -u 1 5 — ни один пользовательский процесс не показывал аномальной загрузки. iostat -x 1 тоже был спокоен: %util дисков в пределах 10-15%, очередей нет. Гипотезу про диск отбросили быстро.

Третья гипотеза — NUMA-эффекты: сервер двухсокетный, и подумали, что сетевая карта физически привязана к одному узлу NUMA, а обработчик прерываний скачет между узлами, теряя время на межсокетный трафик по QPI/UPI. Разбор того, когда NUMA реально роняет производительность, есть в статье что такое NUMA и когда два CPU медленнее одного. Проверили numastat и lstopo — сетевая карта действительно сидела на node0, но само по себе это не объясняло, почему нагрузка концентрируется именно на CPU0, а не размазывается по ядрам node0. Гипотезу отложили как вторичный фактор, не как причину.

Четвёртая гипотеза, самая соблазнительная — «просто мало ядер, надо больше vCPU». Ее отбросили последней, потому что цифры не сходились: если бы не хватало суммарной вычислительной мощности, была бы нагружена вся машина, а не одно ядро на 97% при семи простаивающих.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

/proc/interrupts: вся сеть висела на одном ядре

Ключевую подсказку дал /proc/interrupts:

$ watch -n1 'cat /proc/interrupts | grep -E "CPU|eth0"'
           CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7
 24:    8234123          0          0          0          0          0          0          0   PCI-MSI 32768-edge      eth0

Всего одна строка прерываний для eth0, и вся она росла только в столбце CPU0. Для сравнения нормальная картина на многоядерном сервере с настроенной сетью — это 4-8 строк eth0-TxRx-0eth0-TxRx-7, каждая приписана к своему ядру. У нас была ровно одна очередь.

Проверили количество очередей на карте:

$ ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
TX:             0
RX:             0
Other:          0
Combined:       8
Current hardware settings:
TX:             0
RX:             0
Other:          0
Combined:       1

Карта физически поддерживала 8 комбинированных очередей (Combined: 8 в максимумах), но фактически работала с одной (Combined: 1). Все входящие пакеты хешировались в одну RX-очередь, эта очередь генерировала прерывания только на одно ядро, и как только пакетов стало достаточно много — обработчик прерывания перестал успевать в контексте hardirq и передал работу ksoftirqd, который тоже упёрся в потолок одного ядра.

Дополнительно проверили irqbalance — демон, который в норме раскидывает прерывания по ядрам:

$ systemctl status irqbalance
● irqbalance.service - irqbalance daemon
     Loaded: loaded (/lib/systemd/system/irqbalance.service; disabled; vendor preset: enabled)
     Active: inactive (dead)

disabled и inactive — демон не работал уже давно. Но даже если бы он работал, при Combined: 1 балансировать было физически нечего: у одной очереди одно прерывание, и его некуда «размазать» — irqbalance управляет распределением IRQ между ядрами, а не разбивает пакетный поток на подпотоки.

Почему так вышло: single-queue virtio-net и забытый апгрейд

Сервер оказался виртуальной машиной поверх KVM (гость с драйвером virtio-net). Когда виртуалку изначально создавали полтора года назад под задачу с низкой сетевой нагрузкой, для неё выделили virtio-net без multiqueue — это дефолт в старых шаблонах и не влияет на работу, пока трафика немного. Дальше сервис вырос, добавили ядер (2 → 8), но конфигурацию виртуального сетевого адаптера никто не пересматривал: добавление vCPU не трогает настройки очередей NIC автоматически.

Итог: 8 вычислительных ядер, но сетевой стек как будто остался на однопроцессорной машине образца пары лет назад. Под старой нагрузкой это было незаметно, потому что даже одно ядро справлялось. Рост RPS на 30-40% за последние месяцы (органический рост продукта, никакой атаки) вывел это ограничение наружу.

Похожая логика применима и к физическим серверам: если сетевая карта поддерживает RSS (Receive Side Scaling), но в BIOS/драйвере включена только одна очередь, или если сервер работает в виртуалке с урезанным количеством очередей у vNIC, эффект будет тот же — весь входящий трафик стягивается на одно ядро. Вопрос привязки ядер и её последствий подробнее разобран в статье настройка CPU для виртуалки: типы и привязка.

Как чинили: multiqueue, RPS и XPS

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

Шаг 1. Multiqueue на стороне гипервизора. Для virtio-net количество очередей задаётся в конфигурации домена. В libvirt XML это выглядит так:

<interface type='network'>
  <model type='virtio'/>
  <driver name='vhost' queues='8'/>
</interface>

Значение queues должно соответствовать числу vCPU гостя (в нашем случае 8). После правки XML домен пришлось выключить и включить заново (не reboot гостевой ОС, а полный power cycle на уровне гипервизора — virsh destroy + virsh start), потому что горячее добавление очередей virtio-net не поддерживается.

Если управляете сервером через QEMU напрямую, эквивалент — параметры mq=on,vectors=N в описании -netdev/-device virtio-net-pci.

Шаг 2. Включить очереди внутри гостя. После перезапуска домена карта уже видела 8 доступных очередей, но по умолчанию использовала не все:

$ ethtool -L eth0 combined 8
$ ethtool -l eth0
Current hardware settings:
Combined:       8

Шаг 3. Проверить IRQ affinity и включить irqbalance.

$ systemctl enable --now irqbalance
$ cat /proc/interrupts | grep eth0
 24:  102340    98213    99871   101204   100562    97654    99320   100011   PCI-MSI-edge      eth0-TxRx-0
 25:   99871    97654   102340    98213   101204   100562   100011    97654   PCI-MSI-edge      eth0-TxRx-1
...

Восемь строк вместо одной, и цифры в столбцах CPU0-CPU7 теперь растут примерно равномерно.

Шаг 4. RPS/XPS как страховка. Даже с восемью аппаратными очередями оставили включённым Receive Packet Steering — на случай, если хеш-функция карты не идеально равномерно распределит поток (например, при малом числе одновременных TCP-соединений хеш по 5-tuple может лечь неудачно на пару ядер):

for q in /sys/class/net/eth0/queues/rx-*; do
  echo ff > $q/rps_cpus
done

for q in /sys/class/net/eth0/queues/tx-*; do
  echo ff > $q/xps_cpus
done

ff в масках означает «все 8 ядер доступны» (битовая маска 11111111). Это правило не переживает перезагрузку, поэтому вынесли в systemd unit, который выполняется после поднятия интерфейса:

[Unit]
Description=RPS/XPS tuning for eth0
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/tune-rps-xps.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Как проверяли, что починили

Контроль делали в три этапа. Сначала — снова mpstat -P ALL 1, чтобы убедиться, что %soft распределён по ядрам, а не сконцентрирован на одном:

$ mpstat -P ALL 1 5
Average:     CPU    %usr   %sys  %soft   %idle
Average:     all    3.20   1.85   4.10   90.85
Average:       0    3.41   1.90   4.62   90.07
Average:       1    3.02   1.77   3.95   91.26
Average:       2    3.15   1.83   4.05   90.97
...

Разброс %soft между ядрами стал в пределах пары процентных пунктов вместо разрыва «97% на одном, 0% на остальных». Дальше две недели смотрели на p95/p99 задержки API в дневные пиковые часы — они выровнялись и перестали давать скачки, коррелирующие с ростом входящего трафика. Ориентировочно (это не паспортное число, а наблюдение именно на этом сервере и этой нагрузке) верхние персентили латентности снизились в несколько раз именно в моменты пиковой нагрузки — раньше именно там были всплески, и именно они пропали. Общую загрузку CPU специально не сравнивали как главный критерий: она и раньше выглядела нормально в среднем, проблема была именно в неравномерности между ядрами.

Отдельно проверили, что после перезагрузки хоста (плановое окно обслуживания через месяц) настройки очередей и RPS/XPS применились автоматически — без этого чинить одно и то же вручную после каждого рестарта было бы бессмысленно.

Что изменили в процессе после инцидента

Кроме точечного фикса, добавили в мониторинг отдельную проверку на неравномерность %soft между ядрами (алерт срабатывает, когда разброс между максимальным и минимальным значением si по ядрам превышает заданный порог продолжительное время), а не только на среднюю загрузку CPU. Такая метрика ловит именно этот класс проблем — она видна задолго до того, как ksoftirqd упрётся в 100% на одном ядре и начнутся заметные задержки.

Также завели чек-лист для новых виртуалок: при выделении больше 2 vCPU под сеть — сразу проверять ethtool -l на количество активных очередей и включать multiqueue на этапе создания VM, а не постфактум под нагрузкой. Это дешевле сделать один раз в момент создания сервера, чем потом ловить нагрузочный инцидент через полтора года.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как быстро понять, что дело именно в прерываниях, а не в обычной нагрузке приложения?

Смотрите на колонку si в top или mpstat -P ALL. Если один CPU показывает высокий %soft при низком %usr/%sys, а остальные простаивают — это сигнал именно про софт-прерывания и сеть, а не про код приложения.

Нужно ли включать multiqueue на маленьком сервере с 1-2 ядрами?

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

Помогает ли просто добавить ядер (vCPU), если проблема в одной очереди?

Нет. Прерывания одной аппаратной очереди физически обслуживаются одним ядром вне зависимости от того, сколько вычислительных ядер есть у машины в целом. Добавление vCPU без включения multiqueue не решает проблему — именно это и произошло в нашем случае.

Можно ли обойтись только RPS/XPS без аппаратного multiqueue?

Можно, и это рабочий вариант для сетевых карт или виртуальных адаптеров, которые физически поддерживают только одну очередь. RPS распределяет обработку пакетов программно между ядрами уже после того, как пакет попал в ядро ОС, но не убирает нагрузку с самого прерывания на приёме — поэтому аппаратные очереди, если они доступны, предпочтительнее.

Как понять, что причина именно в конфигурации виртуальной машины, а не в физическом железе?

Проверьте ethtool -l на предмет Pre-set maximums — если максимум больше, чем текущее значение Combined, потенциал для очередей есть, просто он не задействован. Если максимум и есть текущее значение совпадают и равны 1 — ограничение либо в драйвере, либо в конфигурации виртуального адаптера на уровне гипервизора.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →