Блог

Пропускна здатність на цикл CPU: число, яке витримує зіткнення з роутером

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

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

На двох фізичних роутерах, під час передавання реального трафіку через реальну гігабітну локальну мережу, ядро нашого тунелю показало 122 Mbit/s там, де власна реалізація тунелю в ядрі роутера показала 66.8 Mbit/s — на тому самому відправнику, при меншому навантаженні CPU на мегабіт. Жодна з двох цифр сама собою не розповідає всієї історії: обом плечам довелося використовувати різні генератори трафіку, бо жоден єдиний генератор не працює на обох сторонах цього порівняння. Тож показник, вартий обговорення, — це співвідношення кожного плеча до власної стелі без тунелю, наведене в таблиці нижче, і воно на нашу користь ще більше, а не менше. Лабораторний прогін на чотирьох ВМ на ноутбуці показує щось швидше в абсолютних цифрах і, за однією віссю, — щось менш вигідне для нас. Ми більше довіряємо числу з роутера, бо саме роутер — єдиний із двох, крізь який справді проходить трафік клієнта.

Це і є висновок, вартий того, щоб з нього починати, і причина, чому в цьому дописі два розділи з результатами замість одного: лабораторія та апаратура розходяться в тому, яка вісь важливіша, а замовчування цього розходження коштувало б довіри більше, ніж виграють будь-які цифри.

Прочитайте це перед цифрами. Наш дата-план за замовчуванням використовує один потік. Компаратор, який ми запускали поруч у лабораторії, за замовчуванням використовує чотири. Без вирівнювання сама лише ця опція змінює пропускну здатність приблизно з 789 до 1427 Mbit/s — розрив у налаштуваннях за замовчуванням, а не в дата-планах, — і це вже одного разу спричинило публікацію хибного числа тут. Кожна лабораторна цифра нижче закріплює обидва ядра на Rust за одним потоком. Реалізації на Go та в ядрі системи в цій таблиці працювали за власними налаштуваннями за замовчуванням, які використовують більше одного потоку — якщо це й впливає, то на їхню користь порівняно з нашим одним потоком. Розрив «як постачається за замовчуванням» показано окремо, один раз, щоб він не залишався прихованим через замовчування.

Що вимірювалося і на чому

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

Апаратура — два роутери MikroTik, одна гігабітна локальна мережа, 2026-09-01. RB3011UiAS (ARMv7, 2×1400 MHz) як відправник, cAP ax (ARM64, 4×864 MHz) як отримувач, RouterOS 7.24.1 на обох. Наше ядро працювало як fv, у контейнері RouterOS, у користувацькому просторі, проти власної реалізації WireGuard-протоколу в ядрі роутера — єдиного порівнюваного на рівних варіанту, доступного на цьому обладнанні, оскільки це єдина альтернатива, яка постачається в самій RouterOS. Два генератори, бо жоден не перетинає реалізації однаково чисто: dd | nc для запуску нашого контейнера, власний bandwidth-test RouterOS для запуску в ядрі — кожен несе власну стелю без тунелю як контроль, тож читач може перевірити стелю, перш ніж довіряти співвідношенню.

Лабораторія — чотири гостьові системи aarch64 Debian під QEMU/HVF на одному хості з Apple-кремнієм, 2026-08-31. Однакові пари ключів, адреси, порти, MTU та піднімання інтерфейсу для кожного бінарника під тестом: наше ядро, boringtun 0.7 (ядро в користувацькому просторі, з якого форкнули наше), 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 на кожному пристрої, проти вбудованого сервера WireGuard-протоколу RouterOS. Повна топологія та кожен сирий рядок лежать у нашому власному docs/benchmarks/results.csv.

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

Показникнаше ядро (userspace)власний тунель ядра роутера
пропускна здатність122 Mbit/s66.8 Mbit/s
CPU, відправник65% (63–67)74% (70–79)
CPU, отримувач18% (10–22)20% (18–21)
CPU на Mbit0.53 %/Mbit1.10 %/Mbit
RTT1.22 ms0.92 ms
резидентна пам'ять (відправник / отримувач)3.0 / 3.1 MBу ядрі, невіддільно
генераторdd | ncbandwidth-test
стеля цього генератора без тунелю187 Mbit/s220.5 Mbit/s
утримано відносно власної стелі65%30%
наше ядро122 Mbit/sтунель ядра роутера66.8 Mbit/s

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

Два незалежних кути зору узгоджуються, а це і робить результат правдоподібним, а не підігнаним: абсолютна пропускна здатність на нашу користь у 1.8×, а пропускна здатність, утримана відносно власної неоспорюваної стелі кожного генератора, — на нашу користь у 2.2×. Чого таблиця не каже: два генератори — не один і той самий інструмент, тому друковані обидві стелі без тунелю, а не одне підсумкове співвідношення, і обидві цифри змінилися б, якби відправник приймав реальний мережевий трафік, а не генерував його локально. На відміну від лабораторної таблиці нижче, це поодинокі прогони: відсотки в дужках — це діапазон, у якому блукало навантаження CPU під час цього прогону, а не розкид за кількома повторами.

Лабораторія: три ядра в користувацькому просторі, одне в ядрі системи, вирівняні потоки

ядротипTCP Mbit/s (діапазон, n=3)пікова пам'ятьзатримка p99
наше, 1 потікuserspace, Rust1535 (1506–1554)3.5 MB0.94 ms
boringtun, 1 потікuserspace, Rust1427 (1346–1479)5.3 MB1.15 ms
wireguard-go, за замовчуваннямuserspace, Go1228 (1221–1239)34.8 MB1.39 ms
kernel WireGuard, за замовчуваннямkernel, C1376 (1366–1386)— (ядро, невіддільно)0.96 ms

Як постачається, без вирівнювання: наше ядро за замовчуванням використовує 1 потік і показало 1535 Mbit/s; boringtun за замовчуванням використовує 4 потоки і за такого налаштування показав ~789 Mbit/s. Наведення лише числа «за замовчуванням проти за замовчуванням» повторило б ту саму помилку у вигідному напрямку. Обидва числа реальні; лише одне з них відокремлює дата-план від кількості потоків.

Вартість CPU — це вісь, заради якої лабораторія займає своє місце в цьому дописі: це та єдина вісь, яку таблиця з роутера показати не може, бо власний компаратор роутера в ядрі не дає розкладки CPU для порівняння.

usersysнаше ядро0.90 s/Gbit всьогоuser 0.347 · sys 0.549 s/Gbitboringtun1.08 s/Gbit всьогоuser 0.290 · sys 0.787 s/Gbitwireguard-go2.37 s/Gbit всьогоuser 0.876 · sys 1.490 s/Gbit

Менше — краще. Вісь 0–2.5 s/Gbit, лінійна. Обидва сегменти складаються зліва направо; місце розрізу — те, де проявляється втрата з наступного розділу.

наше ядро3.5 MBboringtun5.3 MBwireguard-go34.8 MB

Логарифмічна шкала, 1–50 MB, зазначено окремо, бо це не чесне значення за замовчуванням: на лінійній осі 34.8 MB у wireguard-go звели б інші дві смуги до тонких ліній.

Де ми не виграли

Два місця, і обидва пояснено, а не заховано в CSV:

Час CPU в користувацькому режимі, у лабораторії. 0.347 s/Gbit проти 0.290 у дешевшого компаратора на Rust — єдиний стовпець у цій таблиці, де він випереджає нас. Ми тлумачимо це так: це куплено, а не змарновано — наше ядро виконує офлоуд контрольної суми TUN в обох напрямках, що переносить роботу з ядра системи в CPU користувацького простору, врахований як час user — а стовпець системного CPU, де ця угода відпрацьовується, помітно попереду (0.549 проти 0.787). Загальний CPU все одно виграє. Розподіл user/sys вище показано саме для того, щоб цю угоду було видно, а не просто заявлено.

Час туди-й-назад, на роутері. 1.22 ms проти 0.92 ms у тунелю в ядрі — єдина апаратна вісь, де перемога в ядра беззаперечна, з тієї звичайної причини, що наш шлях коштує один перехід через veth контейнера й один цикл користувацького планування, за які тунелю в ядрі платити не треба. Цікаво, що лабораторія показує протилежний порядок за затримкою p99 (0.94 ms проти 0.96 ms у ядра) — ми не робимо з цього жодних висновків, крім «різні середовища, різні вузькі місця», і кажемо це прямо, замість того щоб обирати те число, яке вигідніше виглядає.

Що свіже, а що успадковане

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

Що ми могли б перезапустити вже сьогодні, на цій машині, без роутера чи лабораторії з ВМ, — це ту частину шляху, якої оновлення криптографії торкнулося найбезпосередніше: вартість інкапсуляції та декапсуляції, ізольовано від будь-якого сокета, TUN і питань потоків вище.

операція, розмір пакетанаше ядроboringtun 0.7.1
інкапсуляція, 64 B219.2 ns218.7 ns
інкапсуляція, 512 B436.4 ns442.5 ns
інкапсуляція, 1420 B938.1 ns968.3 ns
декапсуляція, 64 B238.8 ns226.7 ns
декапсуляція, 512 B562.1 ns539.2 ns
декапсуляція, 1420 B1099.5 ns1069.4 ns

Повторний прогін 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 Mbit/s на одному хості) відтворювана будь-ким із цими трьома і без жодного рядка нашого коду в цьому циклі. Якщо це підтвердиться і у вас — вирівняні рядки вище варті обговорення. Якщо ні — ми хочемо про це почути.

Усі дописи

Українська
Запитайте нас про будь-що