Материал подготовлен в рамках курса «Сетевой инженер. Продвинутый уровень».

Привет, Хабр!

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

Потому что причина в обратном. Задержка растёт именно из‑за того, что буфера слишком много. Это bufferbloat — раздутые буферы, которые копят пакеты вместо того, чтобы вовремя дать понять отправителю «притормози». Явление контринтуитивное: инженеры десятилетиями увеличивали буферы, считая, что больше памяти под пакеты — всегда лучше, и получили сеть, где под нагрузкой всё тонет в задержке.

Разберём, почему так вышло, как это диагностировать, и что реально помогает — от алгоритмов активного управления очередью до нового L4S, ради которого операторы уже гоняют полевые испытания.

Откуда берётся задержка при свободном канале

Начнём с механики, потому что без неё «слишком много буфера» звучит как бессмыслица.

TCP устроен так, что разгоняется, пока не упрётся. Он наращивает скорость отправки, пока не увидит сигнал перегрузки, а классический сигнал перегрузки — потеря пакета. Пока пакеты доходят, TCP считает, что можно быстрее, и ускоряется.

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

И вот результат: буфер забит тысячами пакетов, каждый новый пакет — ваш пинг, ваш DNS‑запрос, ваш игровой трафик — встаёт в конец этой очереди и ждёт, пока перед ним разгребут всё остальное. Отсюда и полсекунды задержки на свободном по пропускной способности канале. Пакеты не теряются, они стоят в очереди.

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

Почему увеличение буфера делает только хуже

Отсюда сразу видна ловушка, в которую попадали производители оборудования. Память дешевела, буферы наращивали, борясь с потерями пакетов, — и каждое увеличение буфера удлиняло очередь, в которой пакеты ждут.

Получилась вилка.

  • Маленький буфер теряет пакеты на всплесках, но держит низкую задержку.

  • Большой буфер не теряет пакеты, но копит огромную задержку под нагрузкой.

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

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

Активное управление очередью: CoDel

Идея активного управления очередью (AQM) в том, чтобы не ждать переполнения буфера, а вмешиваться раньше — начинать сигналить о перегрузке, пока очередь ещё не разрослась.

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

Прорыв случился, когда стали управлять не длиной очереди, а временем, которое пакет в ней проводит. Это алгоритм CoDel, и его логика элегантна. Он смотрит на минимальную задержку в очереди за скользящее окно. Если пакеты стабильно проводят в очереди дольше целевого значения — обычно порядка пяти миллисекунд — CoDel начинает ронять отдельные пакеты, давая TCP сигнал притормозить. Если задержка ниже цели — не трогает ничего.

Гениальность в том, что CoDel отличает хорошую очередь от плохой. Короткий всплеск трафика создаёт временную очередь, которая тут же рассасывается, — это нормально, её CoDel не трогает. А вот постоянная, стоячая очередь, которая держится и держится, — это bufferbloat, и по ней CoDel бьёт. Он реагирует на устойчивую задержку, а не на всплески, и потому почти не требует настройки: целевые пять миллисекунд работают на канале любой скорости, потому что это время, а не размер.

FQ‑CoDel: очередь на каждый поток

Следующий шаг — совместить CoDel с честным разделением по потокам. Это FQ‑CoDel, и на сегодня это фактический стандарт для домашних роутеров и не только.

Идея простая и мощная. Вместо одной общей очереди трафик раскладывается по множеству очередей — грубо говоря, по одной на поток, — и они обслуживаются по очереди честно, круговым перебором. К каждой применяется CoDel отдельно.

Что это даёт, видно на классическом сценарии. Идёт большая закачка, которая набивает свою очередь, и параллельно — ваш видеозвонок с редкими маленькими пакетами. В общей очереди пакеты звонка встали бы в хвост за закачкой и ждали. В FQ‑CoDel у звонка своя очередь, короткая, и она обслуживается наравне с очередью закачки — маленький поток не страдает от тяжёлого. Тяжёлая закачка сама себе копит задержку в своей очереди, а интерактивный трафик её не разделяет.

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

Для каналов с асимметрией и NAT есть родственник — CAKE, который поверх FQ‑CoDel добавляет ещё и шейпинг полосы, справедливость по хостам, учёт DiffServ и фильтрацию ACK. Для домашнего шлюза, стоящего перед провайдерским каналом, это обычно и есть готовый ответ.

Где именно копится очередь: не только буфер устройства

Прежде чем идти дальше, стоит уточнить, что очередь, дающая задержку, живёт не в одном месте, и это влияет на диагностику.

  • Самый очевидный источник — буфер интерфейса на устройстве, где быстрый трафик упирается в медленный канал. Но есть и менее заметные. У беспроводных сетей своя большая беда: Wi‑Fi агрегирует кадры и имеет собственные очереди на уровне радио, и bufferbloat там возникает даже при формально свободном эфире, потому что радиодоступ делится между устройствами и кадры ждут своего окна передачи. Для Wi‑Fi придумали отдельную дисциплину — учёт эфирного времени, airtime fairness, которая делит не полосу, а время в эфире, иначе одно медленное устройство на краю зоны утаскивает непропорционально много эфира и создаёт очередь для всех.

  • Ещё одно место — драйвер и сетевой стек самой операционной системы. Между приложением и проводом пакет проходит через несколько внутренних очередей, и они тоже умеют раздуваться. В Linux с этим борются механизмы вроде BQL, которые ограничивают, сколько данных стек отдаёт в очередь драйвера, не давая ей копить лишнее. Это тот же bufferbloat, только внутри хоста, а не на канале.

Практический смысл: когда меряете задержку под нагрузкой и она большая, узкое место может быть не там, где вы думаете. Оно на самом медленном участке пути — но этим участком бывает и радио, и внутренняя очередь хоста, а не только стык с провайдером. Поэтому диагностику ведут от точки, где скорость падает, а не от первого попавшегося устройства.

ECN: сигналить о перегрузке, не роняя пакеты

Пока всё лечение перегрузки строилось на том, чтобы ронять пакеты — это единственный сигнал, который понимает классический TCP. Но ронять пакет расточительно: его придётся переслать, а это ещё задержка. Хочется сказать «притормози», не выбрасывая пакет.

Ровно это делает ECN — explicit congestion notification. Вместо того чтобы дропнуть пакет, устройство ставит в его заголовке метку «я испытываю перегрузку», и пакет доходит до получателя. Получатель видит метку и сообщает отправителю, тот снижает скорость. Пакет не потерян, пересылать нечего, а сигнал перегрузки доставлен.

Работает это только когда все на пути умеют ECN — и отправитель с получателем договорились его использовать, и промежуточные устройства ставят метку вместо дропа. Если кто‑то в цепочке ECN не понимает, всё откатывается к обычным потерям.

Но у классического ECN есть ограничение, которое долго мешало ему раскрыться. Метка перегрузки в нём означает ровно то же, что потеря пакета, — «снизь скорость вдвое». Это грубый сигнал: TCP резко сбрасывает скорость, потом снова разгоняется, снова получает сигнал, снова сбрасывает — пилообразный график, при котором очередь то раздувается, то опустошается. ECN избавил от потерь, но не от колебаний задержки.

L4S: сверхнизкая задержка через частые тонкие сигналы

Здесь появляется самое интересное из свежего — L4S, ради которого операторы уже проводят полевые испытания, а производители встраивают поддержку в оборудование.

Идея L4S — переопределить смысл метки ECN, сделав сигнал перегрузки частым и тонким вместо редкого и грубого. Вместо «переполнилось, режь вдвое» устройство начинает помечать пакеты, едва очередь чуть подросла — при задержке в доли миллисекунды. А отправитель на такой сигнал реагирует не резким сбросом, а маленькой плавной подстройкой скорости.

Результат: вместо пилы, где задержка скачет от нуля до сотен миллисекунд, получается почти ровная линия с задержкой в очереди меньше миллисекунды. Отправитель постоянно получает частые лёгкие сигналы и постоянно чуть корректирует темп, держа очередь почти пустой, но канал при этом загруженным. Сверхнизкая задержка и полная утилизация одновременно — то, что раньше считалось взаимоисключающим.

Корни этого — в дата‑центрах. Там уже давно работал DCTCP, который использовал ровно такую логику частых тонких ECN‑сигналов, но только в замкнутой среде, потому что он несовместим с классическим TCP: агрессивный масштабируемый контроль в общей очереди задавил бы обычные потоки. L4S — это способ вынести эту дата‑центровую технику в общий интернет.

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

И вот тут главная сложность, из‑за которой L4S — не «включил и работает».

Проблема в сосуществовании. Масштабируемый трафик L4S и классический трафик нельзя держать в одной очереди. L4S реагирует на сигналы тонко и потому наращивает скорость агрессивнее — в общей очереди он вытеснил бы классические потоки, загнав их в мизерную долю канала. Их сигналы перегрузки означают разное, а очередь одна.

Решается это разделением.

  1. Один подход — Dual‑Queue Coupled AQM: две отдельные очереди на узком месте, одна для L4S, другая для классического трафика, а поле ECN служит признаком, по какому пакету в какую. Очередь L4S держат мелкой и метят пакеты рано, при задержке в доли миллисекунды, — потому она и даёт сверхнизкую задержку. Классическая очередь живёт по‑старому. Специальная связка между очередями следит, чтобы обе получали справедливую долю канала.

  2. Второй подход — те же честные очереди на поток, что в FQ‑CoDel, только умеющие L4S: раз у каждого потока своя очередь, конфликт семантик снимается сам собой, потоки просто изолированы. Минус тот же — нужен просмотр заголовков и память под очереди, что не везде есть.

И над всем этим висит требование, чтобы поддержка была по всему пути. Отправитель должен использовать масштабируемый контроль перегрузки — вроде TCP Prague, — получатель должен уметь отражать сигналы, а узкое место на пути должно иметь L4S‑совместимый AQM. Выпадет любое звено — и L4S либо не заработает, либо, хуже, L4S‑трафик задавит соседей, потому что попадёт в классическую очередь со своим агрессивным поведением. Именно поэтому это раскатывают осторожно и через полевые испытания, а не флажком в конфиге.

Incast: bufferbloat наоборот, в дата‑центре

Стоит отдельно сказать про дата‑центры, потому что там очереди ведут себя иначе, и наивное «поставим AQM» не всегда работает.

Классический сценарий дата‑центра — incast. Один узел запрашивает данные сразу у множества серверов — так устроены распределённые хранилища, MapReduce, шардированные базы. Все серверы отвечают почти одновременно, и их ответы сходятся в одну точку — на порт коммутатора перед запрашивающим узлом. Десятки потоков утыкаются в один буфер разом.

Здесь проблема противоположна домашней. Дома буфер большой и копит задержку. В дата‑центре буферы коммутаторов маленькие и быстрые, а трафик приходит синхронным залпом — буфер мгновенно переполняется, пакеты теряются пачками, TCP на потерях резко тормозит, и пропускная способность обрушивается, хотя канал быстрый. Задержки тут микросекундные, и классический TCP с его реакцией на потерю в такой среде ведёт себя слишком грубо.

Именно под это и придумали DCTCP — тот самый предок L4S. В замкнутой среде дата‑центра, где всё оборудование ваше и настроено единообразно, можно включить частые тонкие ECN‑сигналы и настроить контроль перегрузки так, чтобы он реагировал плавно и держал буферы почти пустыми. Это снимает и потери от incast, и микровсплески задержки. Работает именно потому, что среда контролируемая — все узлы говорят на одном языке контроля перегрузки, чего в общем интернете нет и ради чего пришлось изобретать L4S с разделением очередей.

Практический вывод для тех, кто строит дата‑центр: bufferbloat и incast — разные болезни, лечатся по‑разному. Домашний bufferbloat — это большой буфер, который надо научить активно управлять очередью. Incast — это маленький буфер и синхронный залп, где помогает DCTCP с тонкими ECN‑сигналами и иногда осознанное увеличение буфера ровно под размер залпа. Перепутать их и применить домашний рецепт к дата‑центру — значит сделать хуже.

Как это диагностировать у себя

Теперь практика — как понять, что у вас именно bufferbloat, а не что‑то другое.

  1. Главный тест — измерять задержку под нагрузкой, а не в покое.

  2. Пинг на простое ничего не покажет — он будет прекрасным.

  3. Нужно нагрузить канал закачкой или заливкой на полную и одновременно мерить пинг.

  4. Если в покое пять миллисекунд, а под нагрузкой сотни — это bufferbloat, классический симптом.

Специальные тесты вроде тех, что меряют задержку под нагрузкой отдельно на закачке и на отдаче, дают ту же картину наглядно и сразу говорят, в какую сторону канала раздут буфер. Часто оказывается, что проблема на отдаче — исходящий буфер на стороне абонента или в первом же устройстве провайдера.

Важно понимать, где узкое место, потому что лечить bufferbloat можно только там, где очередь реально образуется. Обычно это самая медленная точка пути — стык вашего канала с провайдером. Ставить AQM в середине быстрой сети бессмысленно, там очередь не копится. Он нужен ровно на узком месте, где быстрый трафик упирается в медленный канал и начинает складываться в буфер.

Проверить, насколько уверенно вы разбираетесь в таких задачах, можно, пройдя вступительный тест по сетевым технологиям. Он поможет оценить текущий уровень и увидеть темы, которые стоит повторить.

BBR: другой подход со стороны отправителя

Всё, о чём шла речь, — управление очередью на узком месте, то есть в сети. Но есть и второй фронт — сам отправитель, и там за последние годы случилось своё изменение, о котором полезно знать.

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

Это делает BBR устойчивее к bufferbloat со стороны отправителя: даже если на пути раздутый буфер без всякого AQM, BBR старается его не набивать, потому что ориентируется не на потери, а на измеренные задержку и полосу. Для отдачи с сервера — раздача контента, стриминг — это заметно помогает там, где управлять очередью в сети вы не можете, а отправителя контролируете.

Полной заменой AQM это не служит. BBR помогает, когда трафик исходит от вас и вы поставили этот контроль перегрузки; но чужие потоки на классическом TCP по‑прежнему набьют общий буфер, и от них BBR ваши интерактивные пакеты не защитит. Плюс у BBR своя история сосуществования с классическим TCP, которую дорабатывали не одну версию. Так что BBR и AQM — не альтернативы, а два конца одной проблемы: один управляет очередью в сети, другой старается её не создавать со стороны источника. В идеале работают оба.

Что со всем этим делать на практике

  • Если у вас домашний или офисный шлюз перед провайдерским каналом — самый прямой способ убрать bufferbloat — включить на нём FQ‑CoDel или CAKE и задать шейпинг чуть ниже реальной скорости канала. Смысл шейпинга в том, чтобы узкое место переехало с неуправляемого буфера провайдера на ваш управляемый AQM: если вы отдаёте чуть медленнее, чем канал физически может, очередь копится у вас, где ей есть кому управлять, а не в раздутом буфере на той стороне.

  • Если вы оператор или строите сеть — AQM ставится на границах, где сходятся разные скорости, и особенно на абонентских стыках. А дальше вопрос про L4S: он даёт качественно лучший результат для интерактива, но требует поддержки по всему пути и аккуратного развёртывания с разделением очередей, иначе можно навредить соседнему трафику. Полевые испытания у операторов идут как раз для того, чтобы отладить это до массового включения.

И общий принцип, который стоит унести: задержка под нагрузкой — это отдельная характеристика канала, не менее важная, чем пропускная способность, и она не видна, пока не начнёшь мерить под нагрузкой. Гигабитный канал с раздутым буфером для видеозвонка хуже, чем стомегабитный с нормальным AQM. Пропускную способность продают и меряют все, а задержку под нагрузкой — почти никто, и именно она определяет, будет ли сеть комфортной для всего интерактивного.

Как и в случае с bufferbloat, привычный механизм может формально работать, но уже не решать задачу сети. На бесплатном демо-уроке 24 августа в 20:00 разберем альтернативные способы защиты от петель и критерии выбора для разных топологий. Это поможет проектировать L2-сегменты так, чтобы локальный сбой не превращался в аварию всей сети. Присоединяйтесь.

Больше бесплатных уроков смотрите в дайджесте.