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

Сети и протоколы

Опции TCP и типичные патологии

Суть Анатомия заголовка TCP, зависание Nagle+delayed-ACK, ECN, keepalive и ловушка утечки сокетов CLOSE-WAIT, с которой рано или поздно сталкивается каждый бэкенд-разработчик.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 15 min

Вызов Redis занимает 200 мс — хотя Redis находится на том же хосте в датацентре. Нагрузки нет, таймаутов нет, ошибок нет — только 200 мс задержки на каждом маленьком запросе. Причина — взаимодействие двух дефолтных настроек TCP в худшем из возможных сочетаний, а исправление занимает одну строку.

Анатомия заголовка TCP

Каждый TCP-сегмент содержит обязательный заголовок в 20 байт плюс опциональные поля (ещё до 40 байт):

ПолеРазмерНазначение
Порт источника16 битЧасть четырёхкортежа, идентифицирующего соединение
Порт назначения16 битЧасть четырёхкортежа
Порядковый номер32 битаПозиция первого байта данных в потоке
Номер подтверждения32 битаСледующий ожидаемый байт от соседа
Смещение данных4 битаДлина заголовка в 32-битных словах (меняется с опциями)
Флаги8 битURG, ACK, PSH, RST, SYN, FIN, CWR, ECE
Окно16 битБайты, которые получатель готов принять
Контрольная сумма16 битПокрывает псевдозаголовок IP и TCP-сегмент
Указатель срочности16 битПочти никогда не используется; наследие старых терминальных протоколов

Поле опций (до 40 байт) содержит MSS, масштаб окна, SACK, метки времени и cookie TFO. Опции согласовываются только в SYN/SYN-ACK — после установки соединения набор опций фиксируется.

Поле опций TCP

Помимо MSS, масштаба окна и SACK:

Метки времени (RFC 7323): используются как для измерения RTT, так и для PAWS (защита от закольцованных порядковых номеров). PAWS предотвращает ситуацию, когда старые задержанные пакеты принимаются за новые данные при долго живущих высокоскоростных соединениях, где порядковые номера оборачиваются.

Cookie TFO также живёт в поле опций. Подробности — в уроке про SYN cookies и TFO.

Заголовок TCP кратко
Обязательный заголовок
20 байт
Максимум опций
40 байт (всего не более 60 байт)
Байт флагов
URG ACK PSH RST SYN FIN CWR ECE
Поле окна (сырое)
16 бит, максимум 65535 байт
С масштабом окна
до 1 ГиБ
Область контрольной суммы
IP-псевдозаголовок + TCP-сегмент

Алгоритм Нейгла и delayed ACK — ловушка на 200 мс

По умолчанию TCP-отправитель группирует маленькие записи с помощью алгоритма Нейгла: не отправлять неполный сегмент, если в полёте есть неподтверждённые данные. Независимо от этого получатель задерживает ACK до 40 мс (дефолт Linux), надеясь прицепить ACK к исходящим данным приложения.

В сочетании эти два механизма создают опасное взаимодействие на паттернах «маленький запрос — маленький ответ»:

  1. Клиент отправляет маленький запрос. Запрос меньше MSS.
  2. TCP-стек сервера держит ACK до 40 мс, надеясь прицепить его к исходящему ответу.
  3. Клиент не может отправить следующую маленькую запись, потому что Нейгл держит её (ждёт ACK первой записи).
  4. Приложение сервера в конце концов отвечает, и delayed ACK срабатывает вместе с ответом.

Результат: задержка 40–200 мс на каждом маленьком интерактивном обмене.

Исправление: TCP_NODELAY=1 на сокете отключает Нейгла. gRPC, клиенты Redis, реализации HTTP/2 и любой RPC-слой, чувствительный к задержкам, устанавливают его по умолчанию. TCP_QUICKACK=1 говорит Linux немедленно подтвердить следующий сегмент (автосбрасывается после одного пакета, поэтому вызывайте его после каждого read() в плотном цикле).

Проследи
1/5

Проследите задержку 200 мс от Nagle + delayed-ACK у клиента Redis без TCP_NODELAY.

1
Step 1 of 5
Клиент отправляет 10-байтовую команду PING в Redis. Что делает Нейгл?
2
Locked
Redis получает PING. Что ядро делает с ACK?
3
Locked
Redis обрабатывает команду за 0,1 мс и вызывает write('+PONG\r\n'). ACK уходит сейчас?
4
Locked
Теперь клиент отправляет PIPELINE из двух команд быстро подряд (write(CMD1), затем write(CMD2)). Что происходит с Нейглом?
5
Locked
Исправление: устанавливаем TCP_NODELAY=1. Что меняется?

Флаг PSH

Флаг PSH (push) говорит TCP-стеку получателя немедленно передать буферизованные данные приложению, не дожидаясь новых. Современные стеки передают данные приложению сразу по прибытии, поэтому PSH — это скорее подсказка, чем требование. Он устанавливается на последнем сегменте каждого write(), завершающего логическое сообщение, — полезно, когда TCP-стек ядра объединяет несколько записей приложения в один сегмент, гарантируя доставку записи получателю.

Явное уведомление о перегрузке (ECN)

Вместо сброса пакетов для сигнализации перегрузки ECN-совместимые маршрутизаторы помечают их двухбитовым кодом (CE = Congestion Experienced). В заголовке TCP зарезервированы флаги CWR (Congestion Window Reduced) и ECE (ECN-Echo). Согласование ECN происходит при обмене SYN/SYN-ACK: обе стороны объявляют поддержку через флаги ECE+CWR. При получении пакетов с меткой CE получатель устанавливает ECE для уведомления отправителя; отправитель уменьшает cwnd и устанавливает CWR в подтверждение.

ECN включён по умолчанию в Linux и macOS для соединений с надёжными адресатами. Некоторые старые промежуточные устройства сбрасывают пакеты с метками ECN — развёртывания используют обнаружение отказа и отключают ECN для таких адресатов.

Keepalive

По умолчанию TCP-соединение не отправляет пакетов в режиме простоя, поэтому соединение через NAT или файрвол может быть молча закрыто через 5–60 минут. SO_KEEPALIVE отправляет зондирующий пакет каждые tcp_keepalive_time секунд (дефолт Linux: 7200 с = 2 часа — слишком долго для использования в service mesh). Для долгоживущих RPC-соединений настройте:

  • tcp_keepalive_time: 60–120 с
  • tcp_keepalive_intvl: 10–30 с
  • tcp_keepalive_probes: 3–5

Это позволяет обнаруживать мёртвых соседей за минуту, а не за два часа.

Найди ошибку

Вывод ss — диагностика накопления CLOSE-WAIT

log
$ ss -tan state established | wc -l
12384
$ ss -tan state close-wait | wc -l
9821
$ ss -tan state time-wait | wc -l
1247
$ ss -s
Total: 12500
TCP:   23552 (estab 12384, closed 8920, orphaned 2, timewait 1247)
$ ps -p 1234 -o pid,stat,rss,vsz,cmd
PID STAT  RSS    VSZ CMD
1234 Ssl 8392000 12000000 /usr/bin/app-server

У процесса сервиса 12к ESTABLISHED + 9,8к CLOSE-WAIT сокетов, и RSS растёт. В чём баг и как его исправить?

Викторина

Почему сочетание алгоритма Нейгла и delayed ACK вызывает задержку ~200 мс на маленьком RPC-трафике?

Почему это работает

Зачем нужен PAWS. На очень высокоскоростном долгоживущем соединении 32-битное пространство порядковых номеров TCP (~4 ГиБ) может обернуться за несколько часов. Без PAWS задержанный повторно переданный сегмент из предыдущего прохода через пространство номеров мог бы прийти и выглядеть как новый сегмент. Метки времени RFC 7323 включают PAWS: каждый сегмент несёт метку времени, и получатель отбрасывает сегменты с метками старше недавно виденного значения для этого соединения. Это делает атаки на переполнение и случайную коррупцию при оборачивании практически невозможными.

Вспомните перед уходом
  1. 01
    Объясните зависание Nagle + delayed-ACK: какая сторона вызывает какую задержку и каково стандартное исправление?
  2. 02
    От чего защищает PAWS и какая опция TCP его включает?
  3. 03
    Что означает большое количество CLOSE-WAIT на сервере и как найти первопричину?
Итог

Заголовок TCP — это 20 обязательных байт плюс до 40 байт опций. Ключевые опции (MSS, масштаб окна, SACK, метки времени, cookie TFO) согласовываются в SYN/SYN-ACK и затем фиксируются. Алгоритм Нейгла и delayed ACK — оба дефолтных поведения, которые катастрофически взаимодействуют на трафике с маленькими записями: отправитель удерживает запись в ожидании ACK, который получатель задерживает на 40 мс. TCP_NODELAY отключает Нейгла и является стандартом для любого RPC-клиента. Накопление CLOSE-WAIT — каноничный симптом утечки сокетов: сосед отправил FIN, но приложение никогда не вызвало close(). ECN помечает перегруженные пакеты вместо их сброса; SO_KEEPALIVE зондирует простаивающие соединения для обнаружения мёртвых соседей до того, как состояние NAT или файрвола молча истечёт.

Связанные уроки
Продолжить восхождение ↑SYN cookie, TFO и TIME-WAIT при высокой нагрузке
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.