awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

Accept и парсинг: от очереди ядра до типизированного запроса

Суть До запуска вашего кода ядро завершает рукопожатие в ограниченную очередь accept, а сервер превращает сырые байты в строку запроса, заголовки и тело — у каждого шага есть жёсткий лимит, который можно переполнить.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Сервис проходит все нагрузочные тесты, а потом в проде при всплеске трафика отклоняет соединения — не с 500, а TCP-сбросом ещё до запуска вашего кода. Логи пусты, потому что запрос не дошёл до приложения. Узким местом была очередь ядра с глубиной по умолчанию 128, заданной в 1999 году и ни разу не изменённой.

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

У сокета с listen() есть две очереди со стороны ядра, и их путаница порождает большинство загадок «соединения отклоняются под нагрузкой»:

  • SYN-очередь (неполные) — соединения в середине рукопожатия: сервер получил SYN, отправил SYN-ACK и ждёт финальный ACK. Размер задаётся net.ipv4.tcp_max_syn_backlog (обычно ~1024–2048).
  • Очередь accept (полные) — полностью установленные соединения, ждущие вызова accept() приложением. Размер — min(listen(backlog), net.core.somaxconn).

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

Ловушка 128

net.core.somaxconn два десятилетия по умолчанию был 128 в Linux (поднят до 4096 в ядре 5.4). Даже сегодня многие базовые образы контейнеров и управляемые платформы всё ещё ставят 128. Фреймворк, вызывающий listen(511) (бэклог по умолчанию в Node), молча обрезается до 128 через somaxconn. При всплеске 129 одновременно ожидающих соединений уже достаточно, чтобы начать терять.

НастройкаТипичный дефолтЧто ограничиваетСимптом переполнения
tcp_max_syn_backlog~1024–2048Полуоткрытые рукопожатияSYN flood / потерянные SYN
somaxconn128 (старое), 4096 (5.4+)Установленные, ждущие accept()Соединение отклонено / сброс, без лога
listen(backlog)511 (Node), 511 (nginx)Запрошенная глубина acceptОбрезается до somaxconn

Парсинг: байты в запрос

Как только accept() вернул сокет, сервер читает байты и запускает HTTP-парсер — конечный автомат, который распознаёт строку запроса (GET /path HTTP/1.1), затем строки заголовков, затем тело. Парсинг не свободен от лимитов:

  • Лимит размера заголовков. Node.js ограничивает суммарные заголовки 16 КБ (--max-http-header-size, поднято с 8 КБ в Node 12). nginx использует large_client_header_buffers (по умолчанию 4 8k). Превышение даёт 431 Request Header Fields Too Large или 400 — чаще всего из-за раздутого Cookie или большого JWT.
  • Длина строки запроса. URL длиннее буфера (по умолчанию 8k в nginx) отклоняется ещё до маршрутизации.

Откуда берётся тело

Парсер узнаёт длину тела одним из двух способов, и они падают по-разному:

  • Content-Length: N — прочитать ровно N байт. Ложная или обрезанная длина даёт зависание или ошибку парсинга.
  • Transfer-Encoding: chunked — читать чанки с указанием размера до чанка нулевой длины. Удобно для стриминга, но это классический вектор request smuggling, когда прокси и сервер расходятся в том, какой заголовок главнее.

Безопасный парсер отклоняет запрос, несущий одновременно Content-Length и Transfer-Encoding (RFC 9112 §6.1) — расхождение здесь и есть способ спрятать второй запрос внутри первого.

Викторина

При всплеске трафика клиенты получают таймауты соединения, но логи приложения полностью пусты. Наиболее вероятная причина?

Викторина

Запрос падает с 431 Request Header Fields Too Large только у залогиненных пользователей. Вероятная причина?

Викторина

Почему HTTP-парсер должен отклонять запрос, содержащий ОДНОВРЕМЕННО Content-Length и Transfer-Encoding: chunked?

Расставь шаги по порядку

Расставьте шаги от прихода пакета до разобранного запроса:

  1. 1 Приходит SYN клиента; соединение попадает в SYN-очередь (полуоткрыто)
  2. 2 Рукопожатие завершается; соединение переходит в очередь accept
  3. 3 Приложение вызывает accept(); ядро передаёт установленный сокет
  4. 4 Сервер читает байты и парсит строку запроса (метод, путь, версия)
  5. 5 Парсер читает строки заголовков до пустой строки, соблюдая лимит размера
  6. 6 Тело читается по Content-Length или каркасу chunked
Вспомните перед уходом
  1. 01
    Какие две очереди ядра стоят за сокетом listen() и что ограничивает каждую?
  2. 02
    Почему somaxconn = 128 — продакшн-ловушка и как он взаимодействует с бэклогом фреймворка?
  3. 03
    Как парсер определяет длину тела и почему наличие сразу Content-Length и Transfer-Encoding опасно?
Итог

До запуска кода приложения ядро управляет двумя очередями на listen-сокет: SYN-очередью для полуоткрытых рукопожатий (tcp_max_syn_backlog) и очередью accept для установленных соединений, ждущих accept() (минимум из бэклога фреймворка и somaxconn). Очередь accept — тихий убийца: дефолт 128 легко переполнить при всплеске, и переполнение отбрасывает соединения без лога приложения. Как только accept() вернул сокет, HTTP-парсер превращает байты в строку запроса, заголовки (лимит 16 КБ в Node, настраивается в nginx через large_client_header_buffers) и тело, определяемое по Content-Length или chunked. Слишком большие заголовки дают 431/400; оба каркаса сразу — вектор request smuggling, запрещённый RFC 9112. С разобранным запросом следующая остановка — выбор кода: маршрутизация и цепочка middleware.

Связанные уроки
встречается в185
Продолжить восхождение ↑Маршрутизация и middleware: что выполняется и в каком порядке
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.