Модуль: Сети и протоколы · Уровень: 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Примеры
ApplicationL5-L7HTTP, gRPC, DNS, TLS
TransportL4TCP, UDP, QUIC(поверх UDP)
InternetL3IP, ICMP, маршрутизация
LinkL1-L2Ethernet, 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):

  1. Slow start: cwnd растёт экспоненциально до ssthresh.
  2. Congestion avoidance: линейный рост (AIMD).
  3. Fast retransmit / fast recovery: 3 дубликата ACK → ретрансмит без ожидания таймаута.
  4. Алгоритмы: 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-aliveHTTP keep-alive
УровеньL4 (ОС)L7 (приложение)
Цельдетектить мёртвые peer’ы, держать NAT-маппингпереиспользовать TCP-соединение для нескольких HTTP-запросов
УправлениеSetKeepAlive, tcp_keepalive_timeConnection: 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.TCPConnSetNoDelay, 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).