Этап 4 — TCP: соединение и поведение под нагрузкой
Знать что есть «SYN → SYN-ACK → ACK» — этого мало. Реальный TCP сложнее: размер окна, ICW, slow start, congestion control, Nagle, TFO, SYN cookies. Эти детали определяют, сколько твоё приложение выдержит и насколько быстро отдаст 1 МБ.
«Инцидент: файлы больше ~1 МБ через новый VPN не скачиваются — соединение устанавливается, мелкие страницы открываются, а крупная передача замирает намертво. Пинг идеальный». Прежде чем читать: почему «мелкое работает, крупное висит» — это почти диагноз? Какой невидимый механизм TCP мог сломаться от того, что кто-то на фаерволе «на всякий случай запретил весь ICMP»? Ответ соберётся в разделе про MSS и MTU.
- Прочитать TCP-заголовок по полям и tcpdump-вывод рукопожатия с опциями (MSS, WScale, SACK)
- Посчитать MSS из MTU и продиагностировать PMTUD black hole («мелкое ходит, крупное висит»)
- Объяснить slow start и congestion window — и почему первый мегабайт всегда медленнее десятого
- Выбрать congestion control под задачу: CUBIC против BBR, где BBR даёт кратный выигрыш
- Различить TIME_WAIT (норма, тюнится) и CLOSE_WAIT (утечка в коде приложения)
- Снять здоровье соединений на живом сервере:
ss -tine, состояния, ретрансмиты
В этапе 3 anycast привёл тебя на ближайший edge — теперь с ним надо установить соединение, и каждый RTT на счету. База: TCP/UDP и порты — «Сети с нуля», урок 4; MTU и кадры — «Инженер сетей», урок 2. Из этапа 1 помни строку «Initial connection» в DevTools → Timing — этот урок объясняет всё, что в ней происходит.
1. Что мы уже знаем — короткий recap
TCP — это «надёжная» доставка поверх IP. Главные гарантии: порядок, доставка, защита от перегрузки. Перед обменом данными — three-way handshake.
Этот этап — про что под капотом и почему оно работает именно так.
2. TCP-заголовок до байта
Каждый TCP-сегмент имеет заголовок ~20 байт. Знать его поля — обязательно для понимания.
| Поле | Что в нём |
|---|---|
| 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
Что важно увидеть:
- S = SYN, . = ACK (точка), S. = SYN+ACK
- Каждая сторона начинает со своего random ISN (это защита от подделок)
- Ack = «номер, который я жду следующим» = received_seq + 1
- В options согласуется
mss(Maximum Segment Size),wscale(Window Scale),sackOK,TS— об этом ниже - Между SYN и SYN-ACK — 29 мс. Это 1 RTT до сервера
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 интернет рухнул бы. Как работает?
У отправителя есть две переменные:
- cwnd (congestion window) — «сколько я могу слать, не дожидаясь ACK»
- rwnd (receive window) — то, что прислал приёмник
Отправитель шлёт 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 растёт до тех пор, пока:
- Не достигнет ssthresh (slow start threshold). Тогда переключается в congestion avoidance — рост уже линейный (по 1 сегменту за RTT).
- Не произошла потеря пакета. Тогда — Fast Retransmit и срезание cwnd.
При потере пакета
Классический TCP Reno:
- 3 дубликатных ACK — потеря, Fast Retransmit, cwnd / 2
- Timeout (RTO) — катастрофа, cwnd → 1 (полный slow start заново)
Современные алгоритмы (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-ACK кодирует криптографически подписанный «cookie», основанный на 4-tuple + времени + секретном ключе
- Если клиент настоящий — он пришлёт ACK с правильным номером (cookie+1). Сервер проверит подпись и создаст соединение «постфактум»
Это блокирует SYN flood без накладных расходов.
sysctl net.ipv4.tcp_syncookies
# 1 = включено (по умолчанию в Linux)
13. Состояния TCP — все 11 штук
Чтобы прочитать ss -tan, нужно знать состояния. Их 11:
| Состояние | Что значит | 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 секунд. Зачем?
- Защита от «зомби-пакетов» — старые сегменты от прошлого соединения могут прилететь после close, и если бы новое соединение с тем же 4-tuple уже было создано, эти пакеты бы попали в новое
- Гарантия, что финальный ACK дошёл до собеседника
Под высокой нагрузкой (десятки тысяч соединений в секунду) накапливаются тысячи TIME_WAIT, и порты-источники заканчиваются. Симптом: Cannot assign requested address.
Решения:
- Keep-alive / HTTP/2: переиспользовать одно соединение, а не открывать новое для каждого запроса
- tcp_tw_reuse: разрешить переиспользовать порт TIME_WAIT (Linux sysctl)
- Расширить диапазон ephemeral портов
- Не делать tcp_tw_recycle — он удалён из ядра, потому что ломает NAT
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
- Sysctl-тюнинг. Дефолты Linux не оптимальны для серверов под нагрузкой. Знать, что менять и зачем.
- BBR на стриминговых серверах. Видео, образы Docker, S3-bucket — переключение на BBR может дать +30-100% throughput.
- TIME_WAIT. Понимать, когда это становится проблемой, и как чинить.
- CLOSE_WAIT. Это сигнал «приложение баги». Не лечится в инфре — фиксится в коде.
- Keep-alive / connection pooling. В клиентах и в LB. Без них производительность катится.
- Timeout-стратегия. Read / write / idle timeout — настраивать осознанно. Default'ы часто плохие.
- Чтение tcpdump. При инциденте — это твой главный инструмент. Уметь интерпретировать флаги, последовательности.
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. Словарик урока
TCP-канал открыт — но он голый: любой на пути читает и подменяет байты. Прежде чем браузер отправит первый HTTP-байт, стороны должны договориться о шифровании, и сделать это быстро — желательно за один RTT. Как TLS 1.3 умудряется согласовать ключи, проверить личность сервера и не потерять ни миллисекунды — следующий этап.
Чек-лист «понимаю это»
- ☐ Назову поля TCP-заголовка и зачем каждое
- ☐ Прочитаю tcpdump-вывод handshake
- ☐ Объясню MSS, MTU, PMTUD, MSS clamping
- ☐ Понимаю receive window и Window Scale
- ☐ Объясню slow start, congestion avoidance, ICW
- ☐ Различаю CUBIC и BBR
- ☐ Знаю, что такое SACK и зачем
- ☐ Понимаю Nagle + delayed ACK и почему TCP_NODELAY
- ☐ Расскажу про TFO и SYN cookies
- ☐ Различаю CLOSE_WAIT и TIME_WAIT, знаю как лечить каждое
- ☐ Прочитал sysctl-настройки на своём сервере
TIME_WAIT, CLOSE_WAIT) — обязательно для дебага. tcpdump и ss -tine — твои инструменты.