Пошаговая анимация TCP 3-way handshake: SYN, SYN-ACK, ACK + data transfer + FIN
TCP находится под каждым классическим HTTP request, большинством database connections, Redis, Kafka clients, gRPC over HTTP/2, WebSocket и TLS. Когда пользователь жалуется на 200 ms лишней latency, когда backend неожиданно упирается в connection pool, когда сервер не принимает новые sockets после всплеска traffic, причина часто не в бизнес-коде, а в TCP lifecycle.
Трехстороннее рукопожатие стоит один RTT до передачи первых данных. На локальной сети это почти незаметно, на межконтинентальном соединении это 100-200 ms до TLS и HTTP. Закрытие соединения оставляет TIME_WAIT. Потеря packet запускает retransmission. Неправильный keep-alive создает handshake storm. SYN flood заполняет backlog еще до того, как Nginx увидит полноценный request.
Знание TCP нужно архитектору не для ручного выставления всех sysctl, а для правильных решений: использовать connection pooling, keep-alive, TLS session resumption, HTTP/2 multiplexing, разумные timeouts, SYN flood protection и capacity planning по sockets. И главное: TCP reliable только в пределах живого connection и достижимой сети. Он не делает distributed system надежной при partition.
::concept{slug="http-protocol"} ::concept{slug="tls-handshake"} ::concept{slug="websocket"}
TCP это reliable ordered byte stream поверх ненадежного IP. IP доставляет packets best-effort, TCP добавляет sequence numbers, acknowledgements, retransmission, flow control и congestion control. Для приложения это выглядит как поток bytes: записал в socket, прочитал из socket. Но под капотом есть state machine.
Handshake нужен, чтобы обе стороны согласовали initial sequence numbers и подтвердили, что обратный путь тоже работает. Client отправляет SYN с seq=x, server отвечает SYN-ACK с seq=y, ack=x+1, client отправляет ACK с ack=y+1. Только после этого connection считается established.
ADR-style framing: TCP покупает надежный ordered stream, но платит setup latency, kernel state, congestion adaptation и сложным close lifecycle. Для request/response web traffic это нормально, если connection reuse включен. Для ultra-low-latency или loss-tolerant traffic иногда выбирают UDP/QUIC, но тогда часть ответственности переезжает в другой protocol layer.
Диаграмма показывает client host с browser и TCP stack, server host с TCP stack и Nginx. Ребра разделяют application socket API и реальное TCP соединение между stacks. Это важно: browser не отправляет SYN напрямую в Nginx. Он вызывает connect(), kernel TCP stack делает handshake, а Nginx получает connection только после accept().
Сценарии последовательно показывают handshake, передачу данных, FIN close и SYN flood. Диаграмма не пытается показать весь TCP: нет полного congestion control, sliding window dynamics или TLS. Она фокусируется на lifecycle connection и том, какие состояния видит kernel.
Ключевая деталь: sequence и acknowledgement numbers считают bytes, а не messages. HTTP request на 200 bytes меняет ACK на seq+200. Это помогает понять, почему TCP не сохраняет границы application messages и почему protocols поверх TCP должны иметь framing: HTTP headers/content-length, Redis RESP, Postgres protocol и так далее.
handshake показывает SYN, SYN-ACK, ACK и переход server TCP stack в established state. Nginx принимает socket только после завершения handshake. Урок: до application layer уже потрачен RTT и выделено kernel state. Если клиент открывает новое TCP соединение на каждый request, он платит эту цену постоянно.
data показывает PSH+ACK, получение HTTP request, ответ Nginx и ACK от клиента. Урок: TCP подтверждает bytes и управляет окном. Быстрое приложение все равно ограничено RTT, receiver window, congestion window и packet loss.
close показывает 4-way close: FIN, ACK, FIN, ACK и TIME_WAIT на стороне инициатора. Урок: close не мгновенный. TIME_WAIT защищает от старых duplicate packets и позволяет корректно завершить connection, но в high-traffic системах может стать capacity issue.
syn-flood показывает DoS до application layer. Атакующий отправляет много SYN с поддельных IP и не завершает handshake. Server держит half-open entries в backlog, пока он не переполнится. Урок: Nginx, app и auth logic могут быть невиновны; проблема может быть в kernel backlog и SYN handling. SYN cookies позволяют не хранить state до финального ACK.
ADR-001: new connection per request против keep-alive/pooling. Новое соединение проще: нет долгого state, меньше проблем с stale sockets, понятная изоляция. Цена: TCP handshake, TLS handshake, slow start и kernel allocations на каждый request. Keep-alive и connection pools переиспользуют warmed connection, уменьшают latency и CPU. Цена: надо настраивать idle timeout, max lifetime, pool size и handling broken connections. Для HTTP clients, DB и Redis pools reuse почти всегда обязателен.
ADR-002: low latency small writes против Nagle. Nagle's algorithm буферизует маленькие writes, чтобы не слать много tiny packets. Это полезно для throughput, но вместе с delayed ACK может дать latency spikes около 200 ms. Для interactive protocols, RPC и request/response API часто включают TCP_NODELAY. Для bulk transfer Nagle может быть приемлем. Решение зависит от профиля traffic, а не от мифа что один режим всегда лучше.
ADR-003: fast close против TIME_WAIT safety. TIME_WAIT раздражает при большом количестве short-lived connections, потому что занимает ports и kernel memory. Но он защищает protocol correctness. Агрессивные sysctl вроде reuse/recycle исторически ломали NAT и distributed clients. Лучшее решение обычно не убивать TIME_WAIT, а уменьшить churn: keep-alive, pooling, HTTP/2 multiplexing, правильные client-side limits.
ADR-004: TCP против QUIC/HTTP/3. TCP зрелый, ubiquitous и хорошо оптимизирован. Но TCP+TLS требует несколько round trips на cold start, а packet loss может блокировать все streams в connection на HTTP/2. QUIC поверх UDP объединяет transport и crypto handshake, поддерживает stream-level loss recovery и connection migration. Цена: другой operational model, UDP filtering, новые метрики и server support.
Nginx и HAProxy принимают TCP connections, управляют keep-alive, backlog, proxy timeouts и upstream pools. Их настройки напрямую влияют на latency и устойчивость при spikes.
AWS NLB работает на L4 и прокидывает TCP traffic, сохраняя меньше HTTP semantics, чем ALB. Это полезно для protocols, где нужен raw TCP или TLS passthrough.
gRPC поверх HTTP/2 поверх TCP активно использует long-lived connections и multiplexing. Поэтому один плохой connection или неправильный keepalive может повлиять на много RPC.
Database pools используют TCP sockets к Postgres/MySQL. Без pool приложение создает connection storm: TCP handshake, auth handshake, server process/thread allocation и memory на каждую операцию.
HTTP/3 и QUIC показывают, как индустрия пытается уменьшить handshake penalty и head-of-line blocking, но TCP остается базой для огромной части backend инфраструктуры.
Открывать новый TCP connection на каждый HTTP request или SQL query. Это множит handshake latency и убивает server accept capacity.
Считать TCP message-oriented protocol. TCP это byte stream; если application protocol не имеет framing, чтение может получить половину message или несколько messages сразу.
Игнорировать TIME_WAIT и ephemeral ports. Short-lived outbound connections могут упереться в port exhaustion раньше CPU.
Настраивать timeouts только на application layer. Connection timeout, read timeout, idle timeout и request deadline решают разные проблемы.
Отключать Nagle или менять sysctl без измерений. Иногда latency improves, иногда растет packet overhead и CPU.
Считать, что TCP гарантирует доставку при partition. Если connection умер, процесс упал или сеть разделилась, приложение должно иметь retry/idempotency/timeout logic.
Не защищать backlog от SYN flood. До app firewall и auth request может не дойти вообще.
Не выбирай raw TCP для public API, если можно использовать HTTP/gRPC/WebSocket с готовым tooling: observability, proxies, auth, load balancing и retries уже решены лучше.
Не используй TCP long-lived connections там, где clients нестабильны, NAT агрессивно режет idle, а приложение не умеет heartbeat/reconnect. Для mobile push часто лучше platform push channels.
Не оптимизируй kernel TCP settings до измерений. Большинство проблем решаются на уровне pooling, keep-alive, timeout budgets и load balancer config.
Не используй TCP как замену application-level delivery guarantees. Для payment/order events нужны idempotency keys, durable queues или transactions, а не надежда на socket.
Дальше полезно читать http-protocol, потому что HTTP latency складывается поверх TCP. Затем tls-handshake, чтобы увидеть еще один setup layer. Для realtime переходи к websocket, где long-lived TCP становится частью application design. Для reliability обязательно связать TCP с timeout-deadline: socket может висеть, но пользовательский request должен иметь бюджет.
Связанные CloudArch материалы: ::concept{slug="http-protocol"} ::concept{slug="tls-handshake"} ::concept{slug="websocket"} ::concept{slug="timeout-deadline"}
Внешние источники: W. Richard Stevens, TCP/IP Illustrated; Brendan Gregg TCP performance tuning; Google BBR paper; Linux tcp(7); System Design Space chapter on TCP protocol.