Некоторое время назад, в связи со всем известными событиями, я решил защитить свой текущий тоннель до VPS в Нидерландах, для которого до этого использовал обычный WireGuard. Как это часто со мной бывает, я решил пойти не самым простым, но зато самым любимым мной путём – и написал свой тоннель. :) Идея эта была привлекательна тем, что давала мне возможность познакомиться с Tokio, побольше узнать о принципах работы tun/tap–интерфейсов в Linux, почитать исходники Shadowsocks, ну и в процессе даже удалось найти и поправить небольшую багу в networkd. Но на самом деле речь в данном посте пойдёт не об этом.

Когда я писал свой тоннель, у меня было стойкое убеждение, что в качестве транспорта нужно прежде всего ориентироваться на UDP, а на TCP откатываться только в том случае, когда UDP перестаёт работать в силу тех или иных “причин”. И это вроде бы логично: в плане производительности для тоннеля UDP всегда предпочтительнее, т. к. он реализует именно то, что нам нужно – максимально тонкую обёртку над пакетами, а с TCP у нас начинается целый ворох проблем, начиная с TCP Meltdown и заканчивая head-of-line blocking.

Но вот когда я начал думать, как можно подтюнить получившийся тоннель помимо включения довольно очевидных вещей вроде TCP_NODELAY, то набрёл на BBR, который стал для меня очень приятным открытием.

BBR

BBR (Bottleneck Bandwidth and RTT) – это алгоритм TCP congestion control, разработанный в Google. В отличие от традиционных алгоритмов, которые ориентируются на потери пакетов, BBR пытается судить о загруженности канала, наблюдая за тем, как меняется скорость передачи данных и RTT. Сейчас сети уже не те, что были в 80-ых годах, и такой подход работает гораздо лучше, а особенно – в случае трансконтинентальных соединений, где у нас может быть довольно широкий канал, но при этом небольшой процент потерь пакетов является нормой (как в моём случае с дешёвым VPS).

В результате получается, что если у вас такой канал, где нередки небольшие потери пакетов, то это сильно сказывается на скорости TCP-соединения, т. к. эти потери расцениваются традиционными алгоритмами как сигнал к тому, что необходимо сбросить скорость до тех пор, пока они не сойдут на нет. К тому же, даже в случае когда потерь нет, но при этом TCP-соединение способно утилизировать весь канал передачи данных, традиционные алгоритмы склонны снижать скорость только тогда, когда буфер роутера (зачастую довольно большой) уже переполнен, и роутер начинает дропать пакеты, что по факту является сильно запоздалой реакцией, которая приводит к увеличению latency, а BBR как раз пытается этой ситуации избежать. Этой проблеме даже посвящен целый сайт bufferbloat.net.

Польза, которую BBR способен вам нанести по сравнению со стандартным CUBIC’ом, сильно зависит от многих факторов, но вот, к примеру, отчёты Amazon и Google, которые свидетельствуют о том, что после включения BBR у них стабильно улучшились bandwidth и RTT: Amazon, Google. В определённых случаях можно ожидать и кратного увеличения скорости TCP-соединения.

Т. к. BBR является алгоритмом TCP congestion control, то включается он на отправляющей стороне. Т. е., включив его на своём ноутбуке, вы улучшите upload данных, а чтобы улучшить download, он должен быть включён на сервере. При этом, само собой, включение его на одной стороне не требует какой-либо поддержки на другой, т. к. меняются только эвристики внутри TCP-протокола, а не сам протокол.

Критика

Не обошлось правда и без критики данного алгоритма. Всё дело в том, что по сравнению со стандартным CUBIC’ом, BBR ведёт себя достаточно агрессивно, и может получиться так, что если вы, скажем, включите его на своем Linux-ноутбуке и начнёте заливать большие файлы в сеть, то ваш BBR-ноутбук может запросто “задушить” TCP-соединения соседних устройств, использующих традиционные алгоритмы TCP congestion control (а в MacOS, к примеру, BBR и вовсе недоступен).

Есть инициатива в виде BBRv2, которая пытается решить эту проблему, но пока что в ядре используется первая версия, и надо эту особенность иметь в виду.

Эффект от BBR применительно к TCP-тоннелю

Так вот, почитав всё это, я с одной стороны обрадовался (какая многообещающая штука – надо пробовать!), а с другой – тут же взгрустнул: включать BBR нужно на конечных устройствах (сервер + клиент), а на роутере его включать бесполезно, т. к. при forwarding’е пакетов все эти алгоритмы по понятным причинам не задействованы. Но на серверную часть я повлиять не могу, а в качестве клиентской у меня ноутбук с MacOS, в котором BBR и вовсе нет. С другой стороны – подумал я – у меня ведь по факту два TCP-соединения: одно между ноутбуком и конечным сервером, а другое – между двумя точками тоннеля, которые находятся на Linux-серверах, и вот на это соединение я повлиять могу.

Чтож, попробуем… Включил его для TCP-соединения своего тоннеля – и тут же получил 2.5x скорость при скачивании файлов! Если раньше что с UDP, что с TCP-транспортом у меня абсолютным максимумом было 30 Mbit/s, то после включения BBR оно тут же скакнуло до 75 Mbit/s.

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

В итоге я пришёл к следующему: если раньше я всегда в качестве транспорта использовал UDP, и переключение на TCP вызывало заметные глазу подтормаживания при загрузке страниц в браузере, то теперь, когда у меня TCP работает с BBR, картина поменялась на противоположную: UDP выдаёт вполне приемлемый результат, но если переключиться на TCP, то невооружённым глазом становится видно, что всё начинает подгружаться ещё быстрее. Учитывая то, что TCP-тоннель ещё и можно замаскировать под обычное HTTPS-соединение, получается, что в использовании UDP и вовсе нет смысла. Единственное, в чём UDP по прежнему обходит TCP + BBR – это в latency: если запустить ping и начать грузить канал, то в случае с UDP latency ping’ов практически не меняется, в то время как с TCP (с BBR и без) оно может увеличиваться до четырёх раз. Но т. к. это довольно синтетический тест, а при реальном использовании с браузером, как я описал выше, я вижу противоположные результаты, то для меня это не выглядит проблемой.

Включение

Включается BBR очень просто:

sysctl net.ipv4.tcp_available_congestion_control показывает список доступных алгоритмов. Скорее всего, по умолчанию BBR там не будет:

$ sudo sysctl net.ipv4.tcp_available_congestion_control
net.ipv4.tcp_available_congestion_control = reno cubic

– это потому, что не загружен соответствующий модуль ядра.

  1. Загружаем модуль – modprobe tcp_bbr (или echo tcp_bbr > /etc/modules-load.d/bbr.conf, чтобы он загружался автоматом при старте системы).
  2. Включаем BBR – sysctl net.ipv4.tcp_congestion_control=bbr (или echo 'net.ipv4.tcp_congestion_control = bbr' > /etc/sysctl.d/bbr.conf, чтобы он включался автоматом при старте системы). На всякий случай замечу, что, несмотря на префикс net.ipv4.*, включение происходит как для IPv4, так и для IPv6.

При этом его можно включить не для всех, а только для отдельных TCP-соединений, передав опцию TCP_CONGESTION в setsockopt(2). И даже у iperf есть опция -C bbr, с помощью которой можно протестировать поведение различных алгоритмов TCP congestion control конкретно для вашего случая.

Last, but not least

В процессе изучения всего вышеописанного я абсолютно случайно для себя узнал, что в то время как во всех современных дистрибутивах благодаря systemd уже давно в качестве queueing discipline по умолчанию включена fq_codel, которая считается наиболее оптимальным general purpose вариантом, то в Debian/Ubuntu меинтейнеры не смогли преодолеть свою внутреннюю бюрократию – и даже в самых современных Debian/Ubuntu по умолчанию используется pfifo_fast: они не включают в systemd-пакет его стандартный конфиг, но в то же время не смогли найти “правильное место”, куда можно было бы положить аналогичные разумные default’ы – и в результате используется значение по умолчанию, которое установлено в ядре.

pfifo_fast – это самая простая queueing discipline, которая никак не приоритезирует пакеты между различными сетевыми соединениями, и может получиться так, что самое активное из них будет сильно увеличивать latency всех остальных.

Поэтому рекомендую всем пользователям Debian/Ubuntu добавить /etc/sysctl.d/00-qdisc.conf со следующим содержимым:

net.core.default_qdisc = fq_codel

чтобы исправить это недоразумение.