Сетевая карта на загруженном сервере способна поднимать больше миллиона прерываний в секунду, и каждое из них заставляет процессор бросать текущую работу, сохранять контекст и переключаться на обслуживание очереди. Чем мельче пакеты, тем дороже обходится такой режим: ядра заняты обработкой прерываний, а полезной работы успевают меньше. Решение уже встроено в оборудование и драйверы, оно называется coalescing, то есть накопление пакетов перед прерыванием, и настраивается утилитой ethtool без пересборки ядра и без перезагрузки. В статье разобраны параметры таймера и счетчика кадров, адаптивный режим, объединение GRO и LRO, проверка результата по счетчикам и закрепление настроек после перезагрузки. Все команды работают в любом современном дистрибутиве, где установлен пакет ethtool.

Отдельное прерывание на каждый кадр и цена такого режима

Для начала стоит посчитать худший сценарий. На гигабитном канале минимальные кадры по 64 байта идут с интенсивностью 1 488 095 пакетов в секунду, кадры по 1500 байт дают 81 274 пакета в секунду. На десятигигабитном интерфейсе обе цифры умножаются на десять, и получается до 14,9 миллиона мелких или свыше 810 тысяч обычных кадров ежесекундно. Возьмем поток в 800 тысяч кадров в секунду и условную обработку одного прерывания вместе с программной частью за полторы микросекунды: на каждую секунду работы процессор потратит около 1,2 секунды только на прием, то есть одно полное ядро уйдет лишь на то, чтобы сообщать стеку о новых пакетах.

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

Современные карты распределяют потоки по нескольким очередям и прерываниям, их число показывает ethtool -l, однако суммарная нагрузка от этого меньше не становится, она лишь расползается по ядрам. Механизм накопления управляется двумя величинами: таймером rx-usecs и счетчиком rx-frames. Прерывание поднимается, как только карта наберет rx-frames кадров, либо как только с момента первого пакета пройдет rx-usecs микросекунд, смотря что случится раньше. Для передающего направления есть пара tx-usecs и tx-frames, она работает с уведомлениями о завершении отправки и экономит те же циклы процессора.

Чтение текущих параметров накопления командой ethtool

Перед изменениями стоит увидеть заводские значения. Команда ethtool -c eth0 печатает настройки накопления для интерфейса eth0, а ethtool -i eth0 показывает имя драйвера, от которого зависят поддерживаемые величины и шаги округления:

ethtool -c eth0
ethtool -i eth0

Вывод первой команды выглядит примерно так:

Adaptive RX: off  TX: off
stats-block-usecs: 0
sample-interval: 0
pkt-rate-low: 400000
pkt-rate-high: 3000000
rx-usecs: 1
rx-frames: 256
rx-usecs-irq: 0
rx-frames-irq: 256
tx-usecs: 32
tx-frames: 64
tx-usecs-irq: 0
tx-frames-irq: 256

Поле rx-usecs задает задержку в микросекундах, rx-frames определяет число кадров до прерывания. Пары с суффиксом irq описывают поведение во время обработки уже поднятого прерывания, их трогать обычно не нужно. Значения pkt-rate-low и pkt-rate-high задают границы скорости потока, при которых адаптивные драйверы применяют соответственно низкие и высокие таймеры, а sample-interval означает период измерения скорости. Драйвер округляет запрошенные числа до шагов, которые умеет железо, поэтому после каждой установки фактические значения сверяют повторным чтением ethtool -c. На виртуальной машине с драйвером virtio_net часть полей может отсутствовать, для паравиртуального устройства это нормальное поведение.

Ручной подбор таймера и счетчика кадров под профиль нагрузки

Универсальных значений не существует, профиль трафика диктует цифры. Файловому серверу, который гоняет большие объемы на скорости 10 Гбит/с, выгодно копить как можно больше: при потоке в 813 тысяч кадров в секунду на очередь значение rx-frames 64 дает около 12 700 прерываний в секунду вместо сотен тысяч, и ядра почти полностью освобождаются для прикладной работы. Интерактивным задачам, голосу и игровым серверам нужен обратный край: rx-usecs 0 и rx-frames 1 убирают искусственную задержку, но возвращают максимум прерываний. Рабочий компромисс для смешанной нагрузки обычно лежит около rx-usecs 25 и rx-frames 32, что добавляет не больше 25 микросекунд к задержке пакета и при этом режет число прерываний в разы.

Установка выполняется одной командой:

ethtool -C eth0 adaptive-rx off adaptive-tx off rx-usecs 50 rx-frames 32
ethtool -C eth0 rx-usecs 0 rx-frames 1

Первая строка настраивает узел под пропускную способность, вторая возвращает минимальную задержку, например на время теста или для узла с голосовым трафиком. Запрошенные величины драйвер приводит к ближайшей поддерживаемой сетке, поэтому значение rx-usecs 7 может превратиться в 8, а rx-frames 60 в 64, итоговый профиль сверяют по ethtool -c. Карты с независимыми очередями позволяют настраивать их по отдельности, маска задает номера очередей в шестнадцатеричном виде:

ethtool -Q eth0 queue_mask 0x3 --coalesce rx-usecs 50 rx-frames 32

Последовательность подбора на работающем сервере обычно укладывается в четыре шага:

  1. снять базовые замеры скорости и задержки при заводских настройках утилитами iperf3 и ping;
  2. установить rx-usecs 50 и rx-frames 32 и повторить те же замеры;
  3. сравнить число прерываний в /proc/interrupts и долю softirq в выводе mpstat;
  4. при потерях кадров увеличить кольцо приема командой ethtool -G и прогнать тест снова.

Адаптивный режим с порогами pkt-rate-low и pkt-rate-high

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

ethtool -C eth0 adaptive-rx on adaptive-tx on pkt-rate-low 500000 pkt-rate-high 3000000 rx-usecs-low 10 rx-usecs-high 150

Границы задают вилку: ниже 500 тысяч пакетов в секунду драйвер применяет таймер около 10 микросекунд, выше 3 миллионов уходит к 150, между порогами плавно подбирает промежуточные значения. Степень поддержки зависит от драйвера: часть карт использует ручные пороги буквально, часть при включенной адаптации работает по собственному алгоритму и игнорирует конкретные числа, поэтому фактическое поведение проверяют под реальной нагрузкой повторным чтением ethtool -c и сравнением счетчиков прерываний. Практика показывает, что адаптивный режим редко оказывается удачным для серверов с четким профилем, где одно проверенное статическое значение работает предсказуемее, зато почти всегда выигрывает на узлах с непредсказуемым смешанным трафиком.

Объединение кадров средствами GRO и LRO на уровне стека

Накопление прерываний не единственный способ сократить работу. Второй уровень, GRO и LRO, сливает несколько мелких сегментов одного потока в один большой буфер, и стек обрабатывает такой буфер как единое целое. GRO делает это в ядре и сохраняет заголовки, поэтому объединенные данные можно безопасно маршрутизировать и пробрасывать дальше. LRO выполняет слияние в самой карте и упрощает служебную информацию, из-за чего объединенные пакеты нельзя пересылать, они пригодны только локальному приложению. Отсюда правило: на серверах с маршрутизацией, NAT, мостами и туннелями LRO держат выключенным, GRO оставляют включенным, на чисто принимающих узлах LRO допустим.

Состояние функций показывает ethtool -k, переключение выглядит так:

ethtool -k eth0 | grep -E 'generic-receive|large-receive'
ethtool -K eth0 gro on
ethtool -K eth0 lro off

Эффект легко прикинуть на цифрах: без слияния стек на 10 Гбит/с разбирает порядка 813 тысяч буферов в секунду, со слиянием в блоки по 64 килобайта это около 19 тысяч блоков, то есть в десятки раз меньше структур и проходов по коду ядра. Часть карт умеет слияние и на аппаратном уровне, но включать его имеет смысл только на узлах, где объединенные блоки уходят одному локальному приложению. Проверку контрольных сумм при приеме оставляют карте: параметр rx в команде ethtool -K должен быть включен, иначе стек начнет считать суммы программно и получит лишнюю работу. GRO обычно включен по умолчанию во всех современных драйверах, задача администратора в том, чтобы его случайно не выключили во время отладки.

Проверка эффекта по счетчикам прерываний и замерам задержки

Настройка без контрольных замеров слепа, а инструменты для проверки уже есть в системе. Число аппаратных прерываний по ядрам показывает /proc/interrupts, долю времени на обработку сетевых softirq выводит mpstat, скорость и задержку меряют iperf3 и ping:

grep eth0 /proc/interrupts
mpstat -P ALL 1
iperf3 -c 10.0.0.1 -t 30 -P 4

Типичный результат на нагруженном узле выглядит так: до настройки карта поднимала 600 тысяч прерываний в секунду, ядро, обслуживающее очередь, показывало около 45 процентов в колонке %soft, а медианная задержка внутри стойки была 0,18 миллисекунды. После установки rx-usecs 50 и rx-frames 32 счетчик упал до 20 тысяч прерываний в секунду, доля %soft сократилась до 3 процентов, задержка выросла до 0,23 миллисекунды. Такой обмен считается выгодным почти всегда, исключение составляют узлы, где каждая микросекунда задержки стоит денег.

Отдельно стоит следить за потерями: крупные пачки требуют места в кольце приема, и узкое кольцо превращает выигрыш в пропуски кадров. Команда ethtool -g eth0 показывает текущие размеры колец, ethtool -S eth0 вместе с фильтром по слову drop раскрывает счетчики потерь, а увеличить кольцо позволяет команда ethtool -G eth0 rx 4096, если драйвер поддерживает такой размер. Удобно следить за счетчиком в реальном времени командой watch -n1 с grep по имени интерфейса, тогда реакция на изменение нагрузки видна сразу.

Сохранение настроек после перезагрузки и особенности virtio на VPS

Параметры накопления живут в железе только до перезагрузки, поэтому их стоит закрепить. Проще всего завести отдельный модуль systemd, который применяет настройки после подъема сети:

[Unit]
Description=Coalescing for enp3s0
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/sbin/ethtool -C enp3s0 adaptive-rx on adaptive-tx on
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Файл сохраняют как /etc/systemd/system/nic-coalesce.service и включают командой systemctl enable --now nic-coalesce.service. Похожий результат дает правило udev в файле /etc/udev/rules.d/81-coalesce.rules, которое применяет настройки в момент появления интерфейса в системе, а для сетей, управляемых NetworkManager, ту же задачу решает скрипт в каталоге /etc/NetworkManager/dispatcher.d.

Отдельного разговора заслуживают виртуальные машины: на VPS с драйвером virtio_net команда ethtool -C нередко возвращает ошибку Operation not supported, потому что настоящая карта и ее прерывания принадлежат хосту, а гостю достается виртуальная очередь. В такой ситуации накопление настраивают на хосте или через панель провайдера, а внутри гостя остаются доступными GRO и размеры очередей, поэтому там проверяют ethtool -K eth0 gro on и следят за /proc/interrupts уже на уровне виртуальных очередей. Владельцам выделенных серверов проще: ethtool меняет параметры напрямую в железе, значения применяются мгновенно, без перезапуска сети и без разрыва соединений. Связка из таймера, счетчика и GRO покрывает большинство сценариев, а адаптивный режим закрывает оставшиеся, так что настройка накопления пакетов обычно завершается за полчаса работы с ethtool и парой контрольных замеров.