HTTP/3 на QUIC кардинально меняет профиль нагрузки сервера: вместо TCP — UDP с пользовательским контролем перегрузки, меньшей толерантностью к джиттеру и повышенной чувствительностью к дропам в очередях. Поэтому «классические» тюнинги TCP почти не работают, а несколько ключевых параметров Linux дают львиную долю результата: буферы UDP, SO_REUSEPORT, планирование отправки (pacing) через sch_fq и поддержка UDP GSO/GRO. В этой статье собираю проверенный профиль и объясняю, зачем и когда применять каждую настройку. Для стабильной работы на многоядерных VDS эти принципы особенно важны.
Что особенного в QUIC/UDP по сравнению с TCP
QUIC использует UDP как транспорт, но реализует контроль перегрузки, восстановление потерь и мультиплексирование потоков в юзерспейсе. Это несет последствия:
- Буферизация: переполнения приемных/передающих буферов UDP быстрее бьют по задержке и потерям, чем у TCP (нет ретрансмитов ядра).
- Pacing: аккуратная шлифовка трафика на выходе критична — bursts легко перегружают NIC/qdisc/границу провайдера, растет loss и падает скорость HTTP/3.
- Масштабирование: многопоточность через
SO_REUSEPORTпозволяет равномерно распределять потоки между воркерами/тредами и расшить CPU. - Offload: UDP GSO/GRO снижает накладные расходы на системные вызовы и обработку большого числа мелких дейтаграмм (актуально для initial и мелких stream‑фреймов).
Психология тюнинга меняется: вместо «разогреть TCP» мы предотвращаем локальные очереди и выравниваем отправку. Особенно чувствительны проложенные поверх туннелей и NAT‑траектории.
Минимальные требования к ядру и драйверам
Для комфортной работы HTTP/3 на проде рекомендую ядро 5.10+ (LTS): стабильная поддержка UDP GSO/GRO, зрелый sch_fq и драйверные багфиксы. На более старших ядрах часть возможностей будет недоступна или работать неполно.
- UDP GSO: ускоряет отправку пачек UDP‑сегментов единым вызовом; полезно, если стек/библиотека QUIC умеет
sendmmsg()сUDP_SEGMENT. - UDP GRO: агрегирует входящие дейтаграммы; экономит CPU, разгружает softirq.
- sch_fq: планировщик, понимающий pacing по
sk_pacing_rateи временным меткам; нужен для сглаживания bursts.
Быстрые проверки:
# Версия ядра
uname -r
# Возможности драйвера (признаки, зависят от NIC)
ethtool -k eth0 | egrep "gro|gso|udp|tx|rx"
# Активные qdisc
tc qdisc show dev eth0

Базовый профиль sysctl для QUIC/UDP
Цель — не «завинтить значения в потолок», а убрать локальные дропы и выровнять задержку. Стартовые значения ниже подходят для 1–10 Gbps интерфейсов и обычных VDS/VM. Настраивайте с учетом RAM и профиля нагрузки.
# /etc/sysctl.d/99-quic.conf
# Умеренные потолки сокетных буферов (байты)
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.core.rmem_default = 262144
net.core.wmem_default = 262144
# Нижние границы для UDP‑сокетов (байты)
net.ipv4.udp_rmem_min = 16384
net.ipv4.udp_wmem_min = 16384
# Очередь пакетов до обработки softirq (увеличить при всплесках PPS)
net.core.netdev_max_backlog = 16384
# Пейсинг по умолчанию через fq (важно для сглаживания bursts)
net.core.default_qdisc = fq
# Диапазон эфемерных портов (актуально для исходящих клиентских сокетов QUIC, например, при обратных прокси)
net.ipv4.ip_local_port_range = 10000 65535
# Опционально: более крупные опции сокетов
net.core.optmem_max = 262144
Пояснения:
rmem_max/wmem_max— потолок, не реальное потребление. Библиотеки QUIC (ngtcp2, quiche, lsquic и др.) могут масштабировать буферы черезsetsockopt()под нагрузкой.udp_memменять не советую: это тройка значений в страницах, легко «перекормить» систему. Если видите системные лимиты, сначала увеличьтеrmem_max/wmem_maxи пересмотрите количество активных потоков.netdev_max_backlogуменьшит дропы при всплесках PPS, особенно на виртуальных интерфейсах; не путать с TCP backlog — у UDP нет очереди прослушивания.default_qdisc=fqвключает планировщик с поддержкой pacing; если по политике нужен AQMs типа fq_codel — оставьте его на WAN‑границе, а на сервере используйтеfqдля точного pacing.
Применяем без перезагрузки:
sysctl --system
SO_REUSEPORT: масштабирование приемника без боли
SO_REUSEPORT разрешает нескольким сокетам слушать один и тот же UDP‑порт. Ядро распределяет входящие дейтаграммы по хэшу 4‑кортежа (src/dst addr, src/dst port), обеспечивая липкость потока к конкретному воркеру. Это критично для QUIC: все пакеты конкретного соединения должны попадать к одному воркеру.
Плюсы:
- Горизонтальное масштабирование: каждый воркер получает свою долю трафика и собственные буферы ядра.
- Локальность кэша: меньше контеншена на локах и очередях.
- Простая реализация: директива в конфиге Nginx/HAProxy или флаг при биндинге сокета.
Подводные камни и нюансы:
- Балансировка завязана на 4‑кортеж; если вы балансируете на входе UDP‑потоки через внешний L4 (Anycast, UDP‑LB) с ребиндингом портов, убедитесь, что устройство сохраняет стабильность 4‑кортежа для каждого QUIC‑соединения.
- Продвинутый вариант — eBPF‑программа
SO_REUSEPORT, которая учитывает поля QUIC (например, DCID) для балансировки; это дает еще более равномерное распределение, но сложнее и требует аккуратного тестирования.
Pacing: сгладить всплески и снизить потери
Даже когда библиотека QUIC сама дозирует отправку, ядру полезно знать желаемый темп. sch_fq поддерживает pacing по sk_pacing_rate и временным меткам skb. Многие реализации QUIC выставляют SO_MAX_PACING_RATE и отправляют пачками через sendmmsg(). Результат — вместо резких bursts на выходе получаем ровный поток, у провайдера меньше микро‑дропов, выше реальная пропускная.
Минимум, что надо сделать на сервере:
- Включить
fqкак корневой qdisc. - Проверить, что приложение/библиотека умеет pacing (большинство умеют по умолчанию).
- Убедиться, что NIC не насаживает лишних offload‑бурстов, конфликтующих с pacing.
Установка fq на интерфейс до перезагрузки:
tc qdisc replace dev eth0 root fq
Проверка статистики очереди и дропов:
tc -s qdisc show dev eth0
Если видите рост дропов на qdisc при пиках — это сигнал, что либо темп завышен, либо не хватает CPU, либо входящую волну нужно перераспределять (RPS/XPS) и/или поднимать net.core.netdev_max_backlog.
UDP GSO/GRO: меньше системных вызовов — больше полезной работы
QUIC оперирует множеством коротких дейтаграмм. UDP GSO (segmentation offload) позволяет приложению отправить крупный буфер с указанием размера сегмента, а ядро/NIC разобьет его на мелкие UDP‑пакеты. UDP GRO (receive offload) агрегирует обратную сторону на входе.
Проверка поддержки драйвером:
ethtool -k eth0 | egrep "udp|gro|gso"
# Примеры интересующих флагов (не у всех NIC)
# tx-udp-segmentation: on
# rx-udp-gro-forwarding: on
# generic-segmentation-offload: on
# generic-receive-offload: on
Включать/выключать offload нужно осознанно. Если драйвер/виртуальный NIC глючит на UDP GSO — лучше отключить соответствующий флаг, чем получать редкие, но болезненные потери и ретраи на уровне QUIC.
Nginx: HTTP/3, SO_REUSEPORT и GSO
Современный Nginx с поддержкой QUIC умеет слушать 443/udp и использовать reuseport. Важно включить правильный qdisc в системе и, при наличии, GSO в самом Nginx. Для работы HTTP/3 потребуется корректный TLS 1.3 и действующие SSL-сертификаты.
# Фрагмент nginx.conf (escape HTML)
worker_processes auto;
http {
# Включаем GSO, если поддерживается ядром/драйвером
quic_gso on;
server {
listen 443 quic reuseport;
listen 443 ssl http2;
# ... SSL/TLS 1.3 настройки ...
# ... server_name, location и т.д. ...
}
}
# Убедитесь, что на интерфейсе активен fq
# tc qdisc replace dev eth0 root fq
Пара моментов:
listen 443 quic reuseport;создаст отдельные UDP‑сокеты для воркеров и распределит потоки ядром.quic_gso on;включает использованиеUDP_SEGMENT, если доступно. При проблемах с NIC/гипервизором временно отключайте.- Не забывайте про TLS 1.3 и корректные шифросuites; QUIC требует именно TLS 1.3.
HAProxy: QUIC‑листенеры и reuseport по тредам
Начиная с современных релизов HAProxy, QUIC доступен в продакшен‑качестве. Включайте многопоточность и reuseport на bind, чтобы ядро раздавало потоки по тредам равномерно.
global
nbthread 4
# ... прочие настройки ...
defaults
mode http
timeout client 30s
timeout server 30s
timeout connect 5s
frontend fe_https
bind :443 ssl crt /etc/pki/tls/cert.pem alpn h3,h2,http/1.1 quic reuseport
# ... ACL/маршруты ...
default_backend be_app
backend be_app
server s1 127.0.0.1:8080
Уточняйте синтаксис под вашу версию: в разных релизах опции quic/alpn/reuseport могли переезжать или меняться по умолчанию. Принцип неизменен: больше тредов + reuseport = лучше масштабирование на многоядерных VDS.
Наблюдаем: где теряются пакеты и где узкие места
UDP не прощает «тихие» переполнения. Несколько быстрых рецептов:

- Ошибки сокетных буферов:
nstat -az | egrep "Udp(InErrors|RcvbufErrors|SndbufErrors)"
- Очереди qdisc и дропы на выходе:
tc -s qdisc show dev eth0
- Состояние UDP‑сокетов приложения (размеры буферов, флаги):
ss -u -einp | egrep ":443|udp"
- Проверка фич NIC и счетчиков драйвера:
ethtool -k eth0 | egrep "gso|gro|udp"
ethtool -S eth0 | egrep -i "rx|tx|drop|fifo|udp"
- Захват трафика для верификации рукопожатия и potenial reordering:
tcpdump -i any udp port 443 -vv -n
Интерпретация:
- Растут
UdpRcvbufErrors— поднимайтеrmem_max/rmem_defaultи буферы приложения. - Растут дропы на
fq— перепрофилируйте pacing, следите за CPU, увеличьтеnetdev_max_backlog, оцените RPS/XPS. - Много дропов на драйвере — проверьте offload‑флаги, IRQ affinity, NUMA и пересмотрите лимиты очередей.
Пошаговый план внедрения на проде
- Обновите ядро до 5.10+ и убедитесь в стабильности драйвера NIC на вашей платформе.
- Включите
net.core.default_qdisc=fq, перезапустите сервисы сети или хост. - Проверьте и примените sysctl из базового профиля, начав с безопасных значений для
rmem_max/wmem_max. - В приложении/веб‑сервере включите
SO_REUSEPORTи несколько воркеров/тредов, привязывая их к CPU при необходимости. - Проверьте поддержку UDP GSO/GRO и включите в приложении/сервере (например,
quic_gso onв Nginx). При артефактах — откатите. - Нагрузочное тестирование: сравните метрики UdpRcvbufErrors, дропов на
fq, p95/p99 задержек и стабильность goodput на клиентах HTTP/3. - Наблюдаемость в проде: дежурные дашборды по nstat, qdisc, ethtool‑счетчикам и basic SLO.
Частые антипаттерны
- «Закрутить буферы до небес»: огромные
rmem_max/wmem_maxведут к непредсказуемому росту RSS и увеличению задержки GC в приложении; лучше умеренные значения и правильный pacing. - Ставить
fq_codelвместоfqна сервере для QUIC: AQM полезен на узком канале, но точный pacing даетfq. - Выключать GRO/GSO «на всякий случай»: если драйвер стабилен, эти фичи экономят CPU и дают ощутимый выигрыш.
- Игнорировать
SO_REUSEPORTпри высокой PPS: один сокет на процесс — частые дропы на RCV path и горячие локи. - Не контролировать MTU: помните, QUIC специфицирует минимальный размер initial >= 1200 байт. Если под капотом есть туннели/VPN, реальная MTU меньше 1500 — тестируйте пейсинг и фрагментацию.
Финальные штрихи и советы
Три кита практического HTTP/3 на Linux: правильные буферы, SO_REUSEPORT и fq-pacing. UDP GSO/GRO — мощный бонус, если стек и драйверы здоровы. В проде это дает меньше «зубцов пилы» на goodput, более ровный p99 и отсутствие загадочных дропов при всплесках. Начните с малого профиля, включите наблюдаемость и масштабируйте по спросу.
Если готовите миграцию фронтов, посмотрите также про тонкую настройку кэша и подсказок сервера: диапазонные запросы и кэш в Nginx/Apache и HTTP 103 Early Hints. И обязательно документируйте, чем вызвано каждое изменение sysctl и qdisc — через полгода это сэкономит часы на форензику.


