MAATRIX / Блог / Предел скорости VPN на одном ядре: где шифрование упирается в частоту процессора

Предел скорости VPN на одном ядре: где шифрование упирается в частоту процессора

MAATRIX

Вы арендовали канал на гигабит, а VPN упрямо выдаёт в разы меньше — и сколько бы вы ни докидывали полосы, ни меняли тарифный план, цифра не двигается. Дело почти никогда не в сети: один поток шифрования трафика прижат к одному ядру процессора, и именно его частота, а не ширина канала, определяет потолок туннеля. Разберём, почему так устроено, какую роль играет AES-NI, чем однопоточные реализации отличаются от многопоточных и как измерить свой личный предел, прежде чем менять протокол или сервер.

Почему потолок — это ядро, а не канал

VPN-туннель — это не просто маршрутизация пакетов, это ещё и криптографическая обработка каждого байта: шифрование, проверка целостности, инкремент счётчика/nonce, иногда пересборка фрагментов. Для одного логического потока трафика (один туннель между двумя точками, одна SA, одно TCP/UDP-соединение) эта обработка почти всегда должна идти строго по порядку — счётчик пакетов и состояние шифра нельзя обсчитывать параллельно на разных ядрах без риска перепутать порядок или сломать целостность. Поэтому большинство реализаций закрепляют обработку одного потока за одним потоком выполнения (thread), а тот, в свою очередь, ОС планирует на одно ядро в конкретный момент времени.

Отсюда парадокс, который путает многих: сетевая карта поддерживает 10G, канал у провайдера честный гигабит, диск не при делах — а iperf3 через туннель упирается в цифру, которая пугающе точно совпадает с тем, сколько мегабайт в секунду это же ядро прогоняет через AES где-нибудь в openssl speed. Это не совпадение. Пропускная способность туннеля для одного потока равна количеству байт, которое одно ядро успевает зашифровать (плюс упаковать в пакеты, проверить, отправить) за секунду — минус накладные расходы на прерывания, копирования и системные вызовы.

Горизонтальное масштабирование (больше ядер) тут работает не автоматически — реализация должна уметь раскладывать нагрузку на несколько ядер, а это возможно только там, где есть что раскладывать: несколько независимых потоков, несколько SA, несколько клиентов. Один клиент, один туннель, один поток шифрования — это всегда один поток исполнения, и его предел определяется вертикальной характеристикой CPU: тактовой частотой одного ядра и эффективностью набора инструкций, а не количеством ядер в сервере.

AES-NI: что аппаратное ускорение даёт, а что нет

AES-NI — это набор инструкций x86 (Intel и AMD), которые выполняют раунды шифрования AES прямо в кремнии, а не программным циклом. Разница по сравнению с чисто программной реализацией AES ощутима — конкретную кратность не назову, она зависит от модели CPU, версии библиотеки шифрования и режима (GCM, CBC и т.д.), и её несложно проверить прямо на своём сервере:

# Есть ли инструкция в CPU
grep -m1 -o aes /proc/cpuinfo

# Список реализаций шифров, доступных ядру
cat /proc/crypto | grep -A5 aes

# Реальная скорость AES-256-GCM на одном ядре (однопоточный тест)
openssl speed -evp aes-256-gcm -elapsed

Последняя команда даёт число в байтах в секунду для конкретного размера блока — именно это число и есть ваш практический потолок для одного потока шифрования на этом CPU, переведённый в мегабиты умножением на 8. Если результат заметно ниже, чем ожидаете от современного CPU, — либо AES-NI не проброшен гипервизором в виртуалку (актуально для части бюджетных VPS и старых конфигураций KVM), либо тест запущен на не той версии OpenSSL, которая не использует движок ускорения. Подробнее о том, как AES-NI меняет нагрузку на CPU и как проверить её на своём сервере, разобрано в статье про AES-NI и нагрузку на CPU.

Важная оговорка: AES-NI ускоряет только сам шифр. Остальная часть обработки пакета — разбор заголовков, чек-суммы, контекстные переключения между ядром ОС и процессом VPN, системные вызовы на каждый пакет — никуда не девается и при мелких пакетах (VoIP, игровой трафик, множество мелких TCP-сегментов) может стать даже более заметным ограничением, чем сам шифр. Поэтому «есть AES-NI» не равно «упора в CPU не будет» — это лишь означает, что сам шифр перестал быть узким местом, а всё остальное вокруг него — нет.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Однопоточные и многопоточные реализации: в чём разница

Тут удобно разделить протоколы на два лагеря не по названию, а по архитектуре обработки трафика.

Классическое пользовательское пространство, один воркер на соединение. Классический OpenVPN в типовой конфигурации обрабатывает данные одного клиентского соединения в одном процессе/потоке: пакет приходит в tun-интерфейс, процесс OpenVPN его читает, шифрует, отправляет в сокет — и весь этот путь линейный. Для одного клиента это означает жёсткую привязку к одному ядру. Для сервера с множеством клиентов ситуация чуть лучше: разные клиентские сессии в некоторых схемах развёртывания можно раскидать по нескольким процессам-инстансам OpenVPN, каждый на своём порту и, соответственно, своём ядре — но это требует ручной архитектуры (несколько инстансов + балансировка), а не работает «из коробки» для одного тяжёлого потока.

Обработка в пространстве ядра ОС и с раскладкой по очередям. WireGuard в linux-реализации работает как модуль ядра: путь пакета короче (меньше переключений между user space и kernel space), а при наличии нескольких пиров или нескольких потоков трафика ядро может распределять обработку по разным CPU через очереди приёма пакетов (multiqueue/RSS/RPS). Для одного пира это не отменяет последовательность шифрования по счётчику, но за счёт меньшего оверхеда на пакет практический предел на одно ядро у WireGuard, как правило, выше, чем у классического userspace OpenVPN — при прочих равных CPU и одинаковом шифре.

IPsec с несколькими SA. У IPsec (strongSwan, Libreswan, встроенные реализации на роутерах) единица параллелизма — Security Association. Один туннель с одним ключевым материалом — это, опять же, один логический поток. Но конфигурация с несколькими child SA (несколько подсетей, несколько политик на один или разные gateway) даёт несколько независимых потоков шифрования, которые ядро вполне может развести по разным CPU — это особенно заметно на site-to-site сценариях с большим числом туннелей.

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

АрхитектураЕдиница параллелизмаТипичный потолок на 1 поток
OpenVPN (userspace, классика)Процесс/поток на клиентаНиже — сказывается оверхед user space, syscalls
WireGuard (kernel-space)Пир / очередь ядраВыше — короче путь пакета, меньше переключений контекста
IPsec, одна SASecurity AssociationСопоставимо с WireGuard при аппаратном ускорении
IPsec, много SAКаждая SA отдельноСуммарно растёт с числом ядер — если SA разведены по очередям

Где ещё теряются такты помимо самого шифра

Упор в одно ядро редко бывает «чистым» упором именно в криптографию — обычно к шифрованию добавляются соседи по тому же ядру.

Первый частый сосед — обработка прерываний от сетевой карты. Если все прерывания NIC (или единственная очередь RX/TX) закреплены на том же ядре, где крутится процесс шифрования, они начинают конкурировать за такты: SoftIRQ на приём пакетов и crypto-нагрузка делят одно и то же ядро, и по метрикам это выглядит как «CPU 100% на одном ядре при в целом свободном сервере». Это отдельная и довольно частая проблема сама по себе — как выглядит и как диагностируется забитое прерываниями ядро, подробно разобрано в статье про ksoftirqd на 100% из-за прерываний на одном ядре. Проверить распределение прерываний по ядрам можно так:

cat /proc/interrupts | grep -i eth   # или имя вашего сетевого интерфейса
ethtool -l eth0                       # число очередей RX/TX
mpstat -P ALL 1                       # загрузка каждого ядра отдельно, раз в секунду

Второй частый сосед — размер пакетов. Накладные расходы на пакет (разбор заголовков, копирование между буферами, системные вызовы) примерно постоянны вне зависимости от размера самого пакета. Значит, трафик с мелкими пакетами (голос, игры, множество мелких TCP-сегментов при плохом MTU) съедает больше тактов на мегабит, чем поток из крупных пакетов при том же битрейте. Если у вас упор в ядро при трафике, который явно состоит из мелких пакетов, часть проблемы — не в самом шифре, а в PPS (пакетов в секунду), и здесь может помочь не смена протокола, а подбор MTU и включение агрегации/оффлоада на сетевой карте, где это применимо.

Третий сосед — режим энергосбережения CPU. На части серверов и виртуалок governor выставлен в powersave, из-за чего ядро не разгоняется до максимальной частоты под нагрузкой так быстро или так высоко, как могло бы, и намеренно занижает ваш реальный потолок:

cpupower frequency-info
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

Если governor не performance — это первое, что стоит проверить перед выводами о «протокол медленный по своей природе».

Как измерить свой предел на одном ядре

Прежде чем менять архитектуру, стоит убедиться, что вы действительно упираетесь в CPU, а не в что-то ещё (канал, диск на стороне логирования, MTU/фрагментацию). Порядок действий:

  1. Замерьте теоретический потолок шифра на одном ядре. openssl speed -evp aes-256-gcm -elapsed (или chacha20-poly1305 — это шифр WireGuard по умолчанию) даёт число мегабайт в секунду, которое одно ядро способно обработать без сетевого и системного оверхеда сверху. Это верхняя граница — реальная скорость туннеля будет ниже.
  1. Замерьте реальную скорость туннеля под нагрузкой. Классический инструмент — iperf3 через поднятый туннель, с клиента к серверу и в обратную сторону отдельно (шифрование симметрично, но профили нагрузки на CPU клиента и сервера разные). Методика измерения и типичные ошибки при таком тесте разобраны в статье измерение скорости VPN через iperf3.
  1. Смотрите на загрузку CPU по ядрам, а не в среднем. mpstat -P ALL 1 или htop с включённым отображением по ядрам (клавиша t/раздельные полосы) во время теста. Если одно ядро уходит в 100%, а остальные простаивают — это прямое подтверждение, что вы упёрлись именно в однопоточную обработку, а не в канал или диск.
  1. Найдите, какое именно ядро занято, и что на нём висит. Для процесса в user space (OpenVPN) это top -H -p $(pidof openvpn) для потоков и taskset -pc <pid>, чтобы увидеть привязку к CPU. Для WireGuard как модуля ядра полезнее смотреть mpstat -P ALL в связке с cat /proc/interrupts, потому что нагрузка размазана по softirq, а не по одному видимому процессу.
  1. Сравните три числа. Теоретический потолок шифра на ядре (шаг 1), реальную скорость туннеля (шаг 2) и загрузку ядер (шаг 3). Если реальная скорость туннеля близка к теоретическому потолку шифра, а одно ядро при этом забито под завязку — вывод однозначен: вы упёрлись в CPU одного ядра, и добавление канала или смена тарифа с большей полосой ничего не даст.

Когда и как переходить на другую архитектуру

Если измерения подтвердили упор именно в одно ядро, варианты зависят от того, что именно вы масштабируете.

Один тяжёлый поток от одного клиента. Здесь честный ответ не всегда приятный: для одного логического потока шифрования нельзя обойти требование последовательной обработки, наращивая число ядер сервера — нужен более быстрый одиночный поток, то есть CPU с более высокой частотой на ядро (или более новой микроархитектурой с более эффективным исполнением AES-NI/AVX). Более дешёвый обходной путь — убедиться, что вы используете наиболее лёгкую для CPU реализацию: переход с классического userspace OpenVPN на WireGuard часто даёт заметный запас по потолку на том же железе за счёт меньшего оверхеда пути пакета — что выбрать под конкретный сценарий, разобрано в статье какой протокол VPN выбрать: гид по сценариям.

Много клиентов или много туннелей. Тут ситуация принципиально другая: у вас есть что раскладывать по ядрам. Для OpenVPN это означает несколько инстансов процесса (разные порты, разные конфиги) с ручным распределением клиентов между ними. Для WireGuard — несколько пиров на одном интерфейсе обычно и так неплохо размазываются по CPU через очереди ядра, но стоит проверить, что у сетевой карты включено несколько RX/TX очередей (ethtool -l) и что irqbalance или ручная настройка RPS/RSS действительно раскладывает прерывания, а не держит их на одном ядре. Для IPsec — дробление на несколько child SA там, где это осмысленно логически (разные подсети, разные политики), даёт естественный параллелизм.

Шифр без AES-NI. Если сервер (часто ARM или бюджетная виртуалка без проброшенного флага) не имеет аппаратного ускорения AES, стоит проверить ChaCha20-Poly1305 — на CPU без AES-NI он нередко эффективнее программного AES, именно поэтому это шифр по умолчанию в WireGuard. Но переключение шифра не убирает саму последовательность обработки по одному ядру — оно лишь поднимает потолок этого ядра.

Виртуализация. На VPS отдельно стоит убедиться, что AES-NI вообще виден гостевой системе (grep aes /proc/cpuinfo внутри виртуалки) — часть облачных площадок и старых гипервизоров не пробрасывает этот флаг по умолчанию, и тогда даже мощный физический CPU хоста не спасает конкретную виртуальную машину.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

У меня канал 1 Гбит/с, а VPN не превышает несколько сотен мегабит — почему?

Почти наверняка вы упёрлись в одно ядро CPU, обрабатывающее шифрование одного потока. Проверьте загрузку по ядрам через mpstat -P ALL 1 во время теста — если одно ядро на 100%, а остальные свободны, это подтверждение.

Точно ли на моём сервере есть AES-NI?

Выполните grep -m1 -o aes /proc/cpuinfo. На физическом сервере это почти всегда так на любом современном CPU, на виртуалке зависит от того, пробросил ли хостер соответствующий флаг гостевой системе.

WireGuard всегда быстрее OpenVPN на одном ядре?

Как правило да, за счёт архитектуры в пространстве ядра и меньшего оверхеда на пакет, но это не универсальный закон — конкретная разница зависит от версий, настроек MTU, размера пакетов и самого CPU. Проверяйте на своём железе, а не полагайтесь на общие утверждения.

Можно ли распараллелить один туннель между двумя точками на несколько ядер?

Для одного непрерывного потока — как правило нет, порядок обработки должен сохраняться. Обходной путь — поднять несколько независимых туннелей параллельно и вручную балансировать трафик между ними на уровне приложения, но это усложняет схему и оправдано только при действительно больших объёмах.

ChaCha20 вместо AES решит проблему упора в ядро?

Сама проблема серилизации по одному ядру никуда не денется, но если AES-NI недоступен, ChaCha20-Poly1305 обычно даёт более высокий потолок на то же ядро, чем программный AES — то есть проблему не убирает, а отодвигает.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →