Блог

Пропускная способность на такт CPU: число, которое выживает при столкновении с роутером

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

Опубликовано 6 сентября 2026 г.

На двух физических роутерах, гоняя реальный трафик по настоящей гигабитной локальной сети, ядро нашего туннеля выдало 122 Мбит/с там, где собственная туннельная реализация ядра роутера выдала 66,8 Мбит/с — на одном и том же отправителе, при меньшей нагрузке CPU на мегабит. Ни одна из цифр сама по себе не рассказывает всю историю: двум плечам сравнения пришлось использовать разные генераторы трафика, потому что ни один генератор не работает на обеих сторонах этого сравнения. Поэтому спорить стоит о соотношении каждого плеча к собственному потолку без туннеля, который приведён в таблице ниже и говорит в нашу пользу ещё сильнее, а не слабее. Лабораторный прогон на четырёх ВМ на ноутбуке даёт более высокое число в абсолютных величинах и, по одной из осей, число менее выгодное для нас. Мы больше доверяем цифре с роутера, потому что из двух именно роутер — то место, через которое реально проходит трафик клиента.

Прочитайте это перед цифрами. Наш дата-плейн по умолчанию использует один поток. Компаратор, который мы запускали рядом с ним в лаборатории, по умолчанию использует четыре. Без выравнивания один этот флаг меняет его пропускную способность с ~789 до 1427 Мбит/с — разрыв в настройках по умолчанию, а не в датапасе — и он уже однажды привёл к публикации одной неверной цифры здесь. Каждая лабораторная цифра ниже закрепляет оба ядра на Rust за одним потоком. Реализации на Go и в ядре ОС в этой таблице работали при собственных настройках по умолчанию, которые используют больше одного потока, — если это на что-то и влияет, то скорее в их пользу против нашего одного потока. Разрыв между настройками по умолчанию «как есть» показан отдельно, один раз, чтобы он тоже не был скрыт умолчанием.

Что измерялось и на чём

Две независимые среды, потому что бенчмарк честен только в пределах той среды, в которой он проводился — число из iperf3 и число из bandwidth-test — это не одно число под двумя именами, а ВМ, делящая ядра с ноутбуком, — не роутер.

Железо — два роутера MikroTik, одна гигабитная локальная сеть, 2026-09-01. RB3011UiAS (ARMv7, 2×1400 МГц) в роли отправителя, cAP ax (ARM64, 4×864 МГц) в роли получателя, RouterOS 7.24.1 на обоих. Наше ядро работало как fv, в контейнере RouterOS, в пользовательском пространстве, против собственной реализации протокола WireGuard прямо в ядре ОС роутера — единственный доступный на этом железе сравнимый вариант «на равных», поскольку это единственная альтернатива, которая поставляется прямо в составе RouterOS. Два генератора, потому что ни один не переносится между реализациями чисто: dd | nc для нашего прогона в контейнере, собственный bandwidth-test от RouterOS для прогона в ядре — каждый несёт свой собственный потолок без туннеля как контрольную точку, чтобы читатель мог проверить потолок, прежде чем доверять соотношению.

Лаборатория — четыре гостевые системы aarch64 Debian под QEMU/HVF на одном хосте с Apple Silicon, 2026-08-31. Одни и те же пары ключей, адреса, порты, MTU и поднятие интерфейса для каждого бинарника под тестом: наше ядро, boringtun 0.7 (пользовательское ядро — референс протокола WireGuard в пользовательском пространстве, от которого ответвилось наше), wireguard-go (референсная реализация протокола WireGuard, пользовательское пространство) и собственный модуль WireGuard ядра Linux. 15-секундные прогоны iperf3, по три замера на каждый, оба ядра на Rust закреплены за одним потоком датапейна по причине, указанной в примечании выше.

task test:impls    # lab: ours vs boringtun vs wireguard-go vs the kernel
task test:all      # lab: single-hop, multi-hop, and the comparison

Прогон на роутере пока не оформлен как цель task — это fv, запущенный вручную в контейнере RouterOS на каждом устройстве, против встроенного в RouterOS сервера протокола WireGuard. Полная топология и каждая необработанная строка лежат в нашем собственном docs/benchmarks/results.csv.

Число, которое имеет значение: на роутере

Показательнаше ядро (пользовательское пространство)собственный туннель ядра роутера
пропускная способность122 Мбит/с66,8 Мбит/с
CPU, отправитель65% (63–67)74% (70–79)
CPU, получатель18% (10–22)20% (18–21)
CPU на Мбит0,53 %/Мбит1,10 %/Мбит
RTT1,22 мс0,92 мс
резидентная память (отправитель / получатель)3,0 / 3,1 МБв ядре, неотделима
генераторdd | ncbandwidth-test
потолок этого генератора без туннеля187 Мбит/с220,5 Мбит/с
сохранено от собственного потолка65%30%
наше ядро122 Мбит/стуннель ядра роутера66,8 Мбит/с

Длина полосы ∝ Мбит/с, ось 0–140. Один и тот же отправитель RB3011 в обоих прогонах; именно он — потолок по CPU на протяжении всего теста, 65–79%, пока принимающий cAP ax простаивает у отметки около 20%.

Две независимые схемы согласуются, что и делает результат заслуживающим доверия, а не подобранным: абсолютная пропускная способность выше у нашего ядра в 1,8×, а доля, сохранённая от собственного неоспоримого потолка каждого генератора, — в 2,2×. Чего таблица не говорит: два генератора — не один и тот же инструмент, поэтому показаны оба потолка без туннеля, а не один заголовочный коэффициент, и обе цифры изменились бы, если бы отправитель принимал настоящий сетевой трафик, а не генерировал его локально. В отличие от лабораторной таблицы ниже, это единичные прогоны: проценты в скобках — это диапазон, в котором колебалась загрузка CPU за время этого прогона, а не разброс по повторным испытаниям.

Лаборатория: три ядра в пользовательском пространстве, одно в ядре ОС, потоки уравнены

ядротипTCP Мбит/с (диапазон, n=3)пиковая памятьзадержка p99
наше, 1 потокпользовательское пространство, Rust1535 (1506–1554)3,5 МБ0,94 мс
boringtun, 1 потокпользовательское пространство, Rust1427 (1346–1479)5,3 МБ1,15 мс
wireguard-go, по умолчаниюпользовательское пространство, Go1228 (1221–1239)34,8 МБ1,39 мс
WireGuard ядра ОС, по умолчаниюядро, C1376 (1366–1386)— (ядро, неотделимо)0,96 мс

Как поставляется, без выравнивания: наше ядро по умолчанию использует 1 поток и выдало 1535 Мбит/с; boringtun по умолчанию использует 4 потока и выдал при такой настройке ~789 Мбит/с. Сообщить только цифру «по умолчанию против по умолчанию» значило бы повторить ту же ошибку, только в выгодном для нас направлении. Обе цифры реальны; но только одна из них изолирует датапас от количества потоков.

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

usersysнаше ядро0,90 с/Гбит итогоuser 0,347 · sys 0,549 с/Гбитboringtun1,08 с/Гбит итогоuser 0,290 · sys 0,787 с/Гбитwireguard-go2,37 с/Гбит итогоuser 0,876 · sys 1,490 с/Гбит

Меньше — лучше. Ось 0–2,5 с/Гбит, линейная. Оба сегмента складываются слева направо; разбивка показывает, откуда берётся потеря, о которой пойдёт речь в следующем разделе.

наше ядро3,5 МБboringtun5,3 МБwireguard-go34,8 МБ

Логарифмическая шкала, 1–50 МБ, указано отдельно, потому что это нечестный вариант по умолчанию: на линейной шкале 34,8 МБ у wireguard-go свели бы два других столбца к тонким линиям.

Где мы не победили

Два места, оба объяснены, а не спрятаны в CSV:

CPU в пользовательском режиме, в лаборатории. 0,347 с/Гбит против 0,290 у более дешёвого компаратора на Rust — единственный столбец в этой таблице, где он опережает нас. Мы читаем это так, что это куплено, а не потрачено впустую: наше ядро делает офлоад контрольной суммы TUN в обоих направлениях, что переносит работу из ядра ОС в пользовательский CPU, учитываемый как время user — а колонка системного CPU, где эта сделка окупается, у нас заметно впереди (0,549 против 0,787). Итоговый CPU всё равно побеждает у нас. Разбивка user/sys выше показана специально, чтобы эта сделка была видна, а не просто заявлена.

Время кругового пути, на роутере. 1,22 мс против 0,92 мс у туннеля в ядре ОС — единственная ось на железе, где встроенное в ядро решение побеждает безоговорочно, по обычной причине: наш путь платит за переход через veth контейнера и переключение планировщика в пользовательском пространстве, за которые путь внутри ядра не платит. Любопытно, что в лаборатории обратный порядок по задержке p99 (0,94 мс против 0,96 мс у ядра) — мы не делаем из этого никаких выводов, кроме «разные среды, разные узкие места», и говорим об этом прямо, вместо того чтобы выбирать цифру, которая выгоднее.

Что свежее, а что унаследовано

У каждой цифры выше есть дата, и даты важны: таблица с роутера — от 2026-09-01, лабораторная таблица — от 2026-08-31. С тех пор скоординированное крупное обновление версий переместило криптографические крейты, на которые наш дата-плейн опирается на каждом пакете, — chacha20poly1305, blake2, curve25519-dalek, — и мы не перезапускали полное лабораторное или роутерное сравнение на этом дереве. Цифры выше реальны и были измерены аккуратно, но на момент публикации им шесть дней, и они относятся к дереву, которое предшествует этому обновлению. Мы публикуем их с этой оговоркой, а не молча, и укажем дату, когда появится повторный прогон. Цифра с роутера также не привязана в наших собственных записях к конкретному коммиту: читайте «2026-09-01» как «начало сентября», а не как привязку к одному конкретному изменению оптимизации.

Что мы могли бы перезапустить прямо сегодня, на этой машине, без роутера или лаборатории с ВМ, — это часть пути, которую обновление крипто затронуло сильнее всего: стоимость инкапсуляции и декапсуляции, изолированная от любых сокетов, TUN и вопросов многопоточности выше по стеку.

операция, размер пакетанаше ядроboringtun 0.7.1
инкапсуляция, 64 Б219,2 нс218,7 нс
инкапсуляция, 512 Б436,4 нс442,5 нс
инкапсуляция, 1420 Б938,1 нс968,3 нс
декапсуляция, 64 Б238,8 нс226,7 нс
декапсуляция, 512 Б562,1 нс539,2 нс
декапсуляция, 1420 Б1099,5 нс1069,4 нс

Повторный прогон от 2026-09-05, Apple M3 Max, cargo bench -p forestvpn-quantum --bench datapath_benches -- --measurement-time 3 --warm-up-time 1, criterion 0.8.2, 100 замеров на точку. Ни сокета, ни TUN, ни потоков — одно завершённое рукопожатие, затем инкапсуляция/декапсуляция в цикле. Результаты неоднородны по направлению и размеру пакета, всё в пределах нескольких процентов в обе стороны, что и ожидаемо: наше ядро ответвилось от вышестоящего проекта именно в этой части пути, и оба сейчас сидят на одном поколении примитивов RustCrypto. Это не заявление о пропускной способности — оно говорит, что форкнутый криптографический путь сам по себе стоит примерно столько же, сколько и у вышестоящего проекта, в пределах шума, а это и есть то, что делает лабораторные и роутерные победы выше заслуживающими доверия: они берутся из другого места в стеке (батчинг системных вызовов, офлоад, многопоточность), а не из более быстрого шифра.

Как воспроизвести

task test:impls          # lab: ours vs boringtun vs wireguard-go vs the kernel
task test:all            # lab: single-hop, multi-hop, and the multihop cost table
cargo bench -p forestvpn-quantum --bench datapath_benches -- --measurement-time 3 --warm-up-time 1

Прогон на роутере сегодня выполняется вручную: fv в контейнере RouterOS на каждом устройстве, против собственного сервера протокола WireGuard в RouterOS.

Честно говоря о том, что это даёт: харнесс и необработанный results.csv за каждой таблицей здесь живут в нашем дереве, которое сегодня не публично. Читайте команды выше как точное описание того, что мы запускали, а не как то, что можно склонировать и повторить. Утверждение, меньше всего зависящее от нас, — оно же и самое стоящее проверки: boringtun и wireguard-go публичны, iperf3 публичен, а ловушка с количеством потоков в начале этого поста (один флаг, двигающий компаратор между примерно 789 и 1427 Мбит/с на одном хосте) воспроизводима кем угодно с этими тремя инструментами и без единой строчки нашего кода в цепочке. Если у вас это подтвердится — уравненные строки выше как раз те, с которыми стоит спорить. Если нет — мы хотим об этом узнать.

Все посты

Русский
Спросите нас о чём угодно