Сети и протоколы
Опции TCP и типичные патологии
Вызов 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.
- Обязательный заголовок
- 20 байт
- Максимум опций
- 40 байт (всего не более 60 байт)
- Байт флагов
- URG ACK PSH RST SYN FIN CWR ECE
- Поле окна (сырое)
- 16 бит, максимум 65535 байт
- С масштабом окна
- до 1 ГиБ
- Область контрольной суммы
- IP-псевдозаголовок + TCP-сегмент
Алгоритм Нейгла и delayed ACK — ловушка на 200 мс
По умолчанию TCP-отправитель группирует маленькие записи с помощью алгоритма Нейгла: не отправлять неполный сегмент, если в полёте есть неподтверждённые данные. Независимо от этого получатель задерживает ACK до 40 мс (дефолт Linux), надеясь прицепить ACK к исходящим данным приложения.
В сочетании эти два механизма создают опасное взаимодействие на паттернах «маленький запрос — маленький ответ»:
- Клиент отправляет маленький запрос. Запрос меньше MSS.
- TCP-стек сервера держит ACK до 40 мс, надеясь прицепить его к исходящему ответу.
- Клиент не может отправить следующую маленькую запись, потому что Нейгл держит её (ждёт ACK первой записи).
- Приложение сервера в конце концов отвечает, и delayed ACK срабатывает вместе с ответом.
Результат: задержка 40–200 мс на каждом маленьком интерактивном обмене.
Исправление: TCP_NODELAY=1 на сокете отключает Нейгла. gRPC, клиенты Redis, реализации HTTP/2 и любой RPC-слой, чувствительный к задержкам, устанавливают его по умолчанию. TCP_QUICKACK=1 говорит Linux немедленно подтвердить следующий сегмент (автосбрасывается после одного пакета, поэтому вызывайте его после каждого read() в плотном цикле).
Проследите задержку 200 мс от Nagle + delayed-ACK у клиента Redis без TCP_NODELAY.
Флаг 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
$ 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: каждый сегмент несёт метку времени, и получатель отбрасывает сегменты с метками старше недавно виденного значения для этого соединения. Это делает атаки на переполнение и случайную коррупцию при оборачивании практически невозможными.
- 01Объясните зависание Nagle + delayed-ACK: какая сторона вызывает какую задержку и каково стандартное исправление?
- 02От чего защищает PAWS и какая опция TCP его включает?
- 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 или файрвола молча истечёт.