Модуль: Сети и протоколы · Уровень: Middle+/Senior
TL;DR#
- TCP/IP — это 4-уровневая модель (Link / Internet / Transport / Application), на практике вместо OSI обсуждают именно её.
- TCP даёт надёжную, упорядоченную, байт-ориентированную доставку поверх ненадёжного IP за счёт sequence/ack-номеров, ретрансмиссий и контрольных сумм.
- Установка — 3-way handshake (SYN / SYN-ACK / ACK), закрытие — 4-way (FIN/ACK обе стороны), активный закрыватель уходит в
TIME_WAITна 2×MSL. - Flow control = защита получателя (sliding window), congestion control = защита сети (slow start, congestion avoidance, fast recovery, CUBIC/BBR).
- Бэкендеру критичны:
TIME_WAIT-исчерпание портов, Nagle vs delayed ACK (латентность), TCP keep-alive vs HTTP keep-alive (это разные вещи), backlog очередей accept.
Теория#
Модель TCP/IP vs OSI#
| TCP/IP | Аналог OSI | Примеры |
|---|---|---|
| Application | L5-L7 | HTTP, gRPC, DNS, TLS |
| Transport | L4 | TCP, UDP, QUIC(поверх UDP) |
| Internet | L3 | IP, ICMP, маршрутизация |
| Link | L1-L2 | Ethernet, ARP, MAC |
Senior-нюанс: TLS формально живёт между L4 и L7, поэтому в собеседовании TLS обычно относят к “L6-ish” в OSI, но в TCP/IP-модели это просто часть Application.
TCP handshake (установка)#
Client Server
| --- SYN (seq=x) ----------> | # клиент шлёт ISN
| <-- SYN-ACK (seq=y, ack=x+1)| # сервер подтверждает + свой ISN
| --- ACK (ack=y+1) --------> | # соединение ESTABLISHED- ISN (Initial Sequence Number) выбирается рандомно (защита от спуфинга/предсказания).
- Параметры согласуются в опциях SYN: MSS, window scaling, SACK, timestamps.
- SYN backlog / accept queue: ОС держит две очереди — incomplete (SYN получен) и complete (handshake завершён, ждёт
accept()). Переполнение complete-queue (somaxconn,net.core.somaxconn) приводит к дропу/ретрансмиссии SYN — частая причина “таймаутов на подключение” под нагрузкой. - TCP Fast Open (TFO): позволяет слать данные уже в SYN, экономит RTT, но требует поддержки с обеих сторон и редко включён.
TCP teardown (закрытие)#
| --- FIN -----------> | (активный закрыватель -> FIN_WAIT_1)
| <-- ACK ------------ |
| <-- FIN ------------ |
| --- ACK -----------> | (-> TIME_WAIT)- Возможен simultaneous close и half-close (одна сторона закрыла отправку, но читает —
CloseWrite()в Go:tcpConn.CloseWrite()).
TIME_WAIT#
Активный закрыватель остаётся в TIME_WAIT на 2×MSL (обычно 60s в Linux, MSL=30s, не настраивается без пересборки ядра).
- Зачем: (1) гарантировать доставку последнего ACK; (2) дать “умереть” задержавшимся сегментам старого соединения, чтобы они не попали в новое с тем же 4-tuple.
- Проблема: при большом числе исходящих коротких соединений (например, сервис, который дёргает другой сервис без пулинга) исчерпываются эфемерные порты → “cannot assign requested address”.
- Решения: переиспользование соединений (HTTP keep-alive, пулы!),
net.ipv4.tcp_tw_reuse=1(для исходящих, безопасно), расширениеip_local_port_range.tcp_tw_recycle— удалён/опасен (ломал NAT), не упоминать как решение.
Flow control (контроль потока)#
- Защищает получателя от переполнения буфера.
- Receiver объявляет
rwnd(receive window) в каждом ACK; sender не шлёт больше, чемmin(rwnd, cwnd)неподтверждённых байт. - Window scaling (опция) расширяет окно за пределы 64КБ — обязательно для high-BDP линков (high bandwidth-delay product).
- Zero window / silly window syndrome: если получатель медленно читает, окно схлопывается в ноль; отправитель шлёт window probes.
Congestion control (контроль перегрузки)#
Защищает сеть. Sender держит cwnd (congestion window):
- Slow start: cwnd растёт экспоненциально до
ssthresh. - Congestion avoidance: линейный рост (AIMD).
- Fast retransmit / fast recovery: 3 дубликата ACK → ретрансмит без ожидания таймаута.
- Алгоритмы: CUBIC (дефолт в Linux, loss-based), BBR (Google, model-based по bandwidth+RTT, лучше на длинных/лоссистых линках).
Nagle и delayed ACK#
- Nagle (
TCP_NODELAY=false, дефолт on): буферизует мелкие записи, пока не подтверждён предыдущий пакет → меньше мелких сегментов, но +латентность. - Delayed ACK: получатель откладывает ACK (~40-200ms), надеясь приклеить его к данным.
- Антипаттерн: Nagle + delayed ACK вместе дают артефактную задержку до ~40ms на request/response протоколах. Поэтому для RPC/интерактивных протоколов ставят
TCP_NODELAY.
// Go по умолчанию ВЫКЛЮЧАЕТ Nagle для TCP-соединений (SetNoDelay(true) по дефолту).
ln, _ := net.Listen("tcp", ":8080")
conn, _ := ln.Accept()
tcp := conn.(*net.TCPConn)
tcp.SetNoDelay(true) // дефолт, но явноKeep-alive: два разных уровня#
| TCP keep-alive | HTTP keep-alive | |
|---|---|---|
| Уровень | L4 (ОС) | L7 (приложение) |
| Цель | детектить мёртвые peer’ы, держать NAT-маппинг | переиспользовать TCP-соединение для нескольких HTTP-запросов |
| Управление | SetKeepAlive, tcp_keepalive_time | Connection: keep-alive, http.Transport.IdleConnTimeout |
d := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second, // TCP keep-alive probes
}Go 1.20+ умеет настраивать keep-alive count/interval через net.KeepAliveConfig.
Где это всплывает в Go#
net.Dialer— таймауты подключения, keep-alive, dual-stack (Happy Eyeballs для IPv4/IPv6).net.TCPConn—SetNoDelay,SetLinger,CloseWrite,SetReadBuffer/SetWriteBuffer.SetLinger(0)шлёт RST вместо FIN при закрытии → соединение НЕ уходит в TIME_WAIT (но теряются недоставленные данные — использовать осторожно).
Подводные камни / gotchas#
- TIME_WAIT на стороне клиента-сервиса: если ваш сервис — активный закрыватель тысяч исходящих коннектов без пула, порты кончатся. Лечится пулингом, а не тюнингом ядра.
- Путают TCP keep-alive и HTTP keep-alive — это разные механизмы на разных уровнях.
tcp_tw_recycle— мёртвое и вредное, упоминание как решения = красный флаг.- Half-open соединения: если peer упал без FIN (kill -9, обрыв сети), вы узнаете об этом только при попытке записи или по keep-alive/таймауту чтения. Отсюда обязательность read/write deadlines.
backlog/somaxconn: дефолты Linux менялись (128 → 4096 в новых ядрах), под нагрузкой надо проверятьss -lnt(колонка Recv-Q для слушающего сокета = текущая accept-queue).- MSS vs MTU и Path MTU Discovery: чёрные дыры PMTUD (ICMP заблокирован файрволлом) → зависания на больших ответах. Классика прода.
- Nagle включён в вашем не-Go клиенте + delayed ACK = плавающие 40ms задержки, которые сложно диагностировать.
Вопросы на собеседовании#
В: Зачем нужен TIME_WAIT и почему он на стороне того, кто закрывает первым? О: Чтобы (1) надёжно доставить последний ACK на FIN peer’а и (2) дать задержавшимся сегментам старого соединения исчезнуть до переиспользования того же 4-tuple. Длится 2×MSL. На активном закрывателе, потому что именно он рискует получить ретрансмит FIN и должен на него ответить.
В: В чём разница между flow control и congestion control?
О: Flow control защищает получателя (rwnd, объявляется в ACK). Congestion control защищает сеть (cwnd, локальная оценка отправителя через slow start/AIMD/loss/RTT). Отправитель ограничен min(rwnd, cwnd).
В: Что такое проблема Nagle + delayed ACK?
О: Nagle придерживает мелкие сегменты до подтверждения предыдущего, delayed ACK придерживает ACK ~40ms. На request/response это даёт взаимную блокировку и латентность до ~40ms. Лечится TCP_NODELAY. Go ставит NoDelay по умолчанию.
В: Чем TCP keep-alive отличается от HTTP keep-alive? О: TCP keep-alive — L4-механизм ОС для детекта мёртвых соединений и удержания NAT. HTTP keep-alive — L7, переиспользование одного TCP для нескольких HTTP-запросов. Они независимы.
В: Сервис под нагрузкой получает “cannot assign requested address” при исходящих запросах. Причина?
О: Исчерпание эфемерных портов из-за множества соединений в TIME_WAIT (сервис — активный закрыватель). Правильное решение — переиспользование соединений (HTTP-пул, MaxIdleConnsPerHost), затем tcp_tw_reuse и расширение ip_local_port_range. НЕ tcp_tw_recycle.
В: Что произойдёт, если accept-queue переполнится?
О: Новые завершённые handshake-соединения дропаются, клиенту приходит ретрансмит SYN или таймаут. Регулируется somaxconn и backlog в listen(). Симптом — спорадические таймауты на connect под пиком.
В: Как обнаружить, что peer “тихо” умер?
О: Через TCP keep-alive probes или, что надёжнее на уровне приложения, через read/write deadlines (SetReadDeadline). Без них горутина может висеть бесконечно на мёртвом соединении.
В: Зачем window scaling и SACK? О: Window scaling позволяет окну превысить 64КБ — нужно для высокого BDP (high bandwidth × delay), иначе throughput упирается в потолок. SACK (selective ACK) позволяет подтверждать непрерывные блоки при потерях, избегая ретрансмита уже доставленного.
На что копают на senior+#
- Различие CUBIC vs BBR и когда BBR выигрывает (длинные/лоссистые линки, bufferbloat).
- Bandwidth-Delay Product и расчёт нужного размера окна/буферов под конкретный RTT.
- Поведение под NAT и почему
tcp_tw_recycleего ломал (timestamp-эвристика на агрегированных адресах). - Как RST отличается от FIN, когда ядро шлёт RST (закрытый порт,
SO_LINGER 0, переполнение очереди), и почему RST не имеет рукопожатия подтверждения. - Diagnostics-инструментарий:
ss -tin(cwnd, rtt, retrans),tcpdump,nstat, что смотреть при подозрении на потери/ретрансмиты. - Влияние выбора TCP vs QUIC на head-of-line blocking на L4 (см. HTTP/3).