MAATRIX / Блог / Что такое backlog соединений и почему клиент получает отказ на живом сервере

Что такое backlog соединений и почему клиент получает отказ на живом сервере

MAATRIX

Сервер отвечает на ping, systemctl status показывает «active (running)», порт слушается — а часть клиентов всё равно получает connection refused или обрыв на этапе установки соединения. Это не миф и не глюк сети: у каждого слушающего сокета есть предел на количество подключений, которые он готов держать в очереди, пока приложение до них не добралось. Разберёмся, что это за очередь, из чего она состоит и почему её переполнение выглядит как «сервер жив, но не пускает».

Что вообще такое backlog

Когда процесс открывает слушающий сокет (в терминах Berkeley sockets — вызывает listen()), он передаёт ядру число — тот самый backlog. Это не просто «максимум одновременных подключений» в бытовом смысле, а размер очереди соединений, которые уже дошли до стадии сетевого рукопожатия, но ещё не забраны приложением через accept().

Ключевая мысль, которую стоит держать в голове: backlog — это буфер между сетевым стеком ядра и кодом приложения. Ядро само по себе прекрасно принимает TCP-рукопожатия на любой скорости, которую позволяет канал и процессор. Проблема начинается там, где приложение — веб-сервер, воркер, демон на Node.js, Java-сервис — не успевает забирать готовые соединения из этой очереди так же быстро, как они появляются.

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

Две разные очереди, а не одна

Здесь часто путаются, потому что слово «backlog» в документации и в коде используют для обеих стадий сразу, хотя по сути это два независимых буфера с разным поведением при переполнении.

Очередь незавершённых соединений (SYN queue). Когда приходит первый пакет TCP-рукопожатия (SYN), ядро создаёт для него запись здесь и отправляет ответ (SYN-ACK). Соединение остаётся в этой очереди, пока не придёт финальный ACK от клиента, завершающий трёхстороннее рукопожатие. Размер этой очереди на Linux регулируется параметром net.ipv4.tcp_max_syn_backlog.

Очередь установленных, но не принятых соединений (accept queue). Как только рукопожатие завершено, соединение с точки зрения TCP полностью готово к передаче данных — оно переходит в состояние ESTABLISHED и перемещается в эту вторую очередь. Здесь оно ждёт, пока приложение вызовет accept() и заберёт его себе. Именно размер этой очереди задаётся тем самым числом, которое передаётся в listen(fd, backlog), и на Linux дополнительно ограничивается сверху параметром net.core.somaxconn.

Разница принципиальная: переполнение SYN-очереди — это, как правило, следствие проблем на сетевом уровне (флуд полуоткрытыми соединениями, атаки типа SYN flood, экзотические сетевые условия). Переполнение accept-очереди — почти всегда следствие того, что само приложение не успевает обрабатывать входящий поток соединений. Это разные диагнозы с разным лечением, и валить их в одну кучу — то, из-за чего диагностика затягивается.

Если вас интересует, что именно происходит на этапе SYN/SYN-ACK/ACK и почему рукопожатие иногда «подвисает», это подробно разобрано в статье про TCP-рукопожатие — здесь мы сознательно не повторяем этот материал, а идём дальше, к тому, что происходит уже после успешного рукопожатия.

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

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

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

Что видит клиент, когда очередь переполнена

Поведение при переполнении accept-очереди зависит от настройки. Есть два основных сценария.

Отказ (RST). Если параметр ядра net.ipv4.tcp_abort_on_overflow включён (значение 1), при переполнении очереди ядро сразу отправляет клиенту пакет RST — соединение разрывается явно. На стороне клиента это чаще всего выглядит как ECONNRESET или сразу connection refused, в зависимости от того, на каком этапе прилетел сброс. Это самый честный вариант: клиент сразу понимает, что его отвергли, и может быстро отреагировать (ретрай, отказоустойчивый клиент, переключение на другой узел).

Молчаливое отбрасывание. В значении по умолчанию (0) Linux ведёт себя мягче: SYN, пришедший при переполненной очереди, просто отбрасывается без ответа. Клиент не получает ни SYN-ACK, ни RST — с его стороны это выглядит как обычный таймаут соединения, только растянутый по времени ретрансмиссий TCP. Логика в том, что переполнение может быть временным (доля секунды под всплеском), и клиентский TCP-стек, скорее всего, повторит SYN сам — тогда как явный RST заставляет некоторые клиентские библиотеки сразу сдаваться без ретрая.

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

Почему сервер «жив», а соединения не принимаются

Это тот самый парадокс, из-за которого backlog часто остаётся непонятым: обычные проверки живости не видят проблему.

  • ICMP ping проверяет доступность сетевого стека на уровне ядра — он вообще не касается ни одного открытого TCP-порта и ничего не знает про accept-очередь.
  • TCP-check самого порта (например, nc -zv host port или простой health-check без реального запроса) в лучшем случае сам встаёт в ту же очередь и либо проходит, если в ней есть место, либо получает тот же отказ/таймаут, что и «боевой» клиент — то есть либо ложно зелёный, либо, если очередь регулярно переполнена, тоже начинает мигать красным, но без объяснения причины.
  • Процесс-менеджер (systemd, supervisor) видит, что процесс приложения жив и не упал — а переполненная accept-очередь ни с точки зрения ядра, ни с точки зрения PID не является сбоем процесса. Приложение просто медленно разгребает свою часть работы.

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

Здесь же стоит подчеркнуть отличие от смежной темы — пула соединений к базе данных: пул ограничивает, сколько соединений приложение держит открытыми к другому сервису (например, к СУБД), а backlog — это про то, сколько *входящих* соединений к самому приложению может накопиться на подходе, ещё до того, как приложение вообще начало с ними что-либо делать.

Почему приложение не успевает вызывать accept()

Сама по себе операция accept() дешёвая — это не обработка запроса, а просто извлечение готового соединения из очереди ядра в пространство процесса. Если backlog переполняется, значит где-то в цикле обработки есть узкое место, которое держит приложение занятым дольше, чем нужно для того, чтобы вернуться к следующему вызову accept(). Типичные причины:

  • Однопоточная модель обработки без асинхронности. Если сервер обрабатывает запросы строго последовательно (принял → обработал целиком, включая медленный I/O, — вернулся к accept), любая долгая операция (запрос к БД, внешний API, диск) блокирует не только текущий запрос, но и приём следующих соединений.
  • Исчерпан пул воркеров/потоков. У большинства продакшен-серверов (nginx, gunicorn, uwsgi, пул потоков в Java) есть ограниченное число обработчиков. Когда все заняты, новые соединения из accept-очереди просто не выгребаются, пока не освободится воркер.
  • Приложение упёрлось в CPU или в лимит файловых дескрипторов. Если процессу физически не хватает процессорного времени (см. load average и что оно на самом деле значит) или он упирается в ulimit -n, вызовы accept() реже добираются до исполнения — про сами лимиты открытых файлов подробно в статье про настройку ulimit.
  • Медленный или зависший даунстрим. Если приложение — это, например, обратный прокси или API-шлюз, который сам ждёт ответа от бэкенда, а бэкенд подвис, worker-процесс проксирующего сервера будет занят ожиданием — и не вернётся к приёму новых соединений вовремя.

Важно понимать: увеличение backlog само по себе не решает ни одну из этих причин. Оно только увеличивает буфер, в котором клиенты могут «подождать» подольше, прежде чем получат отказ — то есть отодвигает симптом, но не устраняет то, что приложение действительно не успевает за нагрузкой.

Как посмотреть на очередь и её переполнения в реальности

Прежде чем что-то крутить, полезно увидеть, действительно ли проблема в backlog, а не в чём-то ещё. Несколько практических точек входа.

Счётчик переполнений accept-очереди на Linux можно посмотреть через netstat -s или nstat, обращая внимание на строку вида overflowed в разделе TCP-статистики:

netstat -s | grep -i overflow
# или, в более новых системах
nstat -az | grep -i Listen

Ненулевое и растущее значение здесь — прямой признак того, что accept-очередь на каком-то из слушающих сокетов машины периодически переполняется.

Текущее состояние конкретного слушающего сокета — сколько соединений сейчас в очереди и каков лимит — можно увидеть через ss:

ss -ltn

Столбец Recv-Q для строки в состоянии LISTEN — это как раз текущая длина accept-очереди, а Send-Q в этом контексте показывает установленный backlog (максимум). Если Recv-Q регулярно близок к Send-Q — очередь работает на пределе, и переполнения — вопрос времени при следующем всплеске нагрузки.

Полезно также сверить настройки конкретного веб-сервера с системными лимитами — они должны быть согласованы, а не противоречить друг другу, о чём в следующем разделе.

Настройка backlog: где она задаётся и с чем должна быть согласована

Backlog настраивается на нескольких уровнях, и они должны работать вместе, а не по отдельности.

На уровне ядра — верхняя граница, выше которой прыгнуть нельзя, что бы ни просило приложение:

# /etc/sysctl.conf или /etc/sysctl.d/99-backlog.conf
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096

Значение somaxconn — это потолок для accept-очереди: даже если приложение при вызове listen() попросит больше, ядро молча урежет до этого значения (в старых версиях) или откажет (в некоторых конфигурациях systemd). Применить без перезагрузки:

sysctl -p /etc/sysctl.d/99-backlog.conf

На уровне веб-сервера — конкретное значение, которое приложение реально запрашивает при listen(). В nginx это параметр backlog в директиве listen:

server {
    listen 80 backlog=4096;
    listen 443 ssl backlog=4096;
}

Если значение здесь выше системного somaxconn, толку от такой настройки не будет — сработает более низкий из двух пределов. Держите оба параметра согласованными.

Таблица для ориентира — какое звено за что отвечает:

УровеньПараметрЗа что отвечает
Ядроnet.ipv4.tcp_max_syn_backlogРазмер очереди незавершённых рукопожатий (SYN)
Ядроnet.core.somaxconnВерхний предел accept-очереди для всех сокетов
Ядроnet.ipv4.tcp_abort_on_overflowЯвный RST (1) или тихий дроп (0) при переполнении
Приложение/веб-серверbacklog в listen()Запрошенный размер accept-очереди конкретного сокета

Увеличивать backlog имеет смысл для сглаживания коротких всплесков — например, если трафик приходит пачками (массовая рассылка пушей, резкий приток пользователей после публикации), а не равномерным потоком, и приложение в среднем справляется, просто не успевает за пиком в первые секунды.

Увеличивать backlog бессмысленно и даже вредно, если приложение стабильно не успевает обрабатывать средний уровень нагрузки — тогда очередь просто будет постоянно забита, а клиенты вместо быстрого отказа получат долгое ожидание в очереди и таймаут уже на своей стороне, что для пользователя обычно хуже, чем честный и быстрый connection refused. В этом случае реальное решение — горизонтальное или вертикальное масштабирование приложения (больше воркеров, больше ядер, балансировка нагрузки), а не игра с размером буфера.

Отдельно стоит отметить связь с частой темой 502/504 у reverse-proxy: если перед приложением стоит nginx как прокси, и backend не успевает принимать соединения, это может проявляться как всплеск 502 или 504 именно в моменты нагрузки — если сталкиваетесь с таким на практике, разбор конкретных причин 502 есть в статье nginx отдаёт 502 через раз.

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

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

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

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

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

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

Backlog — это то же самое, что максимальное число одновременных подключений к серверу?

Нет. Backlog ограничивает только очередь соединений, ожидающих, пока приложение их заберёт через accept(). После того как соединение принято, оно живёт по своим правилам (лимиты воркеров, файловых дескрипторов, памяти) и в backlog уже не учитывается.

Если увеличить backlog до очень большого значения, проблема с отказами исчезнет насовсем?

Нет, если причина в том, что приложение не успевает за средней нагрузкой — очередь просто будет забита постоянно, а не время от времени. Большой backlog помогает пережить кратковременные всплески, но не компенсирует хронически недостаточную пропускную способность приложения.

Как отличить переполнение SYN-очереди от переполнения accept-очереди на практике?

Смотрите netstat -s на предмет счётчиков SYNs to LISTEN sockets dropped (SYN-очередь) отдельно от times the listen queue of a socket overflowed (accept-очередь) — это разные строки статистики, и растут они по разным причинам: первая обычно из-за сетевых аномалий, вторая — из-за медленного приложения.

Нужно ли трогать backlog, если сервер обслуживает низкую и стабильную нагрузку?

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

Влияет ли backlog на TLS-хендшейк отдельно от TCP?

Нет напрямую — TLS-рукопожатие начинается уже после того, как TCP-соединение принято приложением (или терминирующим TLS прокси) через accept(). Backlog касается только сетевого уровня TCP; задержки на самом TLS-хендшейке — отдельная тема, не связанная с этой очередью.

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

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

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