Что такое TCP-окно и почему широкий канал через океан работает как узкий
Вы арендовали сервер за океаном, провайдер обещает канал в 1 Гбит/с или больше, но при копировании одного файла по scp или rsync скорость упирается в жалкие проценты от заявленной пропускной способности — при этом iperf3 в несколько параллельных потоков спокойно выдаёт куда больше. Дело не в канале и не в диске: дело в TCP-окне и задержке до сервера. Разберёмся, что это за окно, почему оно решает всё на длинных маршрутах и что с этим можно сделать.
Содержание
- Что такое TCP-окно приёма и зачем оно нужно
- Формула, которая объясняет всё: окно делить на RTT
- Почему широкий трансатлантический или транстихоокеанский канал не заполняется сам собой
- Window scaling: как TCP научили работать с большими окнами
- Сколько окна нужно, если знать RTT и желаемую скорость
- Что проверить и настроить на сервере, который обслуживает дальние регионы
Что такое TCP-окно приёма и зачем оно нужно
TCP — протокол с подтверждением доставки: каждый переданный байт должен быть либо подтверждён получателем (ACK), либо в конце концов переотправлен, если подтверждение не пришло. Чтобы не ждать подтверждения после каждого пакета (это было бы мучительно медленно), TCP разрешает отправителю держать «в полёте» — отправленными, но ещё не подтверждёнными — сразу несколько пакетов.
Сколько байт можно держать в полёте одновременно, определяет окно приёма (receive window, rwnd). Это значение объявляет получатель отправителю в каждом TCP-сегменте: «у меня в буфере есть место на столько-то байт, можешь слать, не дожидаясь ACK на каждый пакет». Отправитель обязан уважать это число — он не имеет права выслать больше данных, чем помещается в объявленное окно, пока не придёт подтверждение хотя бы части из них и окно не «сдвинется».
Есть ещё окно перегрузки (congestion window, cwnd) — число, которое вычисляет сам отправитель на основе своей оценки состояния сети (не переполнен ли канал, не теряются ли пакеты). Реальный объём данных в полёте — это минимум из rwnd и cwnd. Для темы статьи важнее rwnd: именно его размер вместе с задержкой на маршруте задают физический потолок скорости одного TCP-соединения, который никакой мощный канал сам по себе не пробьёт.
Механику самого установления соединения разбирали отдельно — см. TCP-рукопожатие и почему всё виснет. Здесь же смотрим на то, что происходит уже после хендшейка, во время самой передачи данных.
Формула, которая объясняет всё: окно делить на RTT
Вот ключевая идея, из которой следует всё остальное. Пока отправитель не получит подтверждение на первый пакет, он не может выслать данных больше, чем помещается в окно. Подтверждение возвращается не мгновенно — оно идёт через сеть туда и обратно, и это время называется RTT (round-trip time, время кругового пути). Значит, максимальная скорость, которую способно выдать одно TCP-соединение с заданным размером окна, ограничена сверху формулой:
максимальная скорость ≈ размер окна / RTT
Это и есть произведение пропускной способности на задержку (bandwidth-delay product, BDP) — только вывернутое наизнанку: вместо «сколько байт вмещается в канал за время RTT» мы спрашиваем «какую скорость даст окно фиксированного размера при данном RTT».
Возьмём чисто иллюстративный пример — не измеренные данные с реального маршрута, а просто чтобы почувствовать масштаб. Пусть окно приёма равно 64 КБ (это исторический дефолт без масштабирования — к нему вернёмся ниже) и пусть RTT условно равен 150 мс. Тогда:
64 КБ / 0.15 с ≈ 437 КБ/с ≈ 3.5 Мбит/с
Три с половиной мегабита в секунду — и совершенно неважно, что физический канал между дата-центрами рассчитан на десятки гигабит. Одно TCP-соединение с таким окном физически не может выжать из этого канала больше, потому что оно просто не имеет права держать в полёте больше 64 КБ неподтверждённых данных. Пока не пришёл ACK на первую порцию — можно слать не больше, чем ещё осталось в окне, и точка.
Обратите внимание на форму зависимости: скорость обратно пропорциональна RTT. Тот же сервер, то же окно — но если получатель на соседней стойке того же дата-центра с RTT в единицы миллисекунд, а не через океан, формула даст результат на порядки выше. Поэтому один и тот же сервер может «летать» для локальных клиентов и «тормозить» для клиентов с другого континента — с одинаково широким физическим каналом в обоих случаях.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПочему широкий трансатлантический или транстихоокеанский канал не заполняется сам собой
Это и есть ответ на вопрос из заголовка. «Широкий канал» — характеристика физической или арендованной инфраструктуры между двумя точками: сколько бит в секунду туда в принципе помещается. «Скорость одного TCP-потока» — совсем другая величина, определяемая окном и задержкой. Эти два числа никак не связаны напрямую, и именно это несовпадение сбивает с толку: интуитивно кажется, что если канал в 10 раз шире, то и передача пойдёт в 10 раз быстрее, а на деле она может остаться такой же — потому что упирается не в канал, а в окно.
Чем длиннее физическое расстояние (через океан, через несколько промежуточных сетей), тем больше RTT — сигнал не может распространяться быстрее скорости света в оптоволокне, и добавляются задержки на каждом промежуточном узле. Рост RTT напрямую снижает потолок скорости по формуле выше, если размер окна остаётся прежним. Получается контринтуитивная картина: сервер с супер-скоростным каналом, но с клиентами на другом конце света, будет «казаться» медленным для одиночных передач по одному соединению — не потому что канал плохой, а потому что окно не успевает «прокрутиться» достаточно часто, чтобы заполнить весь доступный канал.
Разница между «канал широкий» и «соединение быстрое» — тот же концептуальный разрыв, что между пропускной способностью и задержкой в целом; мы разбирали его на примере сайтов в статье пинг хороший, а сайт медленный: задержка и полоса. TCP-окно — тот механизм, через который RTT буквально ограничивает достижимую пропускную способность одного потока, даже когда с каналом всё в порядке.
Window scaling: как TCP научили работать с большими окнами
Исходное поле размера окна в заголовке TCP — 16-битное, а значит, без дополнительных ухищрений оно может выразить значение не больше 65535 байт (те самые 64 КБ из примера выше). Когда TCP проектировался, этого хватало с запасом: сети были медленными, а RTT — предсказуемо небольшим в тогдашней топологии. С ростом скоростей каналов и появлением по-настоящему протяжённых межконтинентальных маршрутов 64 КБ окна стали жёстким узким местом почти на любом мало-мальски быстром канале со сколько-нибудь заметной задержкой.
Решение — опция window scaling (RFC 1323): во время установления TCP-соединения (в SYN и SYN-ACK) стороны обмениваются множителем масштабирования — по сути, битовым сдвигом от 0 до 14. Реальный размер окна получается умножением значения из заголовка на 2 в степени этого сдвига. Максимальный сдвиг 14 позволяет расширить эффективное окно примерно до 1 ГБ — этого достаточно для подавляющего большинства реальных сценариев, включая быстрые каналы с большим RTT.
Важный нюанс: window scaling должен согласоваться на этапе установления соединения обеими сторонами. Если хотя бы одна из них (устаревший клиент, кривой NAT, сломанный посредник в сети) не поддерживает или обрезает эту опцию, соединение откатывается на исходные 64 КБ — и вы упираетесь ровно в тот потолок из формулы выше, вне зависимости от настроек сервера. Это одна из тех «граблей», которые сложно диагностировать по симптомам: канал широкий, сервер настроен правильно, а скорость одного потока всё равно низкая — потому что где-то на пути масштабирование окна не согласовалось.
На современных серверных дистрибутивах Linux window scaling включён по умолчанию, но стоит явно проверить:
sysctl net.ipv4.tcp_window_scaling
Значение 1 означает, что опция включена ядром — но это не гарантирует, что она реально применяется на конкретном соединении, если что-то по пути её режет. Проверить фактический размер окна на активном соединении можно через ss:
ss -tin dst <ip-клиента-или-сервера>
В выводе будут поля вроде cwnd, rcv_space и rcv_rtt — по ним видно и реальный RTT для конкретного соединения, и текущее окно перегрузки. Табличные ориентиры по типичным задержкам между регионами (без привязки к конкретному вашему маршруту, только для понимания порядка величин) собраны в статье задержка между локациями: таблица ориентиров.
Сколько окна нужно, если знать RTT и желаемую скорость
Формулу можно развернуть в обратную сторону: если вы знаете (или примерно оцениваете по факту, замером) RTT до клиентов и хотите достичь определённой скорости на одном соединении, минимальный необходимый размер окна считается так:
нужное окно ≈ желаемая скорость × RTT
Это тот самый bandwidth-delay product в его прямой форме. Практический смысл: если у вас сервер обслуживает клиентов с ощутимым RTT (трансатлантический или транстихоокеанский маршрут — типичный случай для UK/US-локаций, обслуживающих клиентов в других частях света), а буферы TCP на сервере или клиенте настроены на скромные значения по умолчанию, вы искусственно режете скорость каждого отдельного соединения — независимо от того, что канал шире.
Практически это означает: чем больше произведение скорости канала на задержку, тем больше должно быть окно, чтобы канал не простаивал в ожидании ACK. Для очень быстрых каналов с большим RTT (условно — десятки и сотни мегабит на соединение с межконтинентальным RTT) требуемые окна могут заметно превышать исторические дефолты старых систем, и здесь на первый план выходит настройка буферов, о которой ниже.
Что проверить и настроить на сервере, который обслуживает дальние регионы
Если сервер стоит в одной локации (скажем, UK), а заметная часть клиентов или интеграций — в другой части света, есть несколько практических шагов, которые стоит проверить именно из-за эффекта окна и RTT:
1. Буферы приёма и отправки TCP. Ядро Linux определяет минимальный, дефолтный и максимальный размер буфера отдельно на приём и отправку:
sysctl net.ipv4.tcp_rmem
sysctl net.ipv4.tcp_wmem
sysctl net.core.rmem_max
sysctl net.core.wmem_max
Каждая переменная выдаёт тройку чисел (минимум, дефолт, максимум) в байтах. Максимум задаёт потолок, до которого автоматическая настройка окна (auto-tuning) может расширить буфер под конкретное соединение. Если максимум занижен относительно вашего реального BDP, auto-tuning не сможет разогнать окно достаточно, и скорость упрётся в потолок независимо от ширины канала. Конкретные числа подбирайте по факту, отталкиваясь от реального RTT до ваших клиентов и нужной скорости на поток — не переписывайте чужие значения бездумно.
2. Congestion control алгоритм. Помимо rwnd, реальную скорость ограничивает и cwnd — окно перегрузки, которое отправитель наращивает по алгоритму congestion control. На длинных высокоскоростных маршрутах классический CUBIC (дефолт во многих дистрибутивах) может разгоняться заметно медленнее, чем современный BBR, особенно на маршрутах с редкими, но случающимися потерями пакетов, которые CUBIC интерпретирует как сигнал резко снизить скорость. Проверить текущий алгоритм и доступные варианты:
sysctl net.ipv4.tcp_congestion_control
sysctl net.ipv4.tcp_available_congestion_control
Включить BBR (если модуль ядра доступен):
modprobe tcp_bbr
sysctl -w net.core.default_qdisc=fq
sysctl -w net.ipv4.tcp_congestion_control=bbr
Эффект от смены алгоритма сильно зависит от конкретного маршрута, характера потерь пакетов и профиля нагрузки — универсальных цифр прироста здесь никто честно не назовёт, проверяйте на своём трафике.
3. Параллелизм вместо одного окна. Иногда проще не бороться с ограничением одного соединения, а обойти его: разбить передачу на несколько параллельных TCP-потоков. У каждого потока своё независимое окно, и суммарная скорость нескольких параллельных соединений может приблизиться к реальной ширине канала там, где одно соединение — нет. Отсюда практика: aria2c с несколькими соединениями на файл, S3 multipart upload, rsync с несколькими процессами через xargs/parallel, HTTP/2 и HTTP/3 с мультиплексированием множества потоков внутри одного или нескольких соединений. Это не «более честный» способ использовать канал, а именно обходной манёвр вокруг лимита одного TCP-окна.
4. Измеряйте, а не гадайте. Прежде чем что-то менять, проверьте, действительно ли дело в окне, а не в чём-то ещё (перегруженный upstream, шейпинг на стороне провайдера, проблемы на маршруте). iperf3 в однопоточном и многопоточном режиме (-P 4, -P 8) на одном и том же маршруте быстро покажет, упирается ли скорость именно в окно одного соединения — если многопоточный тест кратно быстрее однопоточного при том же канале, это прямой признак проблемы BDP. Подробная методика запуска и трактовки результатов — в статье измерение скорости через iperf3.
5. Не забывайте про MTU. Окно и MTU — разные механизмы, но иногда их путают или чинят одно вместо другого. Проблемы с фрагментацией пакетов дают свои характерные симптомы (обрывы, а не просто медленную передачу) — это отдельная тема, разобранная в других статьях блога, и её стоит исключить отдельно, а не списывать всё на TCP-окно.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Если у меня на сервере широкий канал (1 Гбит/с и больше), почему скачивание одного файла через один поток всё равно медленное, если клиент далеко?
Потому что скорость одного TCP-соединения ограничена не шириной канала, а формулой «окно / RTT». Широкий канал определяет теоретический потолок для суммы всех соединений через него, а не для одного конкретного соединения — тем более на маршруте с большим RTT.
Помогает ли просто увеличить net.core.rmem_max и wmem_max до максимума?
Это необходимое условие, но не достаточное: auto-tuning ядра расширяет окно только до заданного максимума, но фактический размер согласуется ещё и с window scaling на этапе установления соединения, и с тем, сколько буфера реально нужно под ваш RTT и целевую скорость. Слепое завышение буферов без расчёта под реальный BDP может просто впустую расходовать память на большое число соединений.
Чем window scaling отличается от congestion control (BBR/CUBIC)?
Window scaling — это протокольная опция, расширяющая максимально возможный размер окна приёма за пределы исходных 64 КБ. Congestion control — алгоритм, по которому отправитель сам решает, сколько данных реально безопасно держать в полёте прямо сейчас, реагируя на потери и задержки. Оба механизма влияют на конечную скорость, но решают разные задачи и настраиваются независимо.
Одинаково ли это ограничение действует на UDP-based протоколы вроде QUIC (HTTP/3)?
У QUIC своя логика управления окном и перегрузкой поверх UDP, концептуально решающая ту же задачу (BDP никуда не девается — это свойство физики канала и задержки, а не конкретного протокола), но детали реализации отличаются от классического TCP. Если тема интересна отдельно — сравнение поведения HTTP/3 разобрано в других статьях блога.
Можно ли вообще обойти ограничение BDP без увеличения окна?
Прямого обхода нет — формула не «настройка», а следствие того, что подтверждение неизбежно идёт через сеть и занимает время RTT. Единственные рабочие пути — увеличить окно (через window scaling и правильные буферы) либо распараллелить передачу на несколько независимых TCP-соединений, каждое со своим окном.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →