Что такое прерывание и почему сетевая карта отбирает у вас целое ядро
Сервер вроде бы не перегружен: top показывает свободные проценты, приложение не жалуется на память, диск не упирается в лимиты. А сайт всё равно подтормаживает под нагрузкой, и если приглядеться к top внимательнее, обнаруживается одно ядро, которое почти всё время сидит в %si — обработке программных прерываний — пока соседние ядра скучают. Разбираемся, что физически происходит внутри процессора, когда приходит сетевой пакет, почему за это может отвечать одно-единственное ядро, и как современный Linux умеет размазывать эту работу по всем ядрам.
Содержание
- Что физически происходит, когда приходит пакет
- Hard IRQ и softirq: почему обработка разбита на две части
- Почему нагрузка ложится на одно ядро
- RSS и множество очередей: как это решают на уровне железа
- IRQ affinity и irqbalance: кто раздаёт прерывания по ядрам
- Практические следствия для серверов с высокой сетевой нагрузкой
Что физически происходит, когда приходит пакет
Сетевая карта — это устройство, которое живёт своей жизнью независимо от того, чем занят процессор в данный момент. Пакет из сети приходит не по расписанию: он может прилететь, пока ядро выполняет ваш код, ждёт в цикле планировщика или обслуживает диск. У процессора нет способа «периодически проверять», не пришло ли что-то новое по сети — точнее, такой способ (опрос, polling) существует, но он расточителен: гонять ядро в холостом цикле «проверка — нет пакета — проверка» ради событий, которые случаются непредсказуемо, значит жечь такты впустую даже тогда, когда сеть молчит.
Поэтому используется обратная схема: устройство само сообщает процессору, что у него есть данные. У сетевой карты есть физическая линия прерывания (IRQ, Interrupt Request), подключённая к контроллеру прерываний материнской платы. Когда в буфер карты попадает пакет, она выставляет сигнал на этой линии. Контроллер прерываний доводит сигнал до конкретного ядра CPU и заставляет его немедленно остановить то, что оно делало: сохранить состояние регистров и указатель на текущую инструкцию, переключиться в привилегированный режим и передать управление обработчику прерывания, зарегистрированному драйвером карты. Именно это имеется в виду под «сетевая карта прерывает ядро»: не метафора, а буквальная принудительная остановка выполняемого кода ради обработки события.
Важный нюанс: прерывание всегда приходит на конкретное ядро, а не на процессор «в среднем». Какое именно ядро получит сигнал — решает связка контроллера прерываний, драйвера и настроек affinity, к которым мы вернёмся ниже. Но по умолчанию, если ничего специально не настраивать, у карт с одной очередью прерываний весь этот трафик годами может идти на одно и то же ядро — например, ядро 0, просто потому что так сложилось при инициализации драйвера.
Hard IRQ и softirq: почему обработка разбита на две части
Обработчик аппаратного прерывания (hard IRQ handler) должен работать предельно быстро — пока он выполняется, на этом ядре обычно маскируются другие прерывания того же уровня, и если бы вся обработка пакета — разбор заголовков, проверка контрольной суммы, передача данных в стек TCP/IP, побудка ожидающего процесса — происходила прямо здесь, ядро надолго теряло бы способность реагировать на что-либо ещё. Поэтому в Linux (и не только) обработка разделена на два этажа.
Первый этаж — верхняя половина (top half), собственно hard IRQ handler. Она делает минимум: подтверждает карте приём пакета, забирает данные из кольцевого буфера DMA и ставит задачу в очередь дальнейшей обработки. Всё это — считаные микросекунды.
Второй этаж — нижняя половина (bottom half), в Linux реализована через softirq (программное прерывание). Ядро планирует softirq-обработчик, который выполнится, как только текущий hard IRQ handler завершится и разрешатся прерывания. Именно здесь происходит вся тяжёлая часть: разбор пакета сетевым стеком (NET_RX_SOFTIRQ), продвижение по TCP/IP, доставка данных в сокет приложения.
Если softirq-обработчиков накопилось больше, чем ядро успевает разгрести за отведённое время (Linux ограничивает число проходов softirq-цикла за один заход, чтобы не заморозить остальную систему), оставшаяся работа передаётся ядерному потоку ksoftirqd/N — по одному на каждое ядро. Именно поэтому при экстремальной сетевой нагрузке в top можно увидеть ksoftirqd/0, потребляющий заметную долю CPU: это признак того, что штатного окна softirq не хватает. Про ситуацию, когда именно этот поток упирается в потолок на одном ядре, есть отдельный разбор — ksoftirqd на 100%: прерывания на одном ядре.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПочему нагрузка ложится на одно ядро
Вот здесь и рождается проблема из заголовка. Если сетевая карта или её драйвер настроены на одну очередь приёма (single RX queue), то все аппаратные прерывания от этой карты физически приходят на одно и то же ядро — то, которому исторически назначена эта линия IRQ. А раз hard IRQ обслуживается ядром X, то и softirq, запланированный из этого обработчика, по умолчанию выполняется тоже на ядре X (softirq выполняется на том ядре, где было обработано породившее его прерывание, — это архитектурное решение, а не случайность).
В результате получается воронка: сколько бы ядер ни было у сервера, весь путь «пакет пришёл → карта дёрнула IRQ → softirq разобрал заголовки → данные попали в сокет» для входящего трафика проходит через одно ядро. Если поток пакетов в секунду достаточно велик — много мелких соединений, DDoS-подобная нагрузка, интенсивный UDP-трафик, множество TCP-хендшейков одновременно, — это ядро может физически не успевать и тратит все такты на приём и разбор пакетов.
Внешне это выглядит парадоксально. top в агрегированном режиме может показывать, что CPU загружен на треть — ложное ощущение запаса. Но если посмотреть загрузку по каждому ядру (в top — клавиша 1, либо mpstat -P ALL 1), картина другая: одно ядро на 100%, из них почти всё — %si, остальные простаивают. Среднее «по больнице» маскирует то, что критичный для сети ресурс — конкретное ядро — исчерпан полностью. Похожая ловушка «в среднем всё в порядке, но конкретный ресурс в потолке» разбирается в статье про диагностику высокой нагрузки на процессор.
Проверить, какие ядра реально обслуживают прерывания конкретной карты, можно через /proc/interrupts:
watch -n1 "grep eth0 /proc/interrupts"
Если вывод показывает, что счётчик растёт только в одной колонке (то есть только у одного CPU), а остальные колонки стоят на месте — это прямое подтверждение single-queue сценария или неудачно настроенного affinity.
RSS и множество очередей: как это решают на уровне железа
Современные серверные сетевые карты (Intel, Mellanox/NVIDIA, большинство карт в виртуализированных облаках через virtio-net с multiqueue) умеют не одну очередь приёма, а несколько — обычно по числу ядер или их части. Технология, которая распределяет входящие пакеты по этим очередям, называется RSS — Receive Side Scaling.
Работает это так: карта считает хеш от полей пакета (исходный и целевой IP-адрес, исходный и целевой порт) и по этому хешу решает, в какую из своих аппаратных очередей положить пакет. У каждой очереди — своя линия прерывания (в современных системах это чаще MSI-X, а не единая legacy-линия), и эти прерывания раскидываются по разным ядрам. Пакеты одного TCP-соединения всегда попадают в одну очередь — иначе они обрабатывались бы вразнобой и переупорядочивались, — а разные соединения размазываются по ядрам почти равномерно.
Посмотреть, сколько очередей поддерживает и реально использует карта, можно через ethtool:
ethtool -l eth0
Вывод покажет Pre-set maximums (сколько очередей карта в принципе умеет) и Current hardware settings (сколько включено сейчас). Если сервер многоядерный, а очередей включено меньше, чем ядер, увеличить их число (в пределах поддержки драйвера) можно так:
ethtool -L eth0 combined 8
Если карта старая или виртуальная и multiqueue не поддерживает, у Linux есть программный аналог — RPS (Receive Packet Steering) и RFS (Receive Flow Steering). RPS эмулирует RSS программно: softirq-обработчик, получив пакет на одном ядре, сам распределяет дальнейшую обработку на другие ядра по такому же хешу — ценой небольшого оверхеда на межъядерную сигнализацию. RFS идёт дальше и старается направлять пакеты на ядро, где выполняется процесс-получатель, учитывая локальность данных в кэше CPU. Включается это записью битовых масок в /sys/class/net/eth0/queues/rx-0/rps_cpus — маску нужных ядер приходится прописывать руками для каждой очереди.
IRQ affinity и irqbalance: кто раздаёт прерывания по ядрам
Даже когда очередей несколько, кто-то должен решить, прерывание какой очереди пойдёт на какое ядро — это называется IRQ affinity (привязка прерывания к ядру или набору ядер). Задать её вручную можно через /proc/irq/<номер>/smp_affinity_list, куда пишется список ядер (или через устаревшую битовую маску smp_affinity):
echo 3 > /proc/irq/128/smp_affinity_list
Вручную это делают редко и обычно только тогда, когда точно знают топологию: например, хотят прибить прерывания карты к ядрам того NUMA-узла, к которому она физически подключена по PCIe — иначе обработка пакета будет упираться в межпроцессорную шину при обращении к памяти. Почему это вообще имеет значение на многосокетных системах — в отдельном разборе про NUMA и когда два CPU медленнее одного. Топологию NUMA-узлов показывает numactl --hardware.
В большинстве дистрибутивов по умолчанию за динамическую балансировку прерываний отвечает демон irqbalance. Он периодически (обычно раз в несколько секунд) смотрит на загрузку по каждому прерыванию и по каждому ядру и перераспределяет affinity так, чтобы не концентрировать всё на одном месте, стараясь при этом учитывать кэш-локальность и топологию NUMA. Проверить, что он вообще запущен, просто:
systemctl status irqbalance
Есть практический нюанс: если вы вручную прописали IRQ affinity для тонкой настройки (например, зарезервировали конкретные ядра под сетевой стек), а irqbalance работает в дефолтном режиме, он может через какое-то время «переиграть» ваши ручные настройки обратно — демон не знает о вашем намерении. В таких случаях либо исключают нужные прерывания флагом --banirq, либо задают диапазон ядер через IRQBALANCE_BANNED_CPULIST в конфиге (/etc/sysconfig/irqbalance или /etc/default/irqbalance), либо просто останавливают демон и держат affinity полностью ручной.
Практические следствия для серверов с высокой сетевой нагрузкой
Из всего сказанного вытекает несколько вещей, которые стоит проверять заранее, а не в момент, когда сайт уже подтормаживает под нагрузкой.
Во-первых, для проектов с интенсивным сетевым трафиком — высоконагруженный веб-сервер, прокси, VPN-концентратор, DNS-резолвер под большим потоком запросов — важно не просто количество ядер, а то, реально ли карта и драйвер используют multiqueue и включён ли irqbalance (или настроен RPS, если карта виртуальная и multiqueue не поддерживает). Виртуальные сети некоторых гипервизоров исторически отдают VPS одну очередь на virtio-net вне зависимости от числа vCPU — это стоит проверять через ethtool -l сразу после разворачивания сервера, а не постфактум при первых признаках нагрузки.
Во-вторых, если по mpstat -P ALL 1 видно, что одно ядро стабильно перегружено %si, а остальные простаивают, — это симптом, который не лечится покупкой тарифа с бóльшим числом ядер: дополнительные ядра не помогут, если весь входящий трафик по-прежнему упирается в одну очередь. Здесь помогает распределение обработки: дополнительные очереди на карте, донастройка affinity, либо RPS/RFS там, где аппаратного RSS нет.
В-третьих, для интенсивной входящей нагрузки — прежде всего UDP и множества мелких TCP-соединений, где число прерываний в секунду особенно велико, — стоит смотреть не только на загрузку CPU, но и на счётчик dropped в ip -s link: если ядро не успевает вычитывать буфер карты, пакеты отбрасываются ещё до попадания в стек приложения, и это заметно раньше, чем сервис «почувствует» проблему на уровне логов.
В-четвёртых, «ядра» в облаке — не всегда равноценные физические ядра с гарантированным доступом к прерываниям карты: на части виртуализированных платформ обработка сетевых прерываний конкурирует за ресурсы хоста с другими арендаторами. Если для проекта критична именно сетевая пропускная способность (стриминг, игровой сервер, прокси с большим числом соединений), разумнее смотреть в сторону конфигураций с выделенными ресурсами и проверенной поддержкой multiqueue, а не полагаться на «ядер много — значит, справится». При высоком pps (пакетов в секунду) число прерываний, а не мегабиты в секунду, часто становится узким местом раньше канала — подробнее в статье про сетевой канал 1G против 10G.
Наконец, при асимметричной нагрузке — сервер в основном принимает трафик, а не отдаёт — иногда осмысленно резервировать под сетевые прерывания отдельный пул ядер и исключить их из пула, где планируется прикладной код (через taskset или cgroups), чтобы обработка сети и приложения не конкурировали за один кэш и один квант времени. Это не универсальный совет — для большинства проектов с умеренной нагрузкой в этом нет смысла, и default-конфигурация с включённым irqbalance справляется сама.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему на VPS я не вижу нескольких очередей на сетевой карте, хотя ядер выделено много?
Это зависит от гипервизора и модели виртуального сетевого адаптера. Часть платформ отдаёт virtio-net с поддержкой multiqueue и включает по очереди на vCPU, часть — по умолчанию одну очередь независимо от числа ядер. Проверить фактическое число очередей можно командой ethtool -l eth0 сразу после получения сервера.
%si в top — это всегда плохо?
Нет, ненулевой %si — нормальная часть работы сетевого стека на любом сервере с трафиком. Тревожный сигнал — это именно перекос: одно ядро стабильно у потолка по %si, а остальные почти свободны, особенно если это совпадает с ростом задержек или таймаутами у клиентов.
Чем RPS отличается от RSS, если по сути делают одно и то же?
RSS — это аппаратная функция самой сетевой карты: хеш и распределение по очередям считает чип, до того как пакет вообще попал в память процессора для обработки ядром. RPS — программная эмуляция того же принципа средствами ядра Linux для карт, которые multiqueue не поддерживают; она требует чуть больше CPU-времени на межъядерную координацию, но даёт похожий эффект распределения нагрузки.
Если включить больше очередей на карте, это гарантированно снимет проблему?
Само по себе увеличение числа очередей не поможет, если прерывания от них при этом всё равно физически привязаны к одному-двум ядрам или если irqbalance выключен и affinity никто не пересчитывает. Число очередей и их фактическое распределение по ядрам — это два разных параметра, проверять нужно оба: ethtool -l для очередей и /proc/interrupts для их реального распределения.
Нужно ли обычному сайту вообще заниматься этой темой?
Для среднего проекта с типичной нагрузкой достаточно, чтобы карта поддерживала multiqueue и был включён штатный irqbalance — этого хватает практически всегда. Ручная настройка affinity и резервирование ядер под сеть оправданы только при действительно высоком pps: прокси, VPN-концентраторы, DNS под большой нагрузкой, стриминг с множеством одновременных соединений.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →