Пропускна здатність на цикл 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/s | 66.8 Mbit/s |
| CPU, відправник | 65% (63–67) | 74% (70–79) |
| CPU, отримувач | 18% (10–22) | 20% (18–21) |
| CPU на Mbit | 0.53 %/Mbit | 1.10 %/Mbit |
| RTT | 1.22 ms | 0.92 ms |
| резидентна пам'ять (відправник / отримувач) | 3.0 / 3.1 MB | у ядрі, невіддільно |
| генератор | dd | nc | bandwidth-test |
| стеля цього генератора без тунелю | 187 Mbit/s | 220.5 Mbit/s |
| утримано відносно власної стелі | 65% | 30% |
Довжина смуги ∝ Mbit/s, вісь 0–140. Той самий відправник RB3011 в обох прогонах; саме він є стелею CPU протягом усього тесту, на рівні 65–79%, тоді як приймальний cAP ax простоює біля 20%.
Два незалежних кути зору узгоджуються, а це і робить результат правдоподібним, а не підігнаним: абсолютна пропускна здатність на нашу користь у 1.8×, а пропускна здатність, утримана відносно власної неоспорюваної стелі кожного генератора, — на нашу користь у 2.2×. Чого таблиця не каже: два генератори — не один і той самий інструмент, тому друковані обидві стелі без тунелю, а не одне підсумкове співвідношення, і обидві цифри змінилися б, якби відправник приймав реальний мережевий трафік, а не генерував його локально. На відміну від лабораторної таблиці нижче, це поодинокі прогони: відсотки в дужках — це діапазон, у якому блукало навантаження CPU під час цього прогону, а не розкид за кількома повторами.
Лабораторія: три ядра в користувацькому просторі, одне в ядрі системи, вирівняні потоки
| ядро | тип | TCP Mbit/s (діапазон, n=3) | пікова пам'ять | затримка p99 |
|---|---|---|---|---|
| наше, 1 потік | userspace, Rust | 1535 (1506–1554) | 3.5 MB | 0.94 ms |
| boringtun, 1 потік | userspace, Rust | 1427 (1346–1479) | 5.3 MB | 1.15 ms |
| wireguard-go, за замовчуванням | userspace, Go | 1228 (1221–1239) | 34.8 MB | 1.39 ms |
| kernel WireGuard, за замовчуванням | kernel, C | 1376 (1366–1386) | — (ядро, невіддільно) | 0.96 ms |
Як постачається, без вирівнювання: наше ядро за замовчуванням використовує 1 потік і показало 1535 Mbit/s; boringtun за замовчуванням використовує 4 потоки і за такого налаштування показав ~789 Mbit/s. Наведення лише числа «за замовчуванням проти за замовчуванням» повторило б ту саму помилку у вигідному напрямку. Обидва числа реальні; лише одне з них відокремлює дата-план від кількості потоків.
Вартість CPU — це вісь, заради якої лабораторія займає своє місце в цьому дописі: це та єдина вісь, яку таблиця з роутера показати не може, бо власний компаратор роутера в ядрі не дає розкладки CPU для порівняння.
Менше — краще. Вісь 0–2.5 s/Gbit, лінійна. Обидва сегменти складаються зліва направо; місце розрізу — те, де проявляється втрата з наступного розділу.
Логарифмічна шкала, 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 B | 219.2 ns | 218.7 ns |
| інкапсуляція, 512 B | 436.4 ns | 442.5 ns |
| інкапсуляція, 1420 B | 938.1 ns | 968.3 ns |
| декапсуляція, 64 B | 238.8 ns | 226.7 ns |
| декапсуляція, 512 B | 562.1 ns | 539.2 ns |
| декапсуляція, 1420 B | 1099.5 ns | 1069.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 на одному хості) відтворювана будь-ким із цими трьома і без жодного рядка нашого коду в цьому циклі. Якщо це підтвердиться і у вас — вирівняні рядки вище варті обговорення. Якщо ні — ми хочемо про це почути.