Этап 4 — TCP: соединение и поведение под нагрузкой

Знать что есть «SYN → SYN-ACK → ACK» — этого мало. Реальный TCP сложнее: размер окна, ICW, slow start, congestion control, Nagle, TFO, SYN cookies. Эти детали определяют, сколько твоё приложение выдержит и насколько быстро отдаст 1 МБ.

Рабочая ситуация

«Инцидент: файлы больше ~1 МБ через новый VPN не скачиваются — соединение устанавливается, мелкие страницы открываются, а крупная передача замирает намертво. Пинг идеальный». Прежде чем читать: почему «мелкое работает, крупное висит» — это почти диагноз? Какой невидимый механизм TCP мог сломаться от того, что кто-то на фаерволе «на всякий случай запретил весь ICMP»? Ответ соберётся в разделе про MSS и MTU.

Что узнаешь — после урока ты сможешь
Вспомни из прошлых уроков

В этапе 3 anycast привёл тебя на ближайший edge — теперь с ним надо установить соединение, и каждый RTT на счету. База: TCP/UDP и порты — «Сети с нуля», урок 4; MTU и кадры — «Инженер сетей», урок 2. Из этапа 1 помни строку «Initial connection» в DevTools → Timing — этот урок объясняет всё, что в ней происходит.

1. Что мы уже знаем — короткий recap

TCP — это «надёжная» доставка поверх IP. Главные гарантии: порядок, доставка, защита от перегрузки. Перед обменом данными — three-way handshake.

Этот этап — про что под капотом и почему оно работает именно так.

Каждый TCP-сегмент имеет заголовок ~20 байт. Знать его поля — обязательно для понимания.

TCP header — 20 байт минимум Source port (16 бит) Destination port (16 бит) Sequence number (32 бита) Acknowledgment number (32 бита) offset reserved флаги: SYN ACK FIN RST PSH URG ECE CWR Window size (16 бит) Checksum Urgent pointer Options (0-40 байт): MSS, Window Scale, SACK, Timestamps Data (payload) Каждый TCP-сегмент имеет такой заголовок. Знание полей = ключ к чтению tcpdump.
ПолеЧто в нём
Source / Dest portПорты сторон. С IP-парой образуют 4-tuple, уникально идентифицирующий соединение.
Sequence numberНомер первого байта данных в этом сегменте. Стартует с случайного при SYN — это Initial Sequence Number (ISN).
Ack number«Я получил всё до этого номера, жду следующий». Подтверждение.
Window size«Я готов принять ещё столько байт без подтверждения». См. ниже.
ФлагиSYN (синхронизация), ACK (подтверждение), FIN (закрыть), RST (срочный разрыв), PSH (доставь немедленно), URG (срочные данные).
OptionsРасширения: MSS, Window Scale, SACK, Timestamps. Согласуются при SYN.

3. Three-way handshake — байт за байтом

Откроем tcpdump и пройдём по реальному handshake. Браузер обращается к серверу на 443:

# Запускаем
sudo tcpdump -nn -i any -S 'host example.com and port 443'

# В другой вкладке
curl https://example.com

В выводе ты увидишь примерно такое:

10:00:00.001 IP 192.168.1.42.54321 > 93.184.216.34.443:
    Flags [S], seq 1000000000,
    win 65535, options [mss 1460,sackOK,TS val 1234 ecr 0,nop,wscale 7]

# Сервер отвечает SYN-ACK с СВОИМ начальным номером
10:00:00.030 IP 93.184.216.34.443 > 192.168.1.42.54321:
    Flags [S.], seq 5000000000, ack 1000000001,
    win 65535, options [mss 1380,sackOK,TS val 9999 ecr 1234,nop,wscale 9]

# Клиент подтверждает
10:00:00.031 IP 192.168.1.42.54321 > 93.184.216.34.443:
    Flags [.], ack 5000000001, win 64240

Что важно увидеть:

4. Initial Sequence Number — защита от подделок

Зачем ISN случайный? Если бы он был всегда 0 — злоумышленник мог бы «угадать» состояние твоего соединения и вставить туда свой пакет с правильным номером. Это называется TCP injection.

RFC 1948 (и современный RFC 6528) описывают, как генерировать ISN — на основе криптографической функции от 4-tuple соединения и секретного ключа. Сегодня атаковать ISN практически невозможно.

5. MSS, MTU и фрагментация

Каждая сеть имеет максимальный размер пакета — MTU (Maximum Transmission Unit). Для Ethernet это 1500 байт. В IP-заголовке 20 байт, в TCP-заголовке 20 байт — остаётся MSS = 1460 байт на полезные данные.

MSS согласуется в SYN-options. Каждая сторона говорит «я могу принять сегменты до X байт». Меньшая из двух используется в обе стороны.

Path MTU discovery (PMTUD)

На пути могут быть участки с меньшим MTU (например, VPN-туннели имеют MTU ~1400 из-за overhead). Если ты отправишь пакет 1500 байт, а на пути MTU 1400 — пакет либо фрагментируется (если разрешено), либо отбрасывается с ICMP «fragmentation needed».

В современном IP DF (don't fragment) обычно стоит. TCP узнаёт о проблеме через ICMP и уменьшает MSS. Если ICMP блокируется (привет, фаерволы) — соединение «зависает». Это известная проблема PMTUD black hole.

MSS clamping

Решение — на роутере искусственно «зажать» MSS:

iptables -t mangle -A POSTROUTING -o tun0 -p tcp --tcp-flags SYN,RST SYN \
   -j TCPMSS --clamp-mss-to-pmtu

Это значит «исправлять MSS в SYN, чтобы он соответствовал PMTU». Делается на VPN-шлюзах и роутерах с туннелями.

6. Receive Window и Window Scale

Поле Window size говорит — «я готов принять ещё столько байт до того, как тебе нужно ждать моего ACK». Это flow control — не путать с congestion control.

Зачем это нужно: приёмник может быть медленнее отправителя (медленный диск, занятый CPU). Если бы отправитель слал пакеты как только захочет — приёмник переполнился бы.

В TCP-заголовке окно — 16 бит, то есть максимум 65535. Это мало для современных сетей (на 100Mbps + 100ms RTT нужно 1.25 МБ окна). Поэтому RFC 1323 ввёл Window Scale — option, которая умножает окно на 2scale:

wscale 7 → окно умножается на 128
window 65535 + wscale 7 = эффективно 8 МБ

Согласуется только в SYN. Поэтому если firewall зачем-то режет wscale-option — у тебя серьёзно деградирует производительность.

7. Congestion control — главная магия TCP

Это самая важная часть TCP. Без congestion control интернет рухнул бы. Как работает?

У отправителя есть две переменные:

Отправитель шлёт min(cwnd, rwnd). cwnd регулируется отправителем сам, чтобы не «забить» канал.

Slow start — фаза разгона

В начале соединения отправитель не знает, какая пропускная способность канала. Начинает с Initial Congestion Window (ICW) — обычно 10 сегментов (~14 КБ). Если получил ACK — удваивает cwnd на каждый RTT.

Это не «медленный» старт, как кажется — это экспоненциальный рост:

RTT 0: cwnd = 10  (могу слать 14 КБ)
RTT 1: cwnd = 20  (28 КБ)
RTT 2: cwnd = 40  (56 КБ)
RTT 3: cwnd = 80  (112 КБ)
RTT 4: cwnd = 160 (224 КБ)
...

Это значит, что на старте маленькие файлы (типичный HTML, JS) могут не успеть воспользоваться полной пропускной способностью. Это одна из причин, почему TLS 1.3 / HTTP/2 / TCP Fast Open так важны — каждый сэкономленный RTT означает, что мы быстрее войдём в «крейсерский» режим.

Что прерывает slow start

cwnd растёт до тех пор, пока:

При потере пакета

Классический TCP Reno:

Современные алгоритмы (CUBIC, BBR) ведут себя сложнее.

8. CUBIC vs BBR — современные алгоритмы

Долгое время стандартом в Linux был CUBIC — улучшение Reno, которое лучше работает в широких каналах. Это loss-based: реагирует на потери пакетов.

В 2016 Google представил BBR (Bottleneck Bandwidth and Round-trip propagation time). BBR — model-based: он моделирует канал, измеряя пропускную способность и RTT. Не ждёт потерь.

CUBIC

  • Реагирует на потери
  • Постоянно «забивает» канал — заполняет буферы
  • Стандарт в Linux до недавнего времени
  • Плохо в каналах с buffer bloat

BBR

  • Активно измеряет канал
  • Старается не заполнять буферы (низкая latency)
  • Используется Google, YouTube — гигантский эффект
  • Иногда «агрессивен» к CUBIC-соседям

В Linux переключается одной строкой:

sysctl -w net.ipv4.tcp_congestion_control=bbr
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf

На веб-серверах, отдающих большие файлы (видео, образы, дампы), BBR обычно даёт ощутимое улучшение throughput.

9. SACK — Selective Acknowledgment

Базовый ACK говорит только «получил всё до номера X». Если есть «дыра» — отправитель должен переслать всё после потерянного пакета.

SACK позволяет приёмнику сказать: «получил всё до X, плюс кусок Y-Z и кусок A-B». Тогда отправитель пересылает только настоящие дыры.

Согласуется в SYN. Современные стеки используют по умолчанию. Без SACK производительность в плохих сетях падает в разы.

10. Алгоритм Nagle и delayed ACK

Историческая боль маленьких пакетов. Когда приложение шлёт по 1 байту (например, telnet), каждый байт — это пакет с 40 байтами заголовков. Эффективность 1/41 = 2.4%.

Nagle's algorithm: не отсылать сегмент, пока есть «зависшие» данные без ACK. Накопил больше — отослал. Это объединяет мелкие отправки.

Но! Приёмник тоже хитрит: delayed ACK — не подтверждать сразу, ждать ~200мс, может прилетят ещё данные. Тогда один ACK подтвердит сразу несколько.

В сочетании эти два алгоритма иногда дают задержку до 200мс на мелких сообщениях. Это называется Nagle + delayed ACK interaction.

Решение — на серверах, отдающих API, обычно отключают Nagle:

# В коде приложения
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val));

# В Nginx
tcp_nodelay on;

11. TCP Fast Open (TFO)

«А что, если можно отправить данные сразу с SYN, не дожидаясь handshake?»

TFO позволяет это. При первом подключении сервер даёт клиенту TFO cookie. В следующем — клиент шлёт SYN+cookie+данные одним пакетом. Сервер проверяет cookie и сразу обрабатывает.

Экономит 1 RTT. Поддерживается Linux ≥ 3.7, есть в Chrome для не-HTTPS. С HTTPS почти не используется, потому что TLS 1.3 даёт похожие 0-RTT-возможности.

12. SYN flood и SYN cookies

Классическая атака: злоумышленник шлёт миллионы SYN с подделанных IP. Сервер на каждый создаёт «полу-открытое» соединение в очереди (SYN_RECV) и шлёт SYN-ACK, которое уходит «в никуда». Очередь переполняется — легитимные пользователи не могут подключиться.

Защита — SYN cookies (Linux включён по умолчанию):

Это блокирует SYN flood без накладных расходов.

sysctl net.ipv4.tcp_syncookies
# 1 = включено (по умолчанию в Linux)

13. Состояния TCP — все 11 штук

Чтобы прочитать ss -tan, нужно знать состояния. Их 11:

Жизненный цикл TCP-соединения CLOSED LISTEN SYN_RECV ESTABLISHED SYN_SENT FIN_WAIT_1 CLOSE_WAIT LAST_ACK FIN_WAIT_2 CLOSING TIME_WAIT ↑ начальное состояние ► — открытие ⬜ — закрытие
СостояниеЧто значитDevOps-сигнал
CLOSEDСоединения нет
LISTENСервер слушает портНорма для service
SYN_SENTКлиент отправил SYN, ждётДолго — сервер не отвечает
SYN_RECVСервер отправил SYN-ACK, ждёт ACKМного — возможен SYN flood
ESTABLISHEDАктивный обменНорма
FIN_WAIT_1, FIN_WAIT_2Мы инициировали закрытиеНорма при close()
CLOSE_WAITДругая сторона закрыла, мы — нетМного = баг приложения, не закрывает сокеты
LAST_ACKМы ответили закрытию, ждём ACKКратковременно — норма
TIME_WAITСоединение закрыто, ждём 60-120сПод нагрузкой — много, может стать проблемой

14. TIME_WAIT — проблема и решения

Когда мы закрываем соединение, ОС держит его в TIME_WAIT 2*MSL (Maximum Segment Lifetime) — обычно 60-120 секунд. Зачем?

Под высокой нагрузкой (десятки тысяч соединений в секунду) накапливаются тысячи TIME_WAIT, и порты-источники заканчиваются. Симптом: Cannot assign requested address.

Решения:

15. RTO — Retransmission Timeout

Если ACK не пришёл за RTO, отправитель пересылает. RTO рассчитывается из измеренного RTT с поправкой на дисперсию (Jacobson's algorithm). Современный Linux обычно начинает с ~1 секунды, удваивает при каждом повторе (exponential backoff).

Это значит, что если канал «лагает», ты ждёшь секунду, две, четыре. Поэтому в API-серверах ставят свои timeout'ы короче, чем RTO — чтобы не ждать «застывшие» соединения.

16. Tuning Linux TCP

Прод-сервер обычно нуждается в настройке. Ключевые параметры:

# Размеры буферов — критично для высоких скоростей
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = "4096 87380 16777216"
net.ipv4.tcp_wmem = "4096 65536 16777216"

# Congestion control — BBR для серверов
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq

# Включить SACK, FACK, timestamps
net.ipv4.tcp_sack = 1
net.ipv4.tcp_timestamps = 1

# SYN cookies
net.ipv4.tcp_syncookies = 1

# Keepalive
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 6

# Очередь полу-открытых соединений
net.ipv4.tcp_max_syn_backlog = 4096
net.core.somaxconn = 4096

# TIME_WAIT reuse
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30

# Расширить ephemeral ports
net.ipv4.ip_local_port_range = "1024 65535"

17. Мини-лаба: попробуй прямо сейчас

Нужен любой Linux (виртуалка, WSL, сервер). Посмотри на живые соединения своей машины:

# Все TCP-соединения с состояниями
ss -tan
ss -tanep                    # + процессы (нужен root для других пользователей)

# По состояниям
ss -tan state listen
ss -tan state established
ss -tan state close-wait     # посмотри, нет ли утечки
ss -tan state time-wait | wc -l

# Расширенная инфо: cwnd, rtt, retrans
ss -tine

# Захват handshake
sudo tcpdump -nn -i any -S 'tcp[tcpflags] & (tcp-syn|tcp-ack|tcp-fin) != 0'

# Текущий congestion control
sysctl net.ipv4.tcp_congestion_control

# Доступные
sysctl net.ipv4.tcp_available_congestion_control

# Статистика TCP в целом
nstat | grep -i tcp
cat /proc/net/snmp | grep -A1 Tcp:
Ожидаемый результат

В ss -tine у каждого established-соединения ты найдёшь живые значения из этого урока: cwnd (окно перегрузки в сегментах), rtt (среднее/разброс), mss, имя congestion-алгоритма (обычно cubic), а при потерях — счётчик retrans. В tcpdump-захвате открой любое новое соединение и сверь три пакета рукопожатия: флаги S / S.A / ., опции MSS и WScale в первых двух. Если time-wait | wc -l показал сотни — перечитай раздел 14: теперь ты знаешь, норма это или звонок.

18. Что с этим делает DevOps

19. Задачи самопроверки — реши, потом раскрой ответ

Задача 1. На сервере 40 000 соединений в TIME_WAIT и 900 в CLOSE_WAIT. Какое из двух чисел тебя тревожит и почему?

Тревожит CLOSE_WAIT: это значит, что удалённая сторона закрыла соединение (пришёл FIN), а наше приложение так и не вызвало close() — соединения «утекли», и это чинится только в коде приложения. TIME_WAIT — штатная фаза после активного закрытия: сокеты в ней сами исчезнут через таймаут; при нехватке портов помогает tcp_tw_reuse и connection pooling, но сам по себе TIME_WAIT — признак нормальной работы, а не бага.

Задача 2. Пользователи за корпоративным VPN не могут открыть сайт: браузер «крутится» на больших страницах, маленькие открываются. ICMP на периметре запрещён полностью. Что происходит и какие два способа починить?

PMTUD black hole: VPN уменьшил MTU пути, сервер шлёт сегменты под Ethernet-MTU 1500 с DF-битом, промежуточное устройство их дропает и сообщает об этом ICMP-пакетом «fragmentation needed», который режет фаервол. Отправитель ничего не узнаёт — большие ответы молча теряются. Лечение: разрешить нужный тип ICMP (правильно) или включить MSS clamping на туннеле (роутер подрезает MSS в проходящих SYN), как обходной путь — уменьшить MTU интерфейса.

Задача 3. API отвечает за 3 мс, но клиент с RTT 150 мс получает файл 1 МБ почти секунду, хотя канал — гигабит. Почему так и что можно сделать?

Slow start: соединение начинает с маленького окна (ICW ~10 сегментов ≈ 14,6 КБ) и удваивает его каждый RTT. Чтобы разогнаться до мегабайта «в полёте», нужно несколько RTT по 150 мс — вот и секунда. Помогают: держать соединения тёплыми (keep-alive/pooling — окно уже разогнано), CDN (RTT падает с 150 до 10–20 мс), BBR на отдающем сервере, HTTP/2 — один разогнанный канал вместо шести холодных.

Задача 4. В логах балансировщика всплеск полуоткрытых соединений: тысячи SYN без завершения рукопожатия, backlog переполнен, легитимные клиенты получают отказ. Что за атака и какой механизм позволяет серверу пережить её без таблицы состояний?

SYN flood: атакующий шлёт SYN с подделанных адресов, сервер плодит полуоткрытые записи, пока backlog не кончится. SYN cookies: сервер не хранит состояние, а кодирует параметры соединения в самом ISN своего SYN-ACK; состояние восстанавливается из ACK честного клиента. Плюс: уменьшить счётчик ретрансмиссий SYN-ACK, включить фильтрацию/rate-limit на границе, спрятаться за anycast-CDN, размазывающим атаку.

Задача 5. После включения TCP_NODELAY интерактивный сервис (маленькие частые сообщения) резко ускорился. Что делали Nagle и delayed ACK до этого?

Классическое взаимное ожидание: Nagle на отправителе придерживает маленькие сегменты, пока не подтверждён предыдущий, а delayed ACK на приёмнике откладывает подтверждение до ~40 мс в надежде поехать «зайцем» с ответными данными. Вместе они дают паузы до 40+ мс на каждый мелкий обмен. TCP_NODELAY выключает Nagle — для интерактивных протоколов (RPC, игры, Redis) это стандартная практика.

20. Словарик урока

рукопожатие · three-way handshakeSYN → SYN-ACK → ACK: согласование ISN и опций, 1 RTT до данных
номер последовательности · sequence numberпозиция первого байта сегмента в потоке; основа порядка и надёжности
MSS · maximum segment sizeмаксимум полезных данных в сегменте; для Ethernet 1500 − 20 − 20 = 1460
поиск MTU пути · PMTUDопределение минимального MTU маршрута по ICMP «fragmentation needed»
окно приёма · receive window (rwnd)сколько байт готов принять получатель; масштабируется Window Scale
окно перегрузки · congestion window (cwnd)сколько байт отправитель рискует держать «в полёте»; растёт от slow start
медленный старт · slow startэкспоненциальный разгон cwnd с ~10 сегментов; цена холодного соединения
CUBIC / BBRалгоритмы контроля перегрузки: по потерям (CUBIC) против по модели канала (BBR)
выборочное подтверждение · SACKACK с указанием полученных «кусков» — переспрашивается только дыра
ретрансмит · retransmissionповтор неподтверждённого сегмента по таймеру (RTO) или по дублям ACK
SYN cookiesзащита от SYN flood: состояние кодируется в ISN, backlog не расходуется
TCP Fast Open · TFOданные уже в SYN повторного соединения — минус один RTT
Мост к следующему уроку

TCP-канал открыт — но он голый: любой на пути читает и подменяет байты. Прежде чем браузер отправит первый HTTP-байт, стороны должны договориться о шифровании, и сделать это быстро — желательно за один RTT. Как TLS 1.3 умудряется согласовать ключи, проверить личность сервера и не потерять ни миллисекунды — следующий этап.

Чек-лист «понимаю это»

TCP — это не только handshake, а целая система: slow start, cwnd, congestion control (BBR/CUBIC), SACK, MSS clamping, TFO, SYN cookies. На бумаге это теория — в продакшене это разница между «сайт летит» и «сайт тормозит». Понимать состояния (TIME_WAIT, CLOSE_WAIT) — обязательно для дебага. tcpdump и ss -tine — твои инструменты.