Каждый гигабит трафика можно пропустить через процессор, а можно отдать большую часть рутины сетевому адаптеру. Разрезание больших буферов на пакеты, склейка мелких на приёме, подсчёт контрольных сумм, расстановка VLAN-тегов, всё это давно умеет железо, и ядро Linux умеет включать и выключать каждую задачу отдельно. Где проходит граница между адаптером и процессором, как её увидеть и как перестроить под свою нагрузку, разбирается ниже, от чтения состояния через ethtool -k до аппаратной разгрузки правил фильтрации.
Разделение работы между адаптером и процессором
Сетевой стек на передаче собирает данные в буфер и режет его на сегменты размером с MTU, каждый сегмент обзаводится заголовками и контрольной суммой. На приёме происходит обратная работа: мелкие пакеты одного потока склеиваются, checksum проверяется, VLAN-тег снимается. Каждая из этих операций стоит тактов на каждом пакете, и при MTU 1500 гигабитный поток превращается в десятки тысяч событий в секунду.
Оффлоад переносит часть таких операций в железо адаптера. Процессор перестаёт считать контрольные суммы по байтам, перестаёт резать и клеить, а драйвер получает укрупнённые буферы. Выигрыш двоякий: меньше тактов на байт и меньше прерываний, потому что одно аппаратное событие покрывает целый поток пакетов. Обратная сторона в том, что часть логики уходит из поля зрения ядра, и при отладке или специфичных сценариях отдельные оффлоады приходится выключать. Поэтому в Linux каждая функция управляется независимо, и инструментов для этого ровно два: ethtool для классических оффлоадов и ip link для тонких пределов GSO.
Граница между адаптером и процессором исторически ползла в сторону железа вместе с ростом скоростей линков. Отдельная тема связана с MTU: jumbo-кадры в 9000 байт уменьшают число пакетов в шесть раз, но требуют одинакового MTU на всём пути, включая коммутаторы. Оффлоады дают похожий эффект даже со стандартным MTU 1500, потому что крупный буфер живёт только внутри хоста, а на проводе уходит обычная нарезка. Отсюда частая связка на магистралях: стандартный MTU наружу плюс TSO и GRO внутри.
Просмотр состояния оффлоадов командой ethtool -k
Текущую раскладку показывает ethtool -k eth0, фрагмент типичного вывода:
rx-checksumming: on
tx-checksumming: on
scatter-gather: on
tcp-segmentation-offload: on
generic-segmentation-offload: on
generic-receive-offload: on
large-receive-offload: off [fixed]
rx-vlan-offload: on
ntuple-filters: off
receive-hashing: on
Каждая строка это отдельный переключатель. Приписка fixed означает, что драйвер не позволяет менять значение, например адаптер без аппаратного LRO всегда покажет large-receive-offload: off [fixed]. Полный вывод длиннее и включает варианты контрольных сумм по протоколам, аппаратную фрагментацию и поведение при сна, но базовая картина складывается из перечисленных десяти строк.
Изменение выполняется через ethtool -K с большой буквой:
ethtool -K eth0 tso on
ethtool -K eth0 gro on
ethtool -K eth0 lro off
ethtool -K eth0 rxvlan off
Короткие имена после ключа совпадают с названиями в выводе -k без дефисов. Некоторые драйверы при смене оффлоада пересоздают очереди, на долю секунды интерфейс пропадает из состояния up, поэтому на рабочем маршрутизаторе изменение делают через bond или в окно обслуживания.
Сегментация на передаче через TSO и GSO
TSO, tcp-segmentation-offload, переносит разрезание исходящего TCP-потока в адаптер. Ядро передаёт драйверу один большой буфер, до 64 КБ, с готовыми заголовками первого сегмента, а адаптер режет его на сегменты по MSS, обновляет номера последовательности и считает контрольные суммы. При MTU 1500 один буфер превращается примерно в сорок четыре пакета, и вся рутина заголовков выполняется железом за один проход.
TSO требует включённой аппаратной суммы: разрезая буфер, адаптер сам проставляет контрольную сумму каждому сегменту. Попытка выключить tx-checksumming при активном TSO откатит оба флага, драйвер сообщит предупреждением, а ethtool -k покажет согласованные значения. Аналогичная связка действует между scatter-gather и TSO: без разбросанной памяти адаптеру нечего резать.
GSO, generic-segmentation-offload, это программная заглушка того же механизма. Если адаптер не умеет TSO, ядро само разрезает большой буфер уже на уровне драйвера, что всё равно дешевле полного пути, потому что верхние слои отработали один раз на весь буфер. Современные драйверы виртуальных машин часто опираются именно на GSO, а гипервизор доделывает разрезание у себя.
Тонкая настройка размеров идёт через ip link:
ip link set dev eth0 gso_max_size 65536
ip link set dev eth0 gso_max_segs 64
Эти пределы говорят стеку, насколько крупный буфер можно отдать на сегментацию. Для локальных копий между серверами 10G увеличение gso_max_size заметно снижает нагрузку на процессор, а для шлюза с маленькими очередями разумнее оставить значения по умолчанию, чтобы не раздувать задержку под нагрузкой.
Приём и укрупнение пакетов через GRO и LRO
GRO, generic-receive-offload, склеивает на приёме пакеты одного потока в один большой буфер до тех же 64 КБ. Склейка происходит в ядре, в слое драйвера, с сохранением полной информации о каждом фрагменте, поэтому укрупнённый буфер можно смело отправить дальше, хоть в приложение, хоть в пересылку на другой интерфейс. Число прерываний и вызовов стека падает во столько же раз, во сколько склеились пакеты.
Цена склейки видна в счётчиках прерываний: на приёме 10G без GRO легко собрать сотни тысяч прерываний в секунду на ядро, с включённым GRO то же количество байт упаковывается в десятки тысяч событий. Дополнительно склейка экономит на заголовках: один заголовок Ethernet приходится на весь укрупнённый буфер, а не на каждый пакет. Наглядный пример даёт сетевая копия между двумя серверами: с выключенными TSO и GRO обработчик прерывания упирается в потолок на потоке в несколько гигабит, с включёнными оффлоадами тот же трафик уходит в счётчики адаптера, и загрузка ядра заметно сокращается.
LRO, large-receive-offload, старший аппаратный родственник GRO: склейку делает сам адаптер, но до ядра доходит буфер с изменёнными заголовками, по которому уже нельзя корректно восстановить маршрут. Отсюда классическое правило: LRO включают на конечных серверах с локальными приложениями и выключают на маршрутизаторах и шлюзах, иначе forwarding ломается. GRO лишена этого дефекта, поэтому на современных системах LRO чаще всего сидит в состоянии off или fixed.
Практическая рекомендация проста: на приёме GRO держат включённым для быстрых интерфейсов, а выключают на маленьких виртуальных машинах и в цепочках с глубоким DPI, где логика разбора сама делит потоки.
Контрольные суммы, VLAN и разгрузка хешей RSS
Контрольные суммы считаются адаптером на передаче и проверяются на приёме, ядро получает готовый вердикт в дескрипторе пакета. Флаги scatter-gather и tx-checksumming связаны: аппаратная сумма требует, чтобы данные лежали в разбросанных страницах, и собирать их в один линейный буфер не нужно. Проверка на приёме помечается в skb, и если адаптер не смог посчитать сумму, пакет проходит программную проверку без отбрасывания.
VLAN-оффлоады снимают и ставят тег в железе: rx-vlan-offload отдаёт ядру пакет уже без тега, но с меткой VLAN в метаданных, tx-vlan-offload вставляет тег на проводе. Виртуальные интерфейсы vlan-устройств опираются на эти флаги, и их отключение сразу заметно по росту нагрузки на процессор на тегированных магистралях. Выключение rxvlan встречается редко и почти всегда в паре с XDP-программами, которым нужен сырой кадр с тегом на месте.
receive-hashing это аппаратный RSS: адаптер считает хеш от заголовков и кладёт пакет в очередь по результату. Отсюда начинается распределение трафика по ядрам, о котором подробно говорится в статьях про очереди и прерывания. Связка RSS с оффлоадами прямая: чем крупнее буферы после GRO, тем важнее, чтобы потоки честно разъезжались по очередям, иначе вся выгода склейки упрётся в одно ядро.
Аппаратная разгрузка правил через ntuple и XDP
ntuple-filters переводит адаптер в режим приёма правил фильтрации от пользователя. Включение делается через ethtool -K eth0 ntuple on, после чего ядру можно скармливать правила:
ethtool -N eth0 flow-type tcp4 dst-ip 10.0.0.15 dst-port 5432 action 2
Такое правило уводит весь помеченный поток прямо в очередь номер 2, минуя хеш RSS. Это основа ручного резервирования очередей под критичный трафик. Расширенный вариант, aRFS, работает автоматически: ядро само программирует аппаратный фильтр по месту нахождения приложения, и пакет приходит в очередь того ядра, где крутится процесс-получатель.
Список активных правил показывает ethtool -n eth0, удаление идёт по номеру из этого списка:
ethtool -n eth0
ethtool -N eth0 delete 3
Число слотов под правила ограничено адаптером, на практике это от десятков до сотен записей, и попытка залить лишнее вернёт ошибку ENOSPC. Правила хранятся только в железе, после перезагрузки их восстанавливают скрипты или сетевые конфигурационные файлы дистрибутива.
Отдельный слой разгрузки даёт XDP в аппаратном режиме. Программа загружается прямо в адаптер и фильтрует пакеты до того, как они попадут в память хоста:
ip link set dev eth0 xdpoffload obj drop_low_port.o sec xdp
Такой режим поддерживают не все драйверы, состояние видно в выводе ip link, а несоответствие возможностей карточки покажется ошибкой при загрузке программы. Для массового отброса мусорного трафика на 100G-линках аппаратный XDP экономит и память, и такты, потому что отфильтрованные пакеты даже не пересекают шину PCIe.
Типичные ошибки при включении и отключении оффлоадов
Практика раскладывает промахи по повторяющимся полочкам:
- выключение TSO и GSO на гигабитном и более быстром интерфейсе, количество пакетов на байт растёт в десятки раз, процессор упирается в потолок раньше адаптера;
- включение LRO на шлюзе, укрупнённые буферы без честных заголовков ломают пересылку и NAT, для маршрутизаторов годится только GRO;
- доверие дефолтам виртуальных машин, пара virtio и vhost не всегда согласует TSO между гостем и хостом, и тормоза в госте списывают на сеть целиком;
- смена оффлоадов на живом интерфейсе без окна обслуживания, часть драйверов пересоздаёт очереди, и связи на долю секунды рвутся;
- смешивание ручных ntuple-правил с aRFS, программа фильтров перетирает друг друга, и часть потоков уезжает в случайные очереди.
Диагностика занимает минуту. До и после изменения запускается один и тот же тест трафика, а разница снимается в трёх местах: ethtool -S eth0 по счётчикам с именами tso, gro, lro, ip -s link по объёму и дропам, и загрузка ядер через mpstat -P ALL 1. Рост объёма на байт обработчика и падение счётчика прерываний говорят, что разгрузка действительно легла на адаптер.
Отдельная осторожность нужна на виртуализации и на bond-интерфейсах. В bond оффлоады наследуются от активного подчинённого устройства, и рассинхрон между членами даёт мигающие под нагрузкой проблемы. В virtio список флагов зависит от версии хостового qemu, и возможности, появившиеся в новых выпусках, просто отсутствуют в выводе ethtool -k гостя, поэтому сравнивать хост и гостя по флагам бессмысленно.
Чтобы настройка пережила перезагрузку, её вписывают в конфигурацию сети. В Debian и Ubuntu привычное место это строки post-up ethtool -K eth0 tso on в /etc/network/interfaces, в systemd-networkd те же флаги задаются в юнит-файле .link параметрами TCPSegmentationOffload=, GenericSegmentationOffload=, GenericReceiveOffload=, LargeReceiveOffload=, которые применяются при появлении интерфейса. NetworkManager подхватывает ethtool-команды через dispatcher-скрипты, и в любом варианте значения применяются раньше, чем интерфейс начнёт принимать трафик.
Оффлоады не требуют пересборки ядра и настраиваются на живой системе, но требуют понимания роли каждого флага. Один раз расписанная раскладка под конкретную нагрузку обычно живёт годами, а в спорных случаях всё возвращается одной командой ethtool -K в исходное состояние.