Почему потеря одного пакета из тысячи роняет скорость вдвое
Вы гоняете iperf между двумя серверами, канал по паспорту гигабитный, ping показывает вменяемые цифры — а реальная скорость передачи упорно застревает на трети или четверти от заявленной. Ни загрузки CPU, ни насыщенного канала, ни видимых проблем. Причина почти всегда одна: где-то на пути теряется небольшой процент пакетов, и TCP реагирует на это не пропорционально, а по правилам, которые изначально писались так, чтобы душить скорость сильно и сразу. Разберём, почему так устроен congestion control и какая математика стоит за этим эффектом.
Содержание
- Что на самом деле происходит, когда пакет теряется
- Почему реакция на потерю — это не плавное торможение, а удар по тормозам
- Медленное восстановление: почему скорость не отскакивает обратно
- Математика на пальцах: почему длинные быстрые каналы страдают сильнее всего
- Почему это касается вас, даже если вы не администрируете магистральные сети
- Что делать на практике: стабильность важнее пиковой скорости
Что на самом деле происходит, когда пакет теряется
TCP — надёжный протокол: если пакет не дошёл, получатель об этом узнаёт и данные передаются повторно. Само по себе это не страшно — ретрансмит одного сегмента почти не заметен на фоне остального трафика. Проблема не в повторной отправке как таковой, а в том, что отправитель делает *дальше*.
У TCP есть два независимых механизма, которые следят за состоянием соединения:
- Управление окном (flow control) — сколько данных получатель готов принять, чтобы не переполнить свой буфер.
- Управление перегрузкой (congestion control) — сколько данных отправитель может выпустить в сеть, не зная заранее, насколько она загружена.
Второй механизм — источник всей боли. У отправителя нет прямого способа узнать состояние очередей на промежуточных маршрутизаторах. Единственный сигнал, который у него есть — это то, дошли пакеты или нет и с какой задержкой пришло подтверждение (ACK). Поэтому классические алгоритмы TCP приняли простое, но грубое допущение: потеря пакета = перегрузка сети. Если пакет потерян, значит, где-то на пути буфер маршрутизатора переполнился и начал отбрасывать пакеты (tail drop) — сеть перегружена, надо снижать темп. Подробнее о том, как такая потеря вообще обнаруживается и почему она не всегда видна в обычном ping, можно посмотреть в разборе диагностики потери пакетов.
Это допущение было разумным в девяностых, когда почти вся потеря в проводных сетях действительно была следствием переполненных очередей. Сегодня пакет может потеряться и по другим причинам — микроскопическая ошибка на радиоинтерфейсе Wi-Fi, кратковременный сбой на пограничном роутере, агрессивная политика QoS у провайдера. Но TCP по-прежнему не умеет отличать «сеть перегружена» от «просто не повезло с одним кадром» — и реагирует одинаково жёстко в обоих случаях.
Почему реакция на потерю — это не плавное торможение, а удар по тормозам
У каждого TCP-соединения есть переменная cwnd (congestion window) — это, грубо говоря, количество данных, которое отправитель может выпустить в сеть, не дожидаясь подтверждения. Чем больше cwnd, тем выше потенциальная скорость передачи. В обычном режиме (congestion avoidance) окно растёт линейно: получили подтверждение на весь текущий объём данных без потерь — увеличиваем окно на небольшую фиксированную величину за каждый RTT (round-trip time, время полного оборота пакета туда-обратно). Это называется additive increase — аккуратный, постепенный разгон.
А вот реакция на потерю устроена иначе. В классических алгоритмах семейства TCP Reno и CUBIC (CUBIC — алгоритм, который де-факто много лет является congestion control по умолчанию в Linux) при обнаружении потери окно не подрезается на процент от текущей загрузки сети, а режется мультипликативно относительно самого себя — типичное поведение классических реализаций - уменьшение примерно вдвое от текущего размера окна. Это называется multiplicative decrease, и вместе с additive increase оно образует алгоритм AIMD (additive increase / multiplicative decrease) — асимметричную схему, которая по конструкции наращивает скорость медленно, а обрезает быстро.
Логика авторов алгоритма понятна: если реагировать на потерю слабо, а сеть действительно перегружена, все участники будут наращивать трафик одновременно и очередь на узком месте продолжит расти до коллапса — так называемого congestion collapse, который в ранних версиях интернета случался всерьёз. Резкое торможение при первом же признаке перегрузки — это защитный механизм для сети в целом, а не оптимизация для конкретного вашего соединения. TCP жертвует вашей скоростью ради стабильности сети как общего ресурса.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверМедленное восстановление: почему скорость не отскакивает обратно
Проблема не только в том, что окно режется резко — а в том, что обратно оно растёт медленно. После срабатывания multiplicative decrease соединение возвращается в режим congestion avoidance и начинает восстанавливать окно по одной единице MSS (maximum segment size) за примерно каждый цикл RTT. Если RTT между вашим сервером и клиентом — условно 150 мс (типично для трансатлантического или трансконтинентального маршрута), возврат от урезанного окна к прежнему размеру может занимать заметное количество полных оборотов — то есть секунды или десятки секунд стабильной передачи без единой новой потери.
Если же потеря обнаружена не через быстрые повторные ACK (fast retransmit), а через таймаут ретрансмита (RTO — retransmission timeout, когда подтверждение вообще не пришло в разумный срок), происходит вещь ещё жёстче: окно перегрузки сбрасывается почти до минимального стартового значения, и соединение заново проходит через slow start — фазу экспоненциального разгона с нуля. Механику самого разгона и почему он не мгновенный, разобрано в статье про TCP slow start. На практике это значит, что одна плохо обработанная потеря может отбросить производительность соединения к состоянию «только что установили связь» — даже если проблема в сети длилась долю секунды.
Именно эта асимметрия — быстрый удар вниз, медленный подъём вверх — и есть главный механизм, из-за которого небольшой процент потерь ощущается несоразмерно своей доле.
Математика на пальцах: почему длинные быстрые каналы страдают сильнее всего
Есть приблизительное соотношение, которое инженеры сети используют, чтобы прикинуть порядок влияния потерь на throughput классического TCP (оно известно как формула Мэтиса и её более поздние уточнения). Точную форму формулы приводить не будем — она не даёт конкретных цифр без знания реальных параметров сети, а нам важна не формула сама по себе, а зависимость, которую она описывает:
Пропускная способность TCP-соединения обратно пропорциональна не самой доле потерь, а примерно квадратному корню из неё, и прямо зависит от размера сегмента и обратно — от RTT.
Что это значит на практике, без формул:
- Если доля потерянных пакетов уменьшается в четыре раза, ожидаемая пропускная способность растёт не в четыре раза, а примерно вдвое (корень из четырёх). Обратная сторона той же зависимости: чтобы просадка скорости от потерь казалась «терпимой», саму потерю нужно снижать далеко не пропорционально — небольшое увеличение процента потерь бьёт по throughput куда сильнее, чем кажется на первый взгляд.
- Чем больше RTT, тем ниже потенциальная пропускная способность при той же доле потерь. Соединение с маленькой задержкой (сервер и клиент в одном городе) восстанавливает окно после потери за доли секунды и почти не замечает редкие потери. То же соединение через океан с RTT в разы больше восстанавливается кратно дольше — окно банально не успевает разогнаться обратно между одной потерей и следующей.
- Произведение полосы пропускания канала на RTT называется bandwidth-delay product (BDP) — это объём данных, который физически «летит в проводе» в любой момент времени. Каналы с большим BDP (быстрые и одновременно длинные — условно, гигабитный канал между дата-центрами в разных регионах) принято называть long fat networks. Именно они страдают от потерь непропорционально сильно: чтобы утилизировать такой канал, нужно очень большое окно, а любая потеря режет это большое окно пополам и заставляет долго его восстанавливать. Подробнее о том, как размер окна связан со скоростью на длинных каналах, разобрано в статье про TCP-окно и почему канал через океан тормозит.
Отсюда практический вывод, который часто удивляет: для соединения сервер-клиент в пределах одного города 1-2% потерь может быть почти незаметны, а для трансконтинентального канала между вашим сервером и удалённым дата-центром та же доля потерь способна срезать пропускную способность в разы. Дело не в качестве канала как такового, а в том, что длинные быстрые соединения физически более чувствительны к самому механизму AIMD.
Почему это касается вас, даже если вы не администрируете магистральные сети
Эффект актуален далеко не только для провайдеров. Он напрямую влияет на:
- Скорость бэкапов и репликации между дата-центрами. Если вы синхронизируете базу данных или гоняете rsync/бэкапы между серверами в разных локациях (например, между RU и UK), даже небольшая нестабильность на промежуточном участке маршрута резко снижает достижимую скорость — при том что «средний» ping и traceroute могут выглядеть нормально, потому что потери часто не линейны во времени, а происходят пачками.
- Скорость отдачи большим файлов через CDN или напрямую. Один TCP-поток на клиента, который проходит через нестабильный участок сети (домашний Wi-Fi клиента, перегруженный upstream провайдера), будет казаться «медленным сервером», хотя сервер тут ни при чём — проблема в реакции TCP на потери где-то по дороге.
- VPN и туннели. Инкапсуляция трафика в VPN сама по себе не создаёт потерь, но если потери есть на внешнем канале, туннелированный TCP-трафик получает двойной удар: страдает и внешнее TCP-соединение туннеля (если используется TCP-based VPN), и внутренние TCP-сессии клиента.
- API и микросервисы с долгоживущими соединениями между регионами. Если ваш бэкенд в одном регионе стучится в базу или в другой сервис в другом регионе через долгий TCP-канал, редкие потери на пути дают не линейное, а скачкообразное замедление ответов — именно потому что congestion window периодически обнуляется и разгоняется заново.
Что делать на практике: стабильность важнее пиковой скорости
Из всего разобранного следует практический вывод, который легко упустить: при выборе сервера и сети для задач с большим объёмом трафика между регионами стабильность канала важнее заявленной пропускной способности. Канал на 1 Гбит/с с редкими, но регулярными микропотерями на практике часто отдаёт меньше полезной скорости, чем более скромный по паспорту канал без потерь вообще — именно из-за механики AIMD, описанной выше.
Что можно сделать реально:
- Проверять потери отдельно от задержки. Обычный
pingс редкими запросами легко пропускает короткие пачки потерь (microbursts). Для честной картины используйтеmtrв режиме отчёта на протяжении длительного времени илиiperf3с параллельными потоками, чтобы увидеть реальную деградацию throughput, а не только среднюю задержку. - Учитывать физическую близость сервера к аудитории. Меньший RTT — меньшая чувствительность к потерям в силу той самой математики выше. Если основная аудитория в Европе, сервер в европейской локации при прочих равных будет устойчивее к сетевым шероховатостям, чем тот же по характеристикам сервер за океаном.
- Не полагаться на один TCP-поток там, где это возможно. Несколько параллельных TCP-соединений (или UDP-based протоколы вроде QUIC, где логика перегрузки устроена иначе) частично сглаживают эффект — потеря режет одно окно, а не всю передачу целиком. У этого подхода есть своя цена в виде накладных расходов протокола — сравнение честно разобрано в статье почему UDP быстрее TCP и чем за это платите.
- Настраивать таймауты и retry с запасом на длинных маршрутах. Если приложение агрессивно рвёт соединение при малейшей задержке ответа, оно будет постоянно попадать в slow start вместо того, чтобы дать TCP восстановить окно естественным образом.
- Мониторить сеть между своими же серверами, а не только внешние каналы. Внутренние потери между вашими собственными нодами (репликация, очереди сообщений, служебный трафик) подвержены той же математике — просто их обычно не проверяют, потому что «это же наша внутренняя сеть, там всё быстро».
Ни один из этих пунктов не отменяет саму механику TCP — вы не можете заставить протокол реагировать на потерю мягче без замены алгоритма congestion control целиком (это отдельная и большая тема). Но понимание того, *почему* небольшой процент потерь даёт непропорциональный удар по скорости, меняет то, на что стоит смотреть при диагностике: не только на «сколько мегабит в канале», но и на стабильность этого канала на длинном промежутке времени.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Если потерь всего доли процента, зачем вообще о них беспокоиться?
Потому что реакция TCP на потерю нелинейна: окно перегрузки режется мультипликативно, а восстанавливается медленно и линейно. Даже редкие потери, распределённые по времени, не дают окну разогнаться до максимума — соединение постоянно находится в состоянии «немного после последнего удара», а не на пике возможной скорости.
Можно ли увидеть эту проблему в обычном мониторинге аптайма или ping?
Обычно нет. Ping раз в несколько секунд легко пропускает короткие пачки потерь, а мониторинг аптайма вообще не смотрит на throughput. Нужны инструменты, которые считают именно потери за интервал (mtr, отчёты iperf) и смотрят на реальную скорость передачи, а не только на факт доступности хоста.
Помогает ли просто увеличить размер TCP-окна (window scaling)?
Частично и не всегда. Большое окно позволяет утилизировать канал с большим BDP при отсутствии потерь, но при потере это же большое окно режется пропорционально своему размеру — то есть падение в абсолютных цифрах будет ещё заметнее, просто относительная доля просадки останется той же.
Отличается ли поведение современных алгоритмов вроде BBR от классического CUBIC?
Да, и заметно — BBR не полагается на потерю пакета как единственный сигнал перегрузки, а оценивает пропускную способность и задержку напрямую. Это отдельная и довольно объёмная тема, которую стоит разбирать отдельно, а не мимоходом в контексте классического AIMD.
Если у меня сервер и клиенты в одном городе, стоит ли вообще думать об этом эффекте?
Стоит держать в голове, но эффект будет заметно слабее — при маленьком RTT окно восстанавливается после потери гораздо быстрее, а BDP канала невелик даже на высокой скорости. Основная боль от этого механизма проявляется именно на длинных и одновременно быстрых каналах.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →