MAATRIX / Блог / Потеря 0,3% пакетов срезала скорость вдвое: как TCP реагирует на редкие потери

Потеря 0,3% пакетов срезала скорость вдвое: как TCP реагирует на редкие потери

MAATRIX

Вы смотрите в mtr и видите на одном из хопов потери в 0,3% — цифра, на которую в обычной жизни никто бы не обратил внимания. А передача файла между серверами при этом идёт вдвое медленнее, чем ожидалось, хотя канал по паспорту не загружен и CPU скучает. Разница между «потери есть, но это же меньше процента» и «скорость упала вдвое» — не совпадение и не редкая аномалия, а прямое следствие того, как устроен congestion control в TCP. Разберём механизм по шагам и покажем, как воспроизвести эффект самому, а не верить на слово.

Что TCP видит, когда пакет не доходит

TCP ничего не знает о реальном состоянии сети между отправителем и получателем. У него нет доступа к очередям на промежуточных маршрутизаторах, нет телеметрии о загрузке каналов провайдеров — есть только один наблюдаемый факт: дошёл ли пакет и с какой задержкой пришло подтверждение (ACK). Из этого скудного набора сигналов классические реализации TCP (Reno и его развитие CUBIC, которое много лет является алгоритмом по умолчанию в Linux) делают жёсткий вывод: если пакет потерян, значит где-то на пути буфер маршрутизатора переполнился и начал отбрасывать пакеты — сеть перегружена.

Это допущение родилось в девяностых, когда почти вся потеря в проводных сетях действительно была следствием переполненных очередей. Сегодня причины другие: микроскопическая ошибка на радиоинтерфейсе Wi-Fi, короткая просадка на пограничном роутере, агрессивный шейпинг у провайдера, переполненный буфер сетевой карты сервера. Но классический TCP по-прежнему не различает «сеть реально перегружена» и «просто не повезло с одним кадром» — реагирует одинаково жёстко в обоих случаях. Именно поэтому 0,3% потерь, взявшиеся из совершенно безобидной причины, запускают ту же самую тормозящую машинерию, что и настоящий затор.

Congestion window: удар вниз, медленный подъём

У каждого TCP-соединения есть переменная cwnd (congestion window) — объём данных, который отправитель может выпустить в сеть, не дожидаясь подтверждения. Чем больше cwnd, тем выше потенциальная скорость. В штатном режиме (congestion avoidance) окно растёт линейно: раз в RTT прибавляется небольшая фиксированная величина — это additive increase, аккуратный постепенный разгон.

Реакция на потерю устроена принципиально иначе. При обнаружении потери классические алгоритмы режут cwnd не пропорционально степени перегрузки, а мультипликативно относительно текущего размера — типичная реакция Reno/CUBIC на потерю, зафиксированную через быстрые повторные ACK (fast retransmit), это уменьшение окна примерно вдвое от текущего значения. Вместе с additive increase это образует классическую схему AIMD (additive increase / multiplicative decrease) — специально асимметричную: разгон медленный, торможение резкое.

Логика понятна: если бы TCP реагировал на потерю слабо, а сеть действительно была перегружена, все участники продолжили бы наращивать трафик и очередь на узком месте росла бы до полного коллапса. Резкое торможение по первому же сигналу — это защита сети как общего ресурса, а не забота о скорости конкретно вашего соединения.

Есть и более жёсткий сценарий. Если потеря обнаружена не через fast retransmit, а через таймаут (RTO — retransmission timeout, когда подтверждение вообще не пришло вовремя), cwnd сбрасывается почти до стартового минимума, и соединение заново проходит slow start — фазу экспоненциального разгона с нуля. Одна плохо обработанная потеря способна отбросить производительность соединения к состоянию «только что установили связь», даже если сбой в сети длился доли секунды.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Почему RTT решает, насколько больно

Асимметрия AIMD — не единственная причина, почему небольшой процент потерь так дорого обходится. Вторая часть механизма — то, сколько времени занимает восстановление cwnd после удара. Окно растёт примерно на одну единицу MSS (maximum segment size) за один цикл RTT. Если RTT маленький, окно откатывается назад буквально за доли секунды и почти не мешает средней скорости. Если RTT большой, тот же откат занимает кратно больше реального времени, а на длинном интервале потери успевают повториться раньше, чем окно дорастёт до прежнего размера.

Здесь и проявляется качественная зависимость, которую сетевые инженеры формулируют по-разному, но суть у неё одна и восходит к так называемой формуле Мэтиса: пропускная способность TCP-потока растёт с уменьшением доли потерь не линейно, а гораздо медленнее — примерно как корень из этой доли, — и падает при увеличении RTT. Точных коэффициентов приводить не будем: они зависят от MSS, конкретной реализации congestion control и профиля трафика, и на реальном канале почти всегда будут иными, чем в учебном примере. Но направление зависимости устойчиво в любой реализации классического TCP:

СценарийТипичный RTTКак ведёт себя канал при потере 0,3%
Сервер и клиент в одном городеединицы мсcwnd восстанавливается почти мгновенно, просадка скорости почти не ощущается
Сервер в соседнем регионе (например RU—EU)пара десятков — полсотни мсвосстановление заметно на глаз, но канал успевает разогнаться между потерями
Межконтинентальный канал (например RU—US, EU—Asia)сотни мсокно не успевает вырасти обратно до следующей потери, средняя скорость держится далеко ниже пиковой

Цифры RTT в таблице — ориентировочные диапазоны, а не гарантия для конкретного маршрута: реальная задержка зависит от пиринга и транзита между сетями, а не только от географического расстояния. Важен сам принцип: канал с большой полосой и большим RTT одновременно — так называемая long fat network — физически более уязвим к любой потере, потому что для утилизации широкого канала нужно большое окно, а любая потеря режет именно его, и режет пропорционально размеру. Подробный разбор связи размера окна, RTT и bandwidth-delay product — в статье про TCP-окно и почему канал через океан тормозит, а более развёрнутая математика самого эффекта AIMD — в статье почему потеря одного пакета из тысячи роняет скорость вдвое.

Практический вывод: одна и та же доля потерь — например те самые 0,3% из начала статьи — почти незаметна для двух серверов в одном дата-центре и способна срезать пропускную способность в разы для канала между вашим сервером в Европе и клиентом или репликой на другом континенте.

BBR: другая философия congestion control

Классический AIMD — не единственный способ управлять перегрузкой. Алгоритм BBR (Bottleneck Bandwidth and Round-trip propagation time), разработанный в Google и доступный в ядре Linux начиная примерно с версии 4.9, устроен принципиально иначе: он не ждёт потери пакета как сигнала «пора тормозить», а пытается напрямую оценить два параметра канала — пропускную способность узкого места (bottleneck bandwidth) и минимальную задержку без очередей (RTprop). Для этого BBR периодически замеряет, как меняется темп доставки подтверждений, и подстраивает темп отправки под эту оценку, а не под факт потери.

Разница в поведении при потере принципиальна. Для CUBIC любая потеря — это команда «режь окно вдвое», даже если потеря случайная и никакой реальной перегрузки нет. BBR в такой ситуации скорее спросит себя «а изменилась ли реально измеренная пропускная способность канала», и если нет — не станет так резко обваливать темп отправки. Именно поэтому BBR обычно заметно устойчивее к редким случайным потерям (радиоинтерфейсы, флап на одном из промежуточных узлов) и особенно выигрывает на тех самых long fat networks — длинных быстрых каналах, где классический AIMD страдает сильнее всего.

Это не значит, что BBR однозначно лучше и его нужно включать везде не глядя. У него есть свои честные ограничения: ранние версии алгоритма (BBRv1) в некоторых сценариях вели себя менее справедливо к параллельным потокам CUBIC на общем узком месте, могли держать в очередях промежуточных буферов больше данных, чем классические алгоритмы. Более новые ревизии (BBRv2 и далее) заметно дорабатывают эту часть, но поведение всё равно зависит от версии ядра и конкретной реализации — сравнивать философии стоит отдельно и вдумчиво, это разобрано в статье BBR против CUBIC. Практический момент: включение BBR — это настройка конкретного сервера (sysctl net.ipv4.tcp_congestion_control=bbr при наличии модуля в ядре), она не отменяет саму физику канала и не гарантирует конкретный прирост — эффект стоит измерять на своей нагрузке, а не переносить чужие цифры.

Как найти потери в реальном канале: mtr

Прежде чем что-то чинить, потери нужно увидеть — а обычный ping с редкими запросами регулярно пропускает короткие пачки потерь (microbursts), которые длятся секунды, но успевают ударить по нескольким TCP-потокам. Практичнее использовать mtr, который совмещает traceroute и непрерывный пинг по каждому хопу:

mtr -rw -c 200 -i 0.2 target.example.com

Здесь -r — режим отчёта (без интерактивного интерфейса, удобно логировать), -w — широкий вывод с полными именами хостов, -c 200 — двести циклов опроса, -i 0.2 — интервал 200 мс между пакетами, чтобы быстрее набрать статистику. В выводе интересна не только итоговая строка, но и конкретный хоп, на котором начинаются потери: если процент растёт на одном промежуточном узле и остаётся высоким на всех последующих — потери, скорее всего, реальные и происходят именно там; если потери видны только на одном хопе, а дальше пропадают — это часто особенность обработки ICMP на конкретном маршрутизаторе (он режет ICMP низким приоритетом), а не потери реального трафика. Как отличить одно от другого на практике — отдельная тема, разобранная в статье про потери, которых не видно в обычном ping.

Полезно и посмотреть на TCP изнутри уже установленного соединения — команда ss -tin на стороне отправителя показывает по каждому сокету текущее значение cwnd, число ретрансмитов и оценку RTT:

ss -tin state established '( dport = :443 or sport = :443 )'

Если счётчик retrans в выводе стабильно растёт, а cwnd держится на низких значениях относительно ожидаемой полосы канала — это прямое подтверждение того, что именно потери, а не насыщенный канал или загруженный CPU, режут скорость конкретного соединения.

Как воспроизвести потери в лаборатории: iperf3 + tc netem

Самый убедительный способ понять эффект — не читать про него, а увидеть на собственном стенде. Связка iperf3 (для измерения реального throughput) и tc netem (для управляемой эмуляции потерь на сетевом интерфейсе) позволяет воспроизвести именно ту картину, которая обсуждалась выше: как одна и та же малая доля потерь по-разному бьёт по каналам с разным RTT.

На принимающей стороне поднимаем сервер:

iperf3 -s

На отправляющей стороне сначала снимаем базовый throughput без потерь:

iperf3 -c 10.0.0.2 -t 30 -P 4

Флаг -P 4 запускает четыре параллельных TCP-потока — это тоже честный практический приём: несколько параллельных соединений частично сглаживают эффект от потерь, потому что удар приходится на одно окно, а не на всю передачу целиком.

Дальше вносим управляемую потерю на исходящем интерфейсе отправителя:

tc qdisc add dev eth0 root netem loss 0.3%

И повторяем тот же тест iperf3 — разница в throughput между запуском без netem и с ним и будет тем самым эффектом, который в проде выглядит загадочно, а в лаборатории воспроизводится по команде. Чтобы увидеть роль RTT, добавьте эмуляцию задержки тем же инструментом:

tc qdisc change dev eth0 root netem loss 0.3% delay 150ms

Здесь важно использовать именно change, а не повторный addnetem не любит две активные конфигурации на одном интерфейсе одновременно. Сравнив четыре комбинации (без потерь и задержки / только потери / только задержка / потери и задержка вместе), вы своими глазами увидите, что потери при большом RTT срезают throughput сильнее, чем те же потери при маленьком RTT — то есть ту самую качественную зависимость, о которой шла речь выше, без необходимости верить чужим цифрам. После тестов не забудьте вернуть интерфейс в исходное состояние:

tc qdisc del dev eth0 root netem

Такой стенд удобно поднимать между двумя арендованными VPS в разных локациях — если один сервер в Европе, а второй в другом регионе, у вас уже есть реальный, а не эмулированный RTT, и можно добавлять netem loss поверх него, чтобы проверить именно свой сценарий трафика (репликация базы, бэкапы, API между сервисами), а не абстрактный тест.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

0,3% потерь — это вообще много или мало?

Само по себе число маленькое, и именно поэтому его легко списать со счетов. Проблема не в абсолютной величине, а в том, что TCP реагирует на потерю непропорционально: режет окно резко, а восстанавливает медленно. При маленьком RTT эффект действительно почти не заметен, при большом — те же 0,3% способны срезать среднюю скорость в разы.

Если у меня в мониторинге низкая средняя задержка (ping), значит потерь можно не бояться?

Нет. Низкий средний ping и наличие потерь — независимые метрики. Короткие пачки потерь (microbursts) почти никогда не видны в редком ping, но исправно бьют по TCP-потокам, которые в этот момент передают данные. Нужен отдельный, длительный замер потерь — например через mtr с большим числом циклов.

Стоит ли просто включить BBR и забыть про проблему?

BBR действительно устойчивее к случайным потерям, потому что не полагается на них как на единственный сигнал перегрузки. Но это не универсальное решение: поведение зависит от версии реализации, есть нюансы совместимости с параллельными CUBIC-потоками на общем узком месте, и включать его стоит осознанно, измерив эффект на своей нагрузке, а не по умолчанию везде.

Можно ли отличить в tc netem потери из-за перегрузки сети от потерь по другой причине?

Сам netem этого не различает — он лишь эмулирует итоговый эффект «пакет не дошёл» независимо от причины, и это специально: TCP тоже не различает причину, а реагирует одинаково на любую потерю. Отличить реальную причину потерь на боевом канале — отдельная диагностическая задача, которая начинается с mtr и продолжается анализом конкретного участка маршрута.

Помогут ли параллельные TCP-потоки вместо одного, если избавиться от потерь нельзя?

Частично да — несколько параллельных соединений (флаг -P у iperf3, или архитектурно — несколько соединений в приложении) распределяют удар: потеря режет окно одного потока, а не всей передачи. Это не устраняет саму потерю и не решает проблему для задач, которым нужен один непрерывный поток, но заметно сглаживает среднюю деградацию throughput.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →