MAATRIX / Блог / Как TCP угадывает ширину канала: BBR против CUBIC и почему новее не значит лучше

Как TCP угадывает ширину канала: BBR против CUBIC и почему новее не значит лучше

MAATRIX

TCP не знает заранее, сколько данных можно безопасно держать в полёте: ширина канала, число конкурентов на нём и глубина буферов по дороге — величины, которые протокол не может ни у кого спросить напрямую. Их приходится угадывать по косвенным признакам, и именно этим занимается алгоритм congestion control — окно перегрузки (cwnd), которое отправитель наращивает и урезает сам, без явного согласия сети. CUBIC и BBR решают эту задачу принципиально по-разному, и разница между ними — не про то, какой алгоритм «новее» и поэтому лучше, а про то, какую информацию каждый из них готов использовать и чем за это платит.

Реактивный подход: как CUBIC читает петлю обратной связи

CUBIC — прямой потомок TCP Reno и семейства алгоритмов, построенных на одной и той же идее: жать на газ, пока не станет ясно, что канал переполнен, и единственный надёжный сигнал переполнения — потеря пакета. Пока потерь нет, cwnd растёт; как только пакет потерян (детектировано через дубликаты ACK или таймаут), CUBIC интерпретирует это как «канал захлебнулся» и резко снижает окно, а затем начинает расти заново.

До того как заработает собственно CUBIC-функция, соединение проходит через фазу разгона с нуля — slow start, отдельно разобранную в статье TCP slow start: почему соединение разгоняется постепенно. Здесь мы смотрим на то, что происходит уже после выхода из этой фазы, — на собственно алгоритм congestion avoidance, которым и различаются CUBIC и BBR.

Форма роста окна у CUBIC — не линейная, а кубическая функция времени, отсюда и название. После снижения окна из-за потери CUBIC запоминает точку, в которой это произошло (W_max), и растит cwnd по кубической кривой, которая сначала быстро приближается к W_max, затем почти останавливается рядом с ним (алгоритм «осторожничает» там, где в прошлый раз случилась потеря), а затем, если новых потерь не происходит, снова ускоряется и уходит выше W_max в поисках нового предела. Смысл конструкции — не зависеть от RTT так жёстко, как зависели более старые алгоритмы: рост окна CUBIC определяется в первую очередь временем с момента последнего события потери, а не числом полученных ACK за RTT.

Ключевая черта этого подхода — он постфактум. CUBIC не пытается узнать реальную ширину канала или задержку до того, как её займут; он выясняет предел методом «жать, пока не потечёт», и потеря пакета здесь не побочный эффект, а обязательная часть механизма обучения. Без единой потери CUBIC никогда не узнает, где потолок, и будет продолжать наращивать окно. Это простая и проверенная временем стратегия, но у неё есть врождённая слабость — она путает два разных типа сигнала.

Проблема, которую CUBIC не умеет решить: не всякая потеря — от переполнения

Потеря пакета в реальной сети бывает вызвана как минимум двумя разными причинами, и с точки зрения CUBIC они неразличимы. Первая — переполнение буфера на промежуточном узле: слишком много данных пытаются пройти через слишком узкое место, буфер переполняется, пакеты сбрасываются. Здесь снижение окна — правильная реакция. Вторая — случайная потеря на физическом или беспроводном участке линии, никак не связанная с перегрузкой: канал вовсе не был занят под завязку, просто конкретный пакет потерялся из-за помехи, кратковременного сбоя на оборудовании или шума в среде передачи. Как отличить один тип потерь от другого на практике — отдельная тема, разобранная в статье потеря пакетов, которой не видно в ping.

CUBIC реагирует на оба случая одинаково — снижением окна. На маршрутах, где случайные потери, не связанные с перегрузкой, происходят регулярно (спутниковые каналы, нестабильные беспроводные участки, некоторые дальние межконтинентальные маршруты с шумными промежуточными сегментами), это означает, что CUBIC систематически недоиспользует канал: он снижает скорость там, где мог бы продолжать разгоняться, просто потому что не способен отличить «сеть перегружена» от «один пакет не повезло». Именно эта слабость реактивного подхода и стала главным поводом для поиска альтернативы.

Ещё одна связанная проблема реактивных алгоритмов — они по конструкции склонны наполнять буферы промежуточных узлов до отказа перед тем, как заметить потерю (это явление известно как bufferbloat). Пока буфер не переполнился, потери нет, а значит нет и сигнала притормозить — а значит окно продолжает расти, раздувая задержку в очереди буфера для всех, кто делит с вами этот канал, задолго до фактической потери пакета.

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

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

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

Проактивный подход: что на самом деле пытается измерить BBR

BBR (Bottleneck Bandwidth and Round-trip propagation time) исходит из другой предпосылки: вместо того чтобы ждать потери как единственного источника истины, можно попытаться напрямую оценить два физических параметра канала — реальную пропускную способность узкого места на маршруте (bottleneck bandwidth) и минимальную задержку распространения без очередей в буферах (round-trip propagation time). Если оба значения известны хотя бы приблизительно, отправитель может рассчитать, сколько данных стоит держать в полёте, не дожидаясь, пока где-то на пути переполнится буфер.

Практически BBR работает в циклах, постоянно уточняя обе оценки. Он периодически ненадолго увеличивает темп отправки, чтобы проверить, не появилась ли на маршруте дополнительная пропускная способность (условно говоря — фаза зондирования канала), и отдельно, реже, ненадолго снижает объём данных в полёте почти до минимума, чтобы измерить задержку без влияния собственных же накопленных в очереди пакетов (фаза зондирования задержки) — иначе задержка, которую видит отправитель, будет включать в себя время ожидания в чужом буфере, а не реальное время распространения сигнала. Ключевая идея: BBR старается держать в полёте ровно столько данных, сколько нужно, чтобы заполнить произведение пропускной способности на задержку (BDP), не больше — то есть не пытается специально забить буферы по дороге, чтобы «нащупать» предел через потерю.

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

Сильные и слабые стороны каждого подхода

Ни один из двух алгоритмов не является объективно лучшим — они оптимизированы под разные условия и платят за свои сильные стороны разными компромиссами.

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

BBR. Сильная сторона — способность агрессивнее и стабильнее использовать канал именно там, где CUBIC зря тормозит: на длинных высокоскоростных маршрутах со случайными потерями, а также в целом меньшая склонность раздувать очереди в буферах промежуточных узлов, потому что цель BBR — не забить буфер, а держать в полёте ровно BDP. Слабая сторона — сложность модели: оценка пропускной способности и задержки строится на косвенных измерениях, которые сами по себе шумные, и в фазах зондирования канала BBR может кратковременно вести себя агрессивнее, чем ожидает классическая логика congestion avoidance. И главное — вопрос честной конкуренции с другими потоками на общем канале, о котором отдельно ниже.

Почему BBR исторически конфликтовал с CUBIC-потоками на общем канале

Это, пожалуй, самый важный практический нюанс, из-за которого выбор алгоритма не сводится к «поставить более новый и не думать». Классические реактивные алгоритмы вроде CUBIC исходно рассчитаны на сосуществование друг с другом по общей логике: когда буфер общего узкого места переполняется, потерю получают более-менее все конкурирующие потоки, каждый снижает своё окно, и в итоге устанавливается какое-то относительно справедливое разделение канала между всеми участниками — это заложено в саму конструкцию AIMD-семейства алгоритмов.

BBR по своей природе слабее реагирует именно на факт потери — он ориентируется на собственную оценку BDP, а не на сигнал переполнения буфера как таковой. Когда BBR-поток делит один и тот же узкий канал с CUBIC-потоками, возникает асимметрия: CUBIC честно снижает окно при потере (которую вызывает переполнение общего буфера), а BBR, ориентируясь на собственную модель канала, может не снижать темп отправки в той же степени — и в результате забирает себе непропорционально большую долю общей пропускной способности за счёт соседних CUBIC-потоков. Это не гипотетическая проблема, а один из главных предметов публичной критики BBR первых ревизий и одна из причин, почему более поздние доработки алгоритма были в первую очередь направлены на улучшение честности сосуществования с классическими реактивными потоками на общем узком месте — конкретные версии и то, насколько полно проблема решена в каждой из них, стоит проверять по актуальной документации ядра, которую вы используете, а не считать вопрос закрытым раз и навсегда.

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

Как выбрать алгоритм под свою нагрузку, а не под моду

«Новее — не значит лучше» здесь буквально означает: не переключайтесь на BBR только потому, что это модно или потому что кто-то в интернете написал про прирост скорости на своём маршруте. Выбор алгоритма стоит делать, отталкиваясь от характера вашей конкретной нагрузки и маршрута.

Проверить текущий алгоритм и список доступных на сервере:

sysctl net.ipv4.tcp_congestion_control
sysctl net.ipv4.tcp_available_congestion_control

Переключить на BBR (модуль ядра должен присутствовать; на современных ядрах Linux он обычно уже собран):

modprobe tcp_bbr
sysctl -w net.core.default_qdisc=fq
sysctl -w net.ipv4.tcp_congestion_control=bbr

Вернуться на CUBIC так же просто:

sysctl -w net.ipv4.tcp_congestion_control=cubic

Обратите внимание на связку с fq (fair queueing) как дисциплиной очереди — BBR исходно проектировался и тестировался в паре именно с ней, и на некоторых конфигурациях без fq поведение BBR может отличаться от ожидаемого, так что менять default_qdisc — не опциональный шаг, а часть корректной настройки.

Ориентиры, когда какой алгоритм разумнее рассматривать:

СитуацияЧто учитывать
Длинный маршрут (межконтинентальный, спутниковый) со случайными потерями без явной перегрузкиBBR потенциально не теряет скорость зря там, где CUBIC снижает окно из-за потерь, не связанных с перегрузкой
Сервер делит узкое место с большим числом чужих CUBIC-потоков (общий аплинк, переполненный канал провайдера)Стоит проверить фактическую честность разделения канала на своей нагрузке, а не считать BBR однозначно выигрышным
Интерактивная нагрузка, чувствительная к задержке под нагрузкой (голос, игры, интерактивные сессии)Меньшая склонность BBR раздувать буферы (bufferbloat) может быть заметным плюсом
Стабильный внутренний канал дата-центра без внешней конкуренции за пропускную способностьРазница между CUBIC и BBR в этом сценарии, как правило, куда менее выражена, чем на длинных зашумлённых маршрутах
Нет возможности протестировать оба варианта на реальном трафикеРазумный дефолт — оставить проверенный CUBIC, а не менять алгоритм наугад ради гипотетического прироста

Главное практическое правило: не переключайте алгоритм на продакшене вслепую. Проверьте его на тестовом сервере под нагрузкой, похожей на вашу реальную (не синтетический iperf3 в вакууме), и сравните не только среднюю скорость, но и задержку со стабильностью. Разница сильно зависит от маршрута, характера потерь и того, с кем вы делите канал — универсальных цифр прироста здесь честно не существует.

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

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

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

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

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

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

BBR всегда быстрее CUBIC?

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

Можно ли просто везде поставить BBR и забыть про CUBIC?

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

Почему после включения BBR не изменилась заметно скорость?

Скорее всего, потолок вашего конкретного соединения определяется не congestion control алгоритмом, а чем-то другим — например, размером TCP-окна и RTT (об этом отдельная статья про TCP-окно и почему канал через океан тормозит), либо реальным физическим ограничением канала, а не выбором алгоритма перегрузки. Congestion control решает, сколько данных безопасно держать в полёте, но не увеличивает ни ширину канала, ни размер окна приёма сам по себе.

Нужно ли включать fq отдельно, если я меняю алгоритм на BBR?

Да, это рекомендуемая связка: BBR проектировался с расчётом на fair queueing как дисциплину очереди пакетов на исходящем интерфейсе, и без неё поведение может отличаться от заявленного в документации алгоритма. Строка sysctl -w net.core.default_qdisc=fq — не опциональный шаг, а часть корректной настройки BBR.

Как понять, что именно congestion control алгоритм — причина проблем со скоростью, а не что-то другое?

Сравните поведение на одном и том же маршруте с разными алгоритмами при прочих равных, глядя не только на среднюю скорость, но и на паттерн потерь и рост задержки под нагрузкой. Если картина резко меняется от переключения алгоритма — вы на верном пути; если нет — вероятно, дело в другом узком месте, и стоит проверить окно приёма, MTU или сам канал, прежде чем считать congestion control виновником.

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

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

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