Learning Platform
Глоссарий Troubleshooting
Урок 09.05 · 22 мин
Средний
TCPCongestion ControlSlow StartBBRCubic

Congestion control — slow start, congestion avoidance и почему TCP замедляется при потерях

В прошлом уроке мы обсудили flow control — как получатель замедляет отправителя, когда не справляется. Теперь — об ещё одной форме саморегуляции: congestion control. Это о том, как отправитель сам замедляется, когда сеть перегружена.

Это, пожалуй, самая важная фича TCP с точки зрения функционирования интернета. Без неё интернет бы не работал. В 1986 году произошёл первый известный «congestion collapse»: пропускная способность ARPANET упала с ожидаемых 32 Kbps до 40 bps — в 800 раз. Все слали так быстро, как могли, сети не справлялись, пакеты дропались, TCP-стеки тогда не реагировали умно, и сеть просто захлёбывалась. После этого Ван Якобсон описал серию алгоритмов congestion control, которые с тех пор лежат в основе TCP.

В этом уроке разберём:

  1. Как TCP детектирует перегрузку (по потерям пакетов).
  2. Алгоритмы slow start и congestion avoidance.
  3. Fast retransmit, fast recovery.
  4. Современные алгоритмы: Reno, Cubic (Linux default), BBR.
  5. Почему TCP «не умеет» быстро восстанавливаться после потерь на длинных каналах.

Главная идея: потери = перегрузка

Базовая интуиция TCP congestion control: если есть потери пакетов, значит, сеть перегружена. Это не всегда верно (потери могут быть из-за помех, например на Wi-Fi), но в 90-е, когда придумывали TCP, основной причиной потерь действительно была перегрузка роутеров.

Эта интуиция приводит к простому правилу:

  • Нет потерь -> можно ускоряться (сеть выдерживает).
  • Есть потери -> замедляться (не убивай сеть).

TCP реализует это через congestion window (cwnd) — внутреннюю переменную отправителя. cwnd определяет, сколько байт максимум может быть «в полёте» (отправлено, не подтверждено). Эффективное окно = min(rwnd, cwnd):

  • rwnd — flow control, заявленное получателем.
  • cwnd — самооценка отправителя, что выдержит сеть.

Когда нет потерь, cwnd растёт. Когда есть — резко падает.


Slow Start — медленный старт

В начале соединения TCP не знает, какая пропускная способность канала. Шпарить со 100% сразу нельзя — можешь перегрузить. Поэтому TCP стартует медленно и экспоненциально ускоряется.

Алгоритм slow start:

  1. Начальный cwnd = IW (Initial Window). На современных Linux IW = 10 MSS (~14.6 KB).
  2. На каждый полученный ACK cwnd увеличивается на 1 MSS.
  3. Получив ACK на N сегментов в RTT, cwnd удваивается каждый RTT.
  4. Продолжается до ssthresh (slow start threshold) или до первой потери.
Slow start -- экспоненциальный рост cwnd
RTT 1: cwnd=10 MSSСтартовый initial window. Отправитель шлёт 10 сегментов, ждёт ACK
ACKs
RTT 2: cwnd=20 MSSПолучили 10 ACKов, каждый добавил 1 MSS -- cwnd удвоилось
ACKs
RTT 3: cwnd=40 MSSУдвоение продолжается. После N RTT cwnd = 10 * 2^N
RTT 4: cwnd=80 MSSУже значительные данные в полёте
ACKs
RTT 5: cwnd=160 MSSСкоро будет потеря, сеть достигает потолка
LOSS!
ssthresh=80, cwnd=80При первой потере сеть подсказала: 'дальше не лезь'. ssthresh устанавливается в половину cwnd, TCP переходит в congestion avoidance

Slow start заканчивается, когда:

  • cwnd достиг ssthresh (переход в congestion avoidance), или
  • произошла потеря (переключение в специальную фазу recovery).

Название «slow start» — историческое. В современных сетях это далеко не медленно: за несколько RTT cwnd может достичь мегабайт. Но «медленно» по сравнению с теоретическим максимумом сразу.

# Замерь slow start вживую:
curl -w 'Total: %{time_total}s, Throughput peak: %{speed_download}\n' \
     -o /dev/null https://speed.cloudflare.com/__down?bytes=1000000

# Сравни с большим файлом:
curl -w 'Total: %{time_total}s, Throughput peak: %{speed_download}\n' \
     -o /dev/null https://speed.cloudflare.com/__down?bytes=100000000

# На маленьком файле скорость низкая -- slow start не успевает раскачаться
# На большом -- TCP «разгоняется», throughput растёт до канального

Congestion Avoidance — линейный рост

После slow start TCP переходит в congestion avoidance: cwnd растёт линейно, а не экспоненциально. Конкретно: на каждый RTT cwnd увеличивается на 1 MSS (а не удваивается).

Идея: близко к пределу сети нужно расти осторожно. Если рывками удваивать, потери возникнут быстро и сильно. Линейный рост — мягче.

Формально: на каждый ACK cwnd увеличивается на (MSS * MSS) / cwnd. В сумме за RTT (когда приходит cwnd/MSS ACKов) — это 1 MSS.

Полный цикл TCP congestion control
Slow startЭкспоненциальный рост: cwnd удваивается каждый RTT. Это быстро 'находит' canacity сети
cwnd >= ssthresh
Congestion avoidanceЛинейный рост: +1 MSS каждый RTT. Аккуратное приближение к лимиту сети
Loss detected3 duplicate ACK (fast retransmit) или RTO. Сеть сигнализирует: 'я не справляюсь'
ssthresh = cwnd/2
Fast recovery (Reno)Не сбрасываем cwnd до 1. Уменьшаем cwnd до ssthresh, переходим в congestion avoidance. Это 'AIMD' -- Additive Increase, Multiplicative Decrease
RTO (timeout)Если ACK совсем не пришёл за RTO -- это сигнал жёсткой проблемы. cwnd сбрасывается до 1 MSS, ssthresh = cwnd/2, начинается slow start заново

AIMD — основной паттерн

То, что TCP делает в установившемся режиме, называется AIMD (Additive Increase, Multiplicative Decrease):

  • Additive Increase: при отсутствии потерь cwnd растёт на 1 MSS за RTT.
  • Multiplicative Decrease: при потере cwnd падает наполовину.

Это даёт характерную «зубчатую» картинку cwnd с течением времени: медленный рост, резкое падение, медленный рост, резкое падение. Каждый «зуб» — это период между потерями.

Почему именно AIMD? Это математически правильное поведение для fairness: если N потоков делят канал, AIMD ведёт всех к равной доле. Multiplicative Decrease при потере уравнивает (всех бьёт в одинаковой пропорции), Additive Increase даёт возможность маленьким догнать. Это доказано в теории — AIMD сходится к fairness.

Сравни с MIMD (Multiplicative Increase, Multiplicative Decrease) или AIAD — они не сходятся.


Fast Retransmit и Fast Recovery

В уроке про reliability мы уже видели fast retransmit: 3 duplicate ACK триггерят немедленный retransmit (не дожидаясь RTO). В контексте congestion control это сопровождается fast recovery (TCP Reno):

  1. Получили 3 dup ACK -> потеря детектирована.
  2. ssthresh = cwnd / 2.
  3. cwnd = ssthresh + 3 MSS (потому что 3 сегмента уже у получателя).
  4. Каждый последующий dup ACK: cwnd += 1 (можно слать ещё сегменты, ведь они уже не «в полёте»).
  5. Когда придёт «свежий» ACK (после retransmit) -> cwnd = ssthresh, переход в congestion avoidance.

Это умное поведение: после потери TCP не сбрасывает cwnd до 1 (это сделал бы slow start). Получаем плавную деградацию вместо обвала.

Однако: при RTO (полный таймаут, не fast retransmit) cwnd всё-таки сбрасывается до 1 — это знак, что сеть серьёзно не справляется, начинаем сначала.


Разные алгоритмы: Reno, Cubic, BBR

TCP Reno — описанное выше. Классика, RFC 5681. Все ОС поддерживают.

TCP Cubic — современный default в Linux (с ~2008). Главное отличие: cwnd растёт не линейно, а по кубической функции от времени с момента последней потери. Это даёт более агрессивный рост на больших каналах с большим RTT, при этом более стабильный размер cwnd около «потолка». Cubic лучше использует bandwidth-delay product современных сетей.

TCP BBR (Bottleneck Bandwidth and Round-trip propagation time) — алгоритм Google, выпущенный в 2016. Принципиально другой подход: BBR не использует потери как сигнал. Вместо этого он измеряет реальный bandwidth и minimum RTT, и держит окно равным BDP = bandwidth * min_RTT.

Планировщик ОС и справедливое распределение ресурсов Это значит:

  1. Не наполняет буферы — отлично решает bufferbloat.
  2. Не теряет throughput при случайных потерях (например, на Wi-Fi).
  3. Работает значительно лучше на «long fat networks» (большой RTT, большая bandwidth, лёгкие потери).
# Какой congestion control используется у тебя сейчас:
sysctl net.ipv4.tcp_congestion_control
# Обычно: cubic (default в Linux)

# Что доступно:
sysctl net.ipv4.tcp_available_congestion_control
# Что-то вроде: reno cubic bbr

# Переключиться на BBR (если поддерживается):
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr

# Проверить, что текущие соединения переключились:
ss -ti | head
# или для конкретного:
ss -ti dst 8.8.8.8

Сравнение коротко:

  • Reno — классика, надёжно работает везде, но плохо использует high-BDP сети.
  • Cubic — оптимизирован для современного интернета, лучше Reno на больших каналах. Default в Linux.
  • BBR — отлично работает с lossy сетями и bufferbloat, но может быть «жадным» в конкуренции с Cubic-потоками.

Почему TCP замедляется на длинных каналах

«Long fat network» (LFN) — это сеть с большим bandwidth и большим RTT. Пример: оптоволокно из Москвы в Сан-Франциско, gigabit + RTT 200 мс. BDP = 1 Gbps * 0.2 sec = 25 MB.

На таких сетях обычный TCP (Reno, Cubic) страдает по нескольким причинам:

  1. Slow start долгий. Чтобы cwnd вырос до 25 MB, нужно много RTT (даже при удвоении). На RTT 200 мс это секунды, а на короткой сессии не успеешь раскачаться.

  2. Чувствительность к loss. Если в established состоянии случилась потеря, cwnd падает наполовину. На 25 MB BDP это значит, что после потери ты теряешь 12.5 MB throughput, и восстанавливается это линейно (1 MSS / RTT). Восстановление до пика может занять минуты.

  3. Один пакет — много данных. При потере одного сегмента, в transit могут быть мегабайты данных. Все «зависают» в окне retransmission queue.

Решения:

  • BBR не использует loss как сигнал, поэтому случайные потери не вызывают коллапс throughput.
  • MPTCP (Multipath TCP) — несколько параллельных подключений, агрегирует bandwidth.
  • QUIC — наследует уроки этих проблем, имеет более продвинутые алгоритмы recovery (RACK, persistent congestion).

ECN — Explicit Congestion Notification

Идея: вместо того чтобы дропать пакеты при перегрузке, роутер может пометить пакет, сигнализируя «я перегружен». ECN — биты в IP-заголовке. Получатель видит ECN-метку, шлёт обратно ECN-Echo (ECE) флаг в TCP-заголовке. Отправитель замедляется, как при потере, но без потери данных.

ECN — гораздо более эффективный сигнал congestion: нет лишней retransmission, нет задержек на recovery. Но ECN требует поддержки на всём пути (роутеры, оба endpoint). Современные ОС поддерживают, но не все приложения включают. Linux:

# Включить ECN:
sysctl net.ipv4.tcp_ecn
# 0 = выключен, 1 = включён, 2 = только инициируется по запросу

В современном интернете ECN постепенно распространяется. Cloudflare и Google активно используют. С QUIC это становится более стандартным.


Влияние congestion control на ваш код

Что нужно знать практически:

  1. Первое подключение всегда медленнее. Slow start. Поэтому connection pooling, HTTP keep-alive, HTTP/2 multiplexing — переиспользование connections даёт огромный буст.

  2. Маленькие запросы не получат полной пропускной способности. За 100 мс slow start пройдёт только 4-5 удвоений, throughput не достигнет максимума.

  3. При случайных потерях (Wi-Fi, сотовая) cubic может вести себя плохо. На таких сетях BBR обычно лучше.

  4. Bandwidth-Delay Product (BDP) определяет требуемый buffer size. Для high-bandwidth high-latency нужны большие буферы (и Window Scaling).

  5. TCP «честно» делит канал между потоками. N подключений -> каждое получает ~1/N. Если хочешь больше — нужно больше параллельных подключений или специальный congestion control.

# Полезные инструменты:
# (1) iperf3 -- тестирует throughput между двумя хостами
iperf3 -c <server>

# (2) ss -ti -- текущая статистика congestion для соединения
ss -ti dst <peer>
# Output: cwnd:10 ssthresh:65535 cubic ...

# (3) Слежение за реальным cwnd:
nstat -az TcpExtTCPLossProbes TcpExtTCPLossProbeRecovery
# Сколько раз сработала TCP probe (для подсчёта loss)

Попробуй сам

# 1. Замерь поведение slow start на разных размерах файлов:
for size in 10000 100000 1000000 10000000; do
  echo "Downloading ${size} bytes:"
  curl -o /dev/null -s -w "Time: %{time_total}s  Throughput: %{speed_download} B/s\n" \
       "https://speed.cloudflare.com/__down?bytes=$size"
done
# На маленьких -- низкий throughput (slow start не успевает раскачаться)
# На больших -- реальная пропускная способность канала

# 2. Посмотри congestion control в действии:
ss -ti dst 8.8.8.8 || true
# Запусти параллельно скачивание:
curl -o /dev/null https://google.com/largefile &
sleep 1
ss -ti | grep google
# Видишь cwnd:NNN -- это текущее congestion window
# Если повторишь несколько раз -- увидишь, как растёт

# 3. Сравни Cubic и BBR (если можешь свитчить):
# Текущий:
sysctl net.ipv4.tcp_congestion_control

# Запусти download большого файла, замерь throughput:
time curl -o /dev/null -s https://speed.cloudflare.com/__down?bytes=100000000

# Переключи на BBR:
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
# Запусти заново:
time curl -o /dev/null -s https://speed.cloudflare.com/__down?bytes=100000000

# На сетях с потерями BBR обычно быстрее. На идеальном канале -- разницы может не быть

# 4. Симулируй потери и посмотри, как cwnd падает:
sudo tc qdisc add dev lo root netem loss 1%
# Запусти TCP-трансфер, мониторь:
watch -n 0.5 'ss -ti | grep ESTAB'
# cwnd будет периодически расти и падать -- это AIMD
sudo tc qdisc del dev lo root

Проверка знанийKnowledge check
Junior спрашивает: 'Я перекачиваю большие файлы между датацентрами через TCP. На быстром link, RTT ~80 мс. Я получаю около 20 Mbps, хотя канал по тестам 1 Gbps. Что не так и как чинить?'
ОтветAnswer
Это классический случай 'TCP не использует весь канал на long fat network' (LFN). Несколько возможных причин и проверок: 1. Buffer size. Bandwidth-Delay Product = 1 Gbps * 0.08 sec = 10 MB. Это сколько данных должно быть в полёте. Если твой receive buffer < 10 MB, TCP не может насытить канал. Проверь: ss -ti dst <peer> # rcv_space -- это эффективное окно. Если < 1 MB на gigabit/80ms -- проблема Увеличь: sudo sysctl -w net.ipv4.tcp_rmem='4096 87380 16777216' sudo sysctl -w net.ipv4.tcp_wmem='4096 65536 16777216' sudo sysctl -w net.core.rmem_max=16777216 sudo sysctl -w net.core.wmem_max=16777216 2. Window Scaling. Без неё TCP-окно ограничено 64 KB. На gigabit/80ms это даёт 6.4 Mbps. Захвати handshake: sudo tcpdump -i any -n -v 'tcp port <port>' -c 4 # Должна быть [wscale N] опция в SYN. Если нет -- что-то её срезает (firewall, NAT) 3. Congestion control algorithm. На LFN классический Cubic может проседать при любых потерях. Попробуй BBR: sudo sysctl -w net.ipv4.tcp_congestion_control=bbr BBR не использует loss как сигнал congestion -- он измеряет реальный bandwidth и держит cwnd на BDP. На LFN это часто дает 2-10x улучшение. 4. Random packet loss. На пути могут быть потери (даже 0.1%) из-за электроники, носимых сетей, или промежуточного оборудования. Каждая потеря даёт Cubic залп вниз. Замерь: ping -c 100 <peer> # Смотри loss rate 5. Параллельные соединения. Если один поток TCP насыщает 20 Mbps, а канал 1 Gbps, может быть, проблема не в TCP-стеке, а в bottleneck где-то ещё (CPU, диск, конкретный server-side limit). Попробуй несколько параллельных: parallel -j 8 'curl -o /tmp/part_{} -s ...' ::: 1 2 3 4 5 6 7 8 Если суммарный throughput пропорционально растёт -- это limit per-flow, не общий. 6. Disk I/O. Если ты пишешь скачиваемое на диск, и диск медленный, recv ждёт disk. Лечится async writes или RAM tmpfs для теста. 7. CPU on either side. На gigabit-скорости TCP CPU non-trivial. Проверь top на обоих сторонах. Мой алгоритм действий в такой ситуации: # (1) iperf3 -c <server> -t 30 -P 4 # Если 4 потока дают gigabit -- проблема per-flow (нужны бOльшие буферы / BBR) # Если все 4 вместе дают 20 Mbps -- проблема канала, не TCP # (2) Замерь BDP: bandwidth * RTT, и сравни с buffer # (3) Переключи на BBR, перезамерь # (4) Если ничего не помогает -- захвати трафик и смотри в Wireshark # Stats > TCP Stream Graph > Window Scaling Graph В 80% случаев проблема -- buffer size + congestion control. В 15% -- невидимая потеря пакетов. В 5% -- что-то более экзотическое.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Зачем TCP нужен congestion control (в дополнение к flow control)?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5