MAATRIX / Блог / Канал гигабит, качается 8 МБ/с: TCP-окно и проклятие длинных толстых каналов

Канал гигабит, качается 8 МБ/с: TCP-окно и проклятие длинных толстых каналов

MAATRIX

В договоре — гигабит, в панели провайдера — гигабит, а scp до сервера в другом регионе упрямо ползёт на скорости, которая с гигабитом не имеет ничего общего. Диск не при чём, VPN не при чём, провайдер обычно тоже не врёт. Дело почти всегда в связке TCP-окна и задержки — в том, что сетевые инженеры называют long fat network, «длинный толстый канал»: широкая труба, в которую упаковано слишком мало данных за один оборот сигнала туда-обратно.

Что такое «длинный толстый канал» и почему это не каламбур

Термин long fat network (LFN) придумали не для красоты — он из RFC 1323, того самого документа, где описан window scaling. «Толстый» — значит канал с большой номинальной пропускной способностью: гигабит, десять гигабит, неважно. «Длинный» — значит с заметной задержкой (RTT), обычно потому что данные идут через полконтинента, через океан или через несколько транзитных сетей с очередями на каждом узле. По отдельности ни то, ни другое не проблема. Проблема — их сочетание.

Симптом почти всегда один и тот же: если измерить канал в лоб — например, залить трафик в несколько параллельных потоков или прогнать iperf3 с -P 8 — цифры близки к заявленным. А если скачать один файл одним TCP-соединением — скорость падает до жалких единиц или низких десятков мегабит, независимо от того, что написано в тарифе. Это не совпадение и не брак канала: одно TCP-соединение физически не может выбрать всю полосу LFN без специальной настройки, и дальше разберём почему.

TCP-окно: сколько данных можно держать «в полёте»

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

Сколько именно — определяют два независимых механизма:

  • Окно приёма (receive window, rwnd) — его объявляет получатель в каждом TCP-сегменте: «у меня в буфере есть место на столько-то байт». Отправитель не имеет права превысить это число, пока не придёт хотя бы частичное подтверждение и окно не сдвинется.
  • Окно перегрузки (congestion window, cwnd) — его вычисляет сам отправитель, оценивая состояние сети: не теряются ли пакеты, не начинает ли канал захлёбываться.

Реальный объём данных в полёте — это минимум из rwnd и cwnd. Для темы длинных толстых каналов решающую роль обычно играет именно rwnd и то, во сколько раз он расширен относительно исторического дефолта — но по мере разгона соединения не менее важно, как быстро наращивается cwnd; это уже вопрос алгоритма congestion control, который здесь трогать не будем — это отдельная тема.

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

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

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

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

BDP: bandwidth × delay — формула, которая расставляет всё по местам

Чтобы одно TCP-соединение выбрало весь номинал канала, окно должно вмещать ровно столько данных, сколько канал успевает пропустить за время одного оборота сигнала туда и обратно. Это и есть bandwidth-delay product (BDP):

BDP (байт) = пропускная способность канала (байт/с) × RTT (с)

Возьмём иллюстративный пример — не измерение конкретного маршрута, а просто чтобы почувствовать порядок величины. Пусть канал гигабитный (около 125 МБ/с в потенциале) и RTT между сервером и клиентом — условно 65 мс (реалистичный порядок для маршрута через океан или несколько транзитных сетей). Тогда:

BDP ≈ 125 МБ/с × 0.065 с ≈ 8 МБ

Восемь мегабайт — это и есть тот объём данных, который нужно держать в полёте одновременно, чтобы гигабитный канал не простаивал в ожидании ACK. Если окно приёма реально ограничено чем-то заметно меньшим — скажем, старым дефолтом или буфером, зауженным где-то по дороге, — скорость одного потока упадёт пропорционально нехватке окна относительно этих 8 МБ. Отсюда и вполне жизненная картина из заголовка: гигабитный канал, а один поток качает те самые единицы мегабайт в секунду — не потому что канал плохой, а потому что окно на порядок меньше требуемого BDP.

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

Window scaling (RFC 1323): как убрали потолок в 64 КБ

Поле размера окна в заголовке TCP — 16-битное, а значит без дополнительных ухищрений оно ограничено значением 65535 байт, то есть теми самыми 64 КБ. Когда протокол проектировался, этого хватало с большим запасом: сети были медленными, а маршруты — короткими и предсказуемыми. С ростом скоростей каналов и появлением по-настоящему протяжённых маршрутов 64 КБ стали жёстким потолком почти на любом гигабитном канале с заметным RTT — задолго до того, как физическая пропускная способность канала исчерпывалась.

Решение — опция window scaling, описанная в RFC 1323 в 1992 году. При установлении соединения (в пакетах SYN и SYN-ACK) стороны обмениваются коэффициентом масштабирования — сдвигом от 0 до 14 бит. Реальное окно получается умножением значения из заголовка на 2 в степени этого сдвига, что позволяет расширить эффективное окно примерно до гигабайта — с большим запасом для подавляющего большинства реальных сценариев, включая гигабитные и более быстрые каналы с межконтинентальным RTT.

На современных серверных и десктопных ОС window scaling включён по умолчанию. Проверить на Linux:

sysctl net.ipv4.tcp_window_scaling

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

Где масштабирование окна тихо ломается посередине

Это самый коварный сценарий, потому что снаружи он неотличим от «провайдер продал не тот канал». Window scaling — опция, которая передаётся только один раз, на этапе трёхстороннего рукопожатия, в SYN и SYN-ACK. Если хоть одна из сторон на пути — не обязательно сервер или клиент, а именно что-то посередине — не пропускает или обрезает эту опцию, соединение молча откатывается на исходные 64 КБ. Никакой ошибки, никакого предупреждения — просто медленная передача на быстром канале.

Типичные виновники:

  • Старый или неверно настроенный firewall / IDS-инспектор, который переписывает или полностью вырезает TCP-опции, которых «не понимает» — это классика для устройств с прошивками десятилетней давности или для агрессивных правил глубокой инспекции пакетов.
  • NAT-устройство с некорректной реализацией — иногда трансляция адресов идёт рука об руку с пересборкой TCP-заголовков, и опции теряются при этой пересборке.
  • Промежуточное оборудование провайдера, оптимизирующее трафик «для ускорения» (WAN-акселераторы, некоторые прокси) — парадоксально, такие устройства сами могут резать масштабирование окна и тем самым замедлять именно те долгие маршруты, для которых и создавались.
  • Асимметричный маршрут, где путь туда и обратно проходит через разное оборудование — отдельная тема сама по себе, но она тоже может быть косвенно замешана: если пакет с опцией SYN-ACK идёт через устройство, которое её режет, а прямой путь — нет, диагностика усложняется вдвое.

Проверить, что реально согласовалось на конкретном соединении, можно через ss:

ss -tin dst <ip-адрес>

В выводе смотрите на поле cwnd и на фактически используемый rcv_space — если они устойчиво держатся в районе 64 КБ на канале, где по BDP нужно на порядок больше, это прямой след срезанного window scaling где-то на пути. Дальше локализация — это уже последовательное исключение узлов на маршруте, и для этого держите под рукой обычные traceroute/mtr, а концептуальную разницу между географией маршрута и его фактическим путём стоит держать в голове — она разобрана в статье почему маршрут не совпадает с географией.

Диагностика: iperf3 в один поток против нескольких параллельных

Прежде чем что-то менять на сервере, стоит убедиться, что дело именно в окне, а не в перегруженном апстриме, шейпинге на стороне провайдера или банальных потерях пакетов. Самый быстрый и надёжный тест — сравнить однопоточный и многопоточный iperf3 на одном и том же маршруте.

Запуск сервера на удалённой стороне:

iperf3 -s

Однопоточный тест с клиента:

iperf3 -c <ip-сервера> -t 20

Многопоточный тест с тем же сервером — несколько параллельных TCP-соединений одновременно:

iperf3 -c <ip-сервера> -t 20 -P 8

Логика простая и почти всегда однозначная: у каждого параллельного потока — своё независимое окно и своя пара rwnd/cwnd. Если однопоточный тест даёт скромный результат, а восемь параллельных потоков в сумме выдают кратно больше — вплоть до почти полного номинала канала — это прямой и практически безальтернативный признак того, что дело именно в размере окна одного соединения, а не в физическом состоянии канала. Если же и многопоточный тест упирается примерно в тот же потолок, что и однопоточный, — ищите проблему не в окне, а в чём-то общем для всех потоков: реальной перегрузке канала, шейпинге на стороне провайдера или потерях пакетов, которые режут скорость через механизм congestion control сразу у всех соединений одновременно.

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

Буферы ядра: идея auto-tuning, а не рецепт под конкретное железо

Если диагностика через параллельные потоки подтвердила проблему окна, следующий шаг — посмотреть, не упирается ли auto-tuning ядра в заниженный потолок. На Linux размер буферов приёма и отправки настраивается тройками значений (минимум, дефолт, максимум) в байтах:

sysctl net.ipv4.tcp_rmem
sysctl net.ipv4.tcp_wmem
sysctl net.core.rmem_max
sysctl net.core.wmem_max

Идея в том, что ядро само динамически расширяет окно под конкретное соединение (auto-tuning), но только до того максимума, который заданы в rmem_max/wmem_max. Если этот максимум занижен относительно вашего реального BDP — auto-tuning просто не сможет разогнать окно достаточно, сколько бы времени соединение ни жило. Здесь принципиально важно не переносить чужие «рецептные» числа из форумов и статей на своё железо и свой маршрут: правильное значение зависит от реального RTT до ваших клиентов и от того, сколько параллельных соединений одновременно делят один и тот же буфер памяти. Считайте от своего BDP (формула выше), проверяйте через ss -tin после изменения и смотрите на реальный эффект в iperf3, а не на теоретическую цифру в конфиге.

Отдельно стоит держать в уме congestion control — алгоритм, которым отправитель регулирует cwnd в ответ на потери пакетов. Даже идеально настроенное окно приёма не спасёт, если алгоритм слишком консервативно реагирует на редкие, но случающиеся потери на длинном маршруте — но это уже отдельная от window scaling история, и мешать эти два механизма в одной настройке не стоит: сначала убедитесь, что окно достаточно большое, и только потом разбирайтесь с реакцией на потери.

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

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

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

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

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

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

Правда ли, что если канал шире, скорость одного файла вырастет пропорционально?

Нет, и это самое частое заблуждение. Скорость одного TCP-соединения ограничена окном и RTT, а не шириной канала напрямую. Расширение канала с 1 до 10 Гбит/с ничего не даст для одного потока, если окно и так уже упирается в 64 КБ или в заниженный буфер — узкое место в этом случае не в трубе, а в протоколе.

Достаточно ли просто поднять net.core.rmem_max и wmem_max до максимума?

Это необходимое, но не достаточное условие. Фактическое окно ещё зависит от того, согласовался ли window scaling на конкретном соединении и не срезала ли его какая-то точка на маршруте. Бездумное завышение буферов на сервере с тысячами одновременных соединений может просто впустую съесть память, не решив проблему одного медленного потока.

Как быстро понять, что дело в окне, а не в реальной перегрузке канала?

Сравнить однопоточный iperf3 с многопоточным (-P 8 и больше) на одном и том же маршруте. Если сумма параллельных потоков заметно ближе к номиналу канала, чем один поток, — это окно. Если и параллельные потоки упираются в тот же потолок — ищите перегрузку, шейпинг или потери пакетов.

Почему у одного и того же сервера скорость разная для локальных и удалённых клиентов?

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

Обязательно ли лезть в sysctl, если нужна просто высокая скорость передачи одного файла?

Не всегда. Часто проще и надёжнее обойти лимит одного окна распараллеливанием: aria2c с несколькими соединениями на файл, S3 multipart upload, rsync через xargs/parallel, или протоколы с мультиплексированием потоков вроде HTTP/2 и HTTP/3. Это не отменяет пользу от правильных буферов и window scaling, но даёт быстрый практический результат без правки sysctl на проде.

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

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

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