Предел очереди на прослушивание: backlog, SYN и тихо потерянные подключения
На пиковой нагрузке часть клиентов вдруг начинает получать обрывы соединения или зависшие запросы — при этом CPU не упирается в потолок, память в порядке, в логе приложения ничего похожего на ошибку нет. Причина часто лежит не в приложении, а на уровне ядра, в очереди, о существовании которой вспоминают только в такой момент: у любого слушающего сокета есть ограниченный backlog, и когда он переполняется, ядро либо тихо роняет пакет, либо рвёт соединение сбросом — а приложение об этом даже не узнаёт.
Содержание
Две очереди одного сокета: SYN-queue и accept-queue
Когда сервер вызывает listen() на сокете, ядро заводит под него не одну очередь, а две, и путаница между ними — источник большинства неверных настроек.
SYN-очередь (SYN backlog, она же request queue) хранит подключения, которые ещё не завершили трёхстороннее рукопожатие: клиент прислал SYN, сервер ответил SYN-ACK и ждёт финальный ACK. Пока ACK не пришёл, запись сидит именно здесь. Размер очереди регулируется net.ipv4.tcp_max_syn_backlog.
Accept-очередь (accept queue, она же completed connection queue) хранит уже полностью установленные соединения (рукопожатие завершено, статус ESTABLISHED), которые ждут, пока приложение вызовет accept() и заберёт их себе. Размер этой очереди задаётся минимумом из двух чисел: системного net.core.somaxconn и параметра backlog, который приложение передало в вызов listen(fd, backlog).
Ключевой момент: очередей две, они независимы, и переполниться может любая из них по своей причине. SYN-очередь растёт, когда много клиентов одновременно открывают соединения (всплеск нагрузки или SYN-флуд). Accept-очередь растёт, когда клиенты уже достучались, но приложение не успевает забирать готовые соединения — event loop занят, воркер завис в блокирующей операции или процесс придавлен GC-паузой.
Клиент Ядро сервера Приложение
| SYN -> [SYN-очередь] |
| <- SYN-ACK tcp_max_syn_backlog |
| ACK -> [перенос в accept-очередь] |
| somaxconn / backlog в listen() |
| |------------ accept() -------->|
net.ipv4.tcp_max_syn_backlog и SYN cookies
tcp_max_syn_backlog ограничивает число полуоткрытых соединений (SYN_RECV), которые сервер готов держать одновременно, пока не получит подтверждающий ACK. Посмотреть текущее значение и поднять его:
sysctl net.ipv4.tcp_max_syn_backlog
# net.ipv4.tcp_max_syn_backlog = 1024
sysctl -w net.ipv4.tcp_max_syn_backlog=4096
На практике SYN-очередь редко переполняется от честного трафика — на это способен разве что резкий одновременный всплеск (тысячи клиентов реконнектятся после сбоя балансировщика) или направленный SYN-флуд. От переполнения спасают SYN cookies: если net.ipv4.tcp_syncookies=1 (в большинстве дистрибутивов — значение по умолчанию), ядро при заполненной очереди перестаёт хранить состояние подключения и кодирует нужную информацию прямо в номер последовательности SYN-ACK. Когда приходит финальный ACK, сервер восстанавливает состояние из самого пакета, а не из таблицы в памяти.
Цена cookies — часть TCP-опций (в первую очередь некоторые расширенные опции масштабирования окна) теряется, потому что закодировать всё в 32-битном поле невозможно. Это осознанный компромисс: принять соединение с чуть худшими характеристиками лучше, чем отбросить его совсем. Отключать tcp_syncookies ради «чистоты» смысла нет — включённые cookies не мешают нормальной работе и включаются в дело только при реальном переполнении.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверnet.core.somaxconn и backlog в коде приложения
Это вторая очередь и вторая по частоте причина «внезапных» обрывов под нагрузкой — и именно здесь чаще всего рассинхронизируются системная настройка и настройка самого приложения.
net.core.somaxconn — системный потолок для accept-очереди, общий для всех слушающих сокетов на машине:
sysctl net.core.somaxconn
sysctl -w net.core.somaxconn=4096
На многих современных дистрибутивах дефолт уже поднят с исторических 128 до нескольких тысяч, но полагаться на это не стоит — образы систем и контейнерные слои нередко тянут за собой старое значение. Проверяйте фактическое число на конкретном сервере, а не то, что «должно быть по идее».
Важный факт, который решает половину проблем с настройкой backlog: значение, которое приложение передаёт в listen(fd, backlog), — это лишь пожелание. Ядро Linux, согласно man 2 listen, молча урезает его до значения somaxconn, если запрошенный backlog больше:
> If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value.
Отсюда практическое следствие: поднять backlog в конфиге приложения до 8192, оставив somaxconn на 128, — бессмысленно, реальный размер очереди всё равно останется 128. И наоборот: поднять somaxconn до 8192, но оставить в приложении listen(fd, 128) — тоже бессмысленно, только уже с другой стороны. Оба значения нужно поднимать вместе и согласованно, причём системное — не ниже, чем то, что просит приложение.
Как это выглядит в конкретных стеках:
# nginx: backlog указывается прямо в listen
server {
listen 443 ssl backlog=4096;
}
// Node.js: второй аргумент listen() — это backlog
server.listen({ port: 8080, host: '0.0.0.0', backlog: 4096 });
# «сырой» socket в Python
sock.listen(4096)
# systemd socket unit
[Socket]
ListenStream=8080
Backlog=4096
Для процессов, живущих под systemd с socket activation, есть отдельная тонкость: если сервис поднят через .socket-юнит, backlog настраивается директивой Backlog= в юните, а не в конфиге самого приложения — приложение в этом случае просто наследует уже созданный сокет.
Что видит клиент: тихий drop против RST
Поведение при переполнении accept-очереди зависит от одного параметра — net.ipv4.tcp_abort_on_overflow:
tcp_abort_on_overflow = 0(по умолчанию). Когда финальный ACK рукопожатия приходит, а места в accept-очереди нет, ядро просто отбрасывает пакет — ответа клиенту не идёт. Клиент SYN-ACK уже получил и формально считает соединение установленным, может успеть отправить данные, которые останутся без ответа. Дальше вступают таймауты клиентской стороны — подключение не отклоняется явно, а «подвисает» на несколько секунд и лишь потом обрывается. Это и выглядит как случайные необъяснимые зависания: ошибки нет, есть только тишина.tcp_abort_on_overflow = 1. Сервер вместо молчания сразу шлёт RST. Клиент получает явный отказ (Connection reset by peer) — некрасиво, зато быстро и однозначно.
Держать tcp_abort_on_overflow=1 постоянно — спорно: это лечит симптом (зависание вместо быстрого отказа), но не причину. Разумнее включать его временно — на период нагрузочного теста или инцидента, чтобы быстрее увидеть проблему в логах, а после найти и устранить саму причину переполнения: тесный backlog, недостаточно воркеров или медленный accept().
Похожая логика возникает и при обрыве уже установленных соединений из-за таймаутов — если вам близка тема того, как приложение теряет клиентов по времени, а не по очереди, у нас есть отдельный разбор о том, как короткий keepalive рвёт соединения каждые несколько минут.
Диагностика переполнения: netstat -s, ss и счётчики ядра
Прежде чем менять параметры вслепую, стоит подтвердить, что проблема действительно в backlog, а не, например, в сетевых потерях или в логике самого приложения.
Общая статистика переполнений — netstat -s (пакет net-tools) выводит агрегированные счётчики TCP-подсистемы:
netstat -s | grep -iE 'overflow|SYN|listen'
Ищите строки вида:
1023 times the listen queue of a socket overflowed
1023 SYNs to LISTEN sockets dropped
Первая строка — переполнение accept-очереди (ListenOverflows), вторая — отбрасывание на этапе SYN (TcpExtListenDrops). Оба счётчика накопительные с момента загрузки ядра, поэтому важна не абсолютная цифра, а её рост во времени.
Более прицельно, через nstat (снимает и умеет обнулять счётчики между замерами):
nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtTCPSynBacklogDrops
Запустите два раза подряд с интервалом в минуту нагрузки — разница покажет, растут ли счётчики прямо сейчас, а не когда-то в прошлом.
Прямой доступ к тем же цифрам — файл /proc/net/netstat, вторая строка блока TcpExt:
cat /proc/net/netstat | grep TcpExt
Текущее состояние очереди конкретного сокета — ss -ltn показывает колонки Recv-Q и Send-Q для слушающих сокетов, и для LISTEN-сокета их смысл особый: Recv-Q — это текущее число соединений в accept-очереди, а Send-Q — тот самый настроенный backlog (эффективный, то есть уже урезанный до somaxconn, если урезание было):
ss -ltn
State Recv-Q Send-Q Local Address:Port
LISTEN 0 511 0.0.0.0:443
LISTEN 487 511 0.0.0.0:8080
Во второй строке видно: приложение слушает с эффективным backlog 511, а в очереди уже 487 соединений — почти под завязку, любой всплеск трафика её переполнит. Это самый быстрый способ поймать проблему до того, как она проявится в счётчиках отбрасываний: Recv-Q, устойчиво близкий к Send-Q, — сигнал, что приложение не успевает вызывать accept(), а не что мал размер очереди. Снимайте оба показателя мониторингом с интервалом в несколько секунд во время нагрузочного теста — разовый снимок покажет один кадр, а нужна динамика накопления.
Как настроить backlog в связке с приложением
Настройка backlog — это не один параметр, а согласованная цепочка из трёх звеньев, и слабое звено определяет реальный предел независимо от того, что выставлено в остальных двух:
| Звено | Параметр | За что отвечает |
|---|---|---|
| SYN-очередь | net.ipv4.tcp_max_syn_backlog | сколько незавершённых рукопожатий держит ядро |
| Accept-очередь (система) | net.core.somaxconn | системный потолок для готовых соединений |
| Accept-очередь (приложение) | backlog в listen() / конфиге сервера | пожелание приложения, урезается до somaxconn |
Практический порядок действий:
- Определите ожидаемый пик одновременных новых подключений, а не средний RPS. Backlog переполняется всплеском, а не средней нагрузкой: тысяча клиентов, реконнектящихся разом после рестарта апстрима, создают всплеск куда острее, чем тот же трафик, растянутый на минуту.
- Поднимите
somaxconnиtcp_max_syn_backlogдо сопоставимых значений, с запасом на всплеск:
# /etc/sysctl.d/99-backlog.conf
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.tcp_syncookies = 1
# применить без перезагрузки
sysctl --system
- Явно пропишите
backlogв самом приложении — не полагайтесь на дефолт (у многих серверов по умолчанию это скромное значение в несколько сотен, а не то, что вы выставили в sysctl). Значение не должно превышатьsomaxconn— превышение просто обрежется, но лучше держать числа в осознанном согласии. - Убедитесь, что приложение действительно успевает вызывать
accept(). Большая очередь — буфер на случай всплеска, а не решение проблемы медленного потребления. Еслиaccept()вызывается в одном потоке с обработкой тяжёлых запросов, очередь копится независимо от размера — расширять её бессмысленно, нужно разносить приём соединений и обработку запросов по разным потокам или процессам. - Перепроверьте после изменений через
ss -ltn— значениеSend-Qдолжно совпадать с ожидаемым эффективным backlog, а не с тем числом, что вы написали в конфиге приложения, если оно было вышеsomaxconn.
Отдельно стоит увязать backlog с лимитом файловых дескрипторов: даже идеально настроенная очередь не спасёт, если у процесса не хватает ulimit -n на реальное число параллельных соединений — узкое место просто переместится на шаг дальше. Этому и разбору того, где реально проходит предел одного процесса, посвящены отдельные материалы — настройка лимитов открытых файлов через ulimit и предел одного nginx worker.
Backlog регулирует только очередь ожидания, а не итоговую пропускную способность сервера — сколько соединений он реально держит после accept(), зависит уже от других ресурсов. Смежный разбор — сколько соединений реально держит nginx до отказа.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему увеличение backlog в приложении не помогает, если проблема в accept-очереди?
Скорее всего, значение упирается в somaxconn — ядро молча урежет запрошенный backlog до системного лимита. Проверьте оба значения и ss -ltn, где колонка Send-Q покажет реальный эффективный backlog, а не то, что написано в конфиге.
Можно ли поставить net.core.somaxconn и tcp_max_syn_backlog в очень большое значение «про запас», чтобы больше не думать об этом?
Формально да, обе очереди занимают память под метаданные соединений, а не под полноценные буферы, так что цена большого запаса невелика. Но огромная очередь — это ещё и большая задержка перед тем, как переполнение вообще станет заметно: приложение может незаметно деградировать по латентности accept() задолго до фактического переполнения. Разумный запас лучше, чем бесконтрольный максимум без мониторинга.
Чем SYN-флуд отличается от легитимного всплеска подключений с точки зрения backlog?
С точки зрения самой очереди — почти ничем, переполняется она одинаково. Отличие в паттерне: SYN-флуд — это множество SYN без последующих ACK (полуоткрытые соединения от случайных или поддельных адресов), легитимный всплеск — это в основном полностью завершающиеся рукопожатия. tcp_syncookies защищает именно от первого сценария, не требуя различать источники вручную.
Нужно ли отдельно настраивать backlog для HTTP/2 или WebSocket-соединений?
Нет, backlog — это механизм уровня TCP-рукопожатия, он не знает о протоколе поверх соединения. Разница в том, что WebSocket и HTTP/2 держат соединения открытыми дольше, из-за чего accept-очередь может дольше не опустошаться при том же входящем потоке новых подключений — но настраивается она точно так же.
Как понять, что дело именно в backlog, а не в firewall или сетевых потерях пакетов?
Переполнение backlog оставляет характерный след в netstat -s / nstat (счётчики ListenOverflows, ListenDrops) на самом сервере. Потери в сети или блокировки firewall до сервера просто не доходят и в этих счётчиках не отражаются — там нужно смотреть tcpdump на сетевом интерфейсе или счётчики самого firewall (например, iptables -L -v с накоплением по правилу DROP).
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →