MAATRIX / Блог / Где сервер теряет пакеты, даже когда канал свободен: очереди сетевой карты

Где сервер теряет пакеты, даже когда канал свободен: очереди сетевой карты

MAATRIX

Загрузка сетевого интерфейса по графику — 10-15% от заявленной полосы, канал явно не узкое место, а клиенты всё равно жалуются на обрывы и подтормаживания под нагрузкой. Первая мысль — искать проблему в коде приложения или в маршруте до сервера. Но иногда пакеты теряются раньше, чем канал вообще успевает загрузиться — прямо на сетевой карте, до того как они попадают в стек ядра. Разберём механизм: что такое буфер приёма сетевой карты, почему у него есть предел, никак не связанный с мегабитами в секунду, и как увидеть эти потери, если знать, куда смотреть.

Пропускная способность и способность обработать поток — это два разных ресурса

Интуитивно кажется, что если канал не загружен, сеть «есть чем дышать» и потерь быть не должно. Это верно только для одного вида нагрузки — когда узкое место действительно в объёме данных (мегабитах в секунду). Но у сетевой карты и у CPU, который её обслуживает, есть второй, независимый от объёма ресурс — пропускная способность по числу пакетов в секунду (pps, packets per second).

Разница проявляется на пакетах разного размера. Одинаковое число пакетов по 1400 байт и по 64 байта дают совершенно разную нагрузку на канал (в первом случае — на порядок больше мегабит), но почти одинаковую нагрузку на CPU: карте и драйверу всё равно приходится обработать одинаковое количество отдельных событий — принять, разобрать заголовок, поставить в очередь дальнейшей обработки. Мелкие пакеты — DNS-запросы, ACK без данных, UDP-телеметрия, SYN-флуд, VoIP-трафик — создают именно такую нагрузку: канал загружен на единицы процентов, а число пакетов в секунду может быть огромным.

Поэтому «канал свободен» и «сервер справляется с потоком пакетов» — это два разных утверждения, и второе не следует из первого. Если хочется сравнить, насколько по-разному эти два ограничения проявляются на практике при переходе между полосами разной ширины, есть отдельный разбор — сетевой канал 1G против 10G: там же видно, почему более широкий канал сам по себе не спасает от упора в pps.

RX ring buffer: кольцевой буфер, в который карта складывает пакеты

Когда сетевая карта принимает пакет из провода, он не попадает в память процесса приложения мгновенно. Сначала карта записывает его через DMA (прямой доступ к памяти, без участия CPU на этом шаге) в область оперативной памяти, зарезервированную драйвером именно под приём — это и есть RX ring buffer, кольцевой буфер приёма. Формально это массив дескрипторов фиксированного размера: каждый дескриптор указывает на область памяти, куда карта может положить очередной пакет, а само кольцо закольцовано — после последнего слота карта возвращается к первому.

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

Вычитывание буфера ядром происходит не в hard IRQ handler напрямую, а через механизм NAPI (New API — историческое название, само API давно не новое): после первого прерывания драйвер переключается в режим опроса (polling) и вычитывает из кольца пакеты пачками, пока не исчерпает выделенный на один проход бюджет или пока кольцо не опустеет, и только потом снова разрешает прерывания. Здесь этот механизм важен ровно в одном аспекте: вычитывание буфера — это работа CPU, выполняемая на конкретном ядре, и она конкурирует за такты этого ядра со всем остальным, что на нём происходит. Подробно про то, как прерывание попадает на конкретное ядро и почему это часто оказывается одно и то же ядро для всей карты, разобрано в статье прерывания и почему сетевая карта отбирает у вас целое ядро — механику RSS и распределения по очередям здесь повторять не будем, дальше отталкиваемся от неё как от данности.

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

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

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

Что происходит, когда буфер переполняется

Если ядро (точнее, softirq-обработчик или ksoftirqd на назначенном ядре) не успевает вычитывать кольцо быстрее, чем карта в него пишет, слоты заканчиваются. Дальше у карты нет выбора: следующий входящий пакет она отбрасывает физически, на своём уровне, ещё до того, как он вообще коснулся стека ядра Linux. Не TCP, не IP, не conntrack, не файрвол — эти потери происходят на шаг раньше любого из них.

Это принципиально отличает такую потерю от потерь, которые часто ищут в первую очередь: переполнения буфера сокета приложения, срабатывания iptables/nftables, отбрасывания на уровне conntrack при исчерпании таблицы соединений. Все эти механизмы работают уже после того, как пакет попал в стек — у них есть свои счётчики, свои логи, свои способы диагностики, и они хорошо описаны в документации приложений. А отбрасывание на RX ring buffer происходит настолько рано, что типичные инструменты уровня приложения его вообще не видят: с точки зрения вашего сервиса пакета просто никогда не существовало.

Отсюда и парадокс из заголовка. Администратор смотрит на загрузку канала (единицы процентов от лимита), на nginx/haproxy access-логи (пакет туда не дошёл — его там и не будет), на ping (отдельный низкочастотный поток, который почти никогда не попадает в то же самое переполнение) — и везде «всё в порядке». А клиент тем временем видит потерянные TCP-сегменты, ретрансмиты и просадки скорости, потому что реальный источник потерь стоит на шаг раньше всех этих точек наблюдения. Похожая ловушка «инструмент диагностики физически не видит проблему» встречается и на уровне ICMP против TCP — она разобрана в статье потеря пакетов, которой не видно в ping, и это полезное дополнение к текущей теме, если после проверки счётчиков карты потери не находятся или находятся не полностью.

Как увидеть эти потери: счётчики на уровне интерфейса

Хорошая новость в том, что сама карта и драйвер честно считают такие отбросы — просто нужно знать, в каком именно счётчике искать, потому что общих «dropped» в выводах команд несколько и они означают разное.

Первое, что стоит посмотреть — статистику интерфейса целиком:

ip -s link show eth0

В выводе будет отдельная колонка dropped для RX и для TX. Рост RX dropped — сигнал, но не диагноз: он агрегирует несколько разных причин отбрасывания на приёме, и переполнение ring buffer — лишь одна из них (там же может отражаться нехватка памяти под sk_buff и другие внутренние причины).

Точнее и специфичнее для интересующей нас проблемы — расширенная статистика самого драйвера через ethtool:

ethtool -S eth0 | grep -iE 'drop|miss|fifo|no_buffer|discard'

Имена конкретных счётчиков зависят от драйвера и модели карты (у Intel, Mellanox/NVIDIA, virtio-net и разных облачных vNIC они называются по-разному), но в целом стоит искать что-то в духе rx_missed_errors, rx_fifo_errors, rx_no_buffer_count, rx_dropped — именно эти значения растут, когда карта не успела положить пакет в кольцо из-за отсутствия свободных дескрипторов. Растущий rx_missed_errors при этом почти нормальном rx_dropped на уровне ip -s link — характерный признак именно переполнения ring buffer, а не какой-то другой причины потерь дальше по стеку.

Ещё один источник тех же цифр — файлы /sys/class/net/eth0/statistics/, их удобно опрашивать в скриптах мониторинга без парсинга текстового вывода:

cat /sys/class/net/eth0/statistics/rx_dropped
cat /sys/class/net/eth0/statistics/rx_fifo_errors

Для наблюдения в динамике, а не разового снимка, подходит sar из пакета sysstat — он умеет писать историю по сетевым интерфейсам, и по ней потом видно, совпадает ли рост дропов с конкретными часами пиковой нагрузки:

sar -n DEV 1

Полезно смотреть счётчики не изолированно, а рядом с загрузкой ядра, которое обслуживает прерывания карты — если рост rx_missed_errors совпадает по времени со стопроцентной %si на одном ядре из mpstat -P ALL 1, картина складывается однозначно: CPU физически не успевал вычитывать буфер, карта заполнилась и начала ронять пакеты на входе.

Отдельно стоит проверить текущий размер самого кольца — сколько дескрипторов у него вообще есть и насколько это близко к максимуму, который поддерживает драйвер:

ethtool -g eth0

Вывод покажет две пары значений: Pre-set maximums (потолок, который умеет карта/драйвер) и Current hardware settings (что реально включено сейчас). Если текущее значение сильно меньше максимума, а счётчики переполнения растут — это первое, что стоит попробовать изменить.

Почему увеличение буфера — не universal fix, а первая линия защиты

Увеличить размер кольца можно так:

ethtool -G eth0 rx 4096

(конкретное число ограничено максимумом конкретной карты — узнаётся тем же ethtool -g; не все драйверы поддерживают изменение размера кольца на лету, иногда операция кратко разрывает соединение на интерфейсе, так что менять стоит не под пиковой нагрузкой).

Важно понимать, что именно это лечит, а что нет. Больший буфер даёт больше запаса на всплеск — короткий период, когда пакетов пришло резко больше обычного, а CPU через долю секунды снова догнал поток и разгрёб накопившееся. Для такого сценария увеличение кольца — рабочее и достаточное решение.

Но если проблема не во всплеске, а в постоянной нехватке производительности CPU относительно устойчивого потока пакетов — одно ядро физически не успевает обрабатывать столько pps даже без пиков, — больший буфер только отодвигает момент переполнения и добавляет побочный эффект: рост буферизации увеличивает задержку в худшем случае (в сетевой литературе это называют bufferbloat — большая очередь означает, что пакет в её хвосте ждёт дольше, прежде чем будет обработан). Отбросы при этом действительно станут реже, но задержки для части трафика — заметнее. Здесь нужен не более просторный буфер, а распределение нагрузки на несколько ядер: увеличение числа очередей приёма через RSS и корректная привязка прерываний, что подробно разобрано в статье про прерывания и сетевую карту, упомянутой выше, — увеличивать оба параметра (буфер и число очередей) обычно правильнее делать вместе, а не выбирать одно вместо другого.

Есть смежный, но другой параметр, который иногда путают с размером ring buffer, — netdev_max_backlog (sysctl net.core.netdev_max_backlog). Это не аппаратный буфер карты, а программная очередь Linux между NAPI-обработчиком и остальным сетевым стеком (актуальна прежде всего при включённом RPS). Переполнение этой очереди считается отдельным счётчиком в /proc/net/softnet_stat (колонка dropped) — увеличение netdev_max_backlog не увеличивает и не заменяет размер RX ring buffer, это два независимых буфера в разных точках пути пакета.

Если после увеличения буфера и настройки RSS/affinity счётчики отбросов всё равно растут под нагрузкой — это сигнал, что текущей вычислительной мощности, выделенной под сетевой стек, объективно не хватает на такой pps, и дальше решает либо резервирование отдельных ядер под сеть (см. настройку CPU pinning и affinity в статье про тонкую настройку CPU для виртуалки — принцип тот же, что и для закрепления ядер под приложение, только цель другая), либо сервер с бóльшим числом ядер и подтверждённой поддержкой multiqueue у сетевого адаптера.

Что дополнительно стоит проверить, прежде чем менять буферы

Прежде чем сразу увеличивать ethtool -G и настраивать RSS, стоит исключить пару более простых причин, которые внешне дают похожую картину.

Во-первых, коалесинг прерываний — настройка, которая объединяет несколько поступивших пакетов в одно прерывание вместо того, чтобы дёргать CPU на каждый пакет отдельно. Слишком агрессивная задержка коалесинга означает, что карта дольше держит пакеты в кольце перед обработкой, повышая риск упереться в размер буфера при резком всплеске. Текущие параметры смотрятся через ethtool -c eth0 и настраиваются через ethtool -C — это компромисс между задержкой и накладными расходами CPU, менять его стоит с измерениями до/после, а не по шаблону.

Во-вторых, убедитесь, что отбросы именно на приёме, а не на передаче — TX-счётчики (tx_dropped, tx_fifo_errors) означают другую проблему: сервер не успевает отправлять исходящий трафик, и лечится это в сторону настройки очередей планировщика пакетов (qdisc), а не RX ring buffer.

В-третьих, на виртуальных серверах стоит проверить, какой драйвер сетевого адаптера отдаёт гипервизор — virtio-net, SR-IOV или что-то ещё, — потому что число очередей, максимальный размер кольца и доступность RSS в облаке определяются не только вашей ОС, но и платформой виртуализации. Часть виртуальных адаптеров годами держит одну очередь по умолчанию вне зависимости от числа vCPU, и это стоит проверить сразу после разворачивания сервера командой ethtool -l eth0, а не постфактум, когда счётчики уже растут под нагрузкой.

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

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

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

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

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

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

Растущий rx_dropped в ip -s link — это всегда переполнение ring buffer?

Нет, это агрегированный счётчик, который может включать и другие причины потерь на приёме, не только нехватку слотов в кольце. Чтобы подтвердить именно эту причину, смотрите более специфичные счётчики драйвера через ethtool -S — рост rx_missed_errors или rx_fifo_errors конкретнее указывает на переполнение буфера карты.

Можно ли поставить размер RX ring buffer на максимум и забыть про проблему навсегда?

Нет. Больший буфер снижает потери на кратковременных всплесках, но увеличивает задержку в очереди и не решает ситуацию, когда CPU устойчиво не успевает обрабатывать поток пакетов — там нужно распределение нагрузки по ядрам (RSS/RPS), а не только больший буфер.

У меня канал загружен на 5%, а потери есть — это точно ring buffer, а не что-то ещё?

Низкая загрузка канала при высоких потерях — сильный намёк именно на ограничение по pps, а не по мегабитам, но однозначный ответ дают счётчики: проверьте ethtool -S на предмет rx_missed/rx_fifo и загрузку по ядрам через mpstat -P ALL 1 в момент проблемы, а не полагайтесь только на косвенные признаки.

Ring buffer у RX и TX — это один и тот же буфер?

Нет, это два отдельных кольца с независимыми счётчиками и независимыми настройками размера — ethtool -g показывает оба, а ethtool -G позволяет менять их по отдельности (rx и tx). Проблемы с приёмом и передачей диагностируются и решаются по-разному.

На виртуальном сервере с одной vCPU есть смысл увеличивать число очередей RSS?

Практически нет — RSS распределяет нагрузку между несколькими ядрами, и на единственном ядре распределять её попросту некуда. В этом случае единственный доступный рычаг — размер самого кольца и коалесинг прерываний; для действительно высокого pps на одном ядре имеет смысл рассматривать конфигурацию с бóльшим числом ядер.

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

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

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