mistgate Документация
RU

Бенчмарки

Память, процессор, диск, время старта и нагрузка на подписки у Mistgate, 3x-ui, Remnawave и PasarGuard на одном небольшом сервере, а также тест, где панель, ядро VPN и трафик Hysteria2 работают на одной машине. Методика, версии и ограничения.

На этой странице

Коротко#

  • Главное отличие — память. Одна панель Mistgate при 0–200 пользователях держит 16–20 МиБ приватной памяти; у 3x-ui 68–74, у PasarGuard 262–265, у Remnawave 426–454. Вместе с ядром VPN и базой на одном сервере: 57 МиБ против 74, 297 и 761.
  • Пользователи и перезапуски туннель не трогают. Пока пользователей добавляли и удаляли, а панель перезапускали, работающий туннель Hysteria2 у Mistgate не потерял ни одного соединения. 3x-ui при смене пользователей перезапускает своё ядро Xray.
  • Скорость туннеля пока не утверждаем. Первый полный прогон с исправленным стендом шёл на машине, которую нагружали другие процессы (повторы одной и той же панели различаются до трёх раз), поэтому фаза B и «туннельная» часть фазы D не публикуются и перемеряются на выделенных ядрах (см. «Результаты: один сервер»). Прежние цифры (все четыре в пределах 17 % друг от друга) сняты с ошибкой стенда, описанной в «После исправлений», и отозваны.
  • Слабые места Mistgate. 29 мс (p95) для маленького запроса внутри нагруженного туннеля (на 1 vCPU — 67 мс) против примерно 2 мс у остальных оказались следствием стенда (см. «После исправлений»); остался ли разрыв при исправленном стенде, выяснит перемер туннеля, поэтому утверждений о задержке на странице пока нет. Подписка мимо кэша отдавалась медленно: 1 630 запросов в секунду на одном vCPU в изменённой сборке, p99 1,7 с. Три исправления по итогам профилирования вышли в v0.1.33 и примерно вдвое ускоряют этот путь, см. «После исправлений». Тысяча пользователей через API создаётся за 49 с против 24 с у 3x-ui и Remnawave, в основном из-за ограничения частоты запросов к админке (см. пояснение под таблицей «одного сервера»).
  • Пропускную способность подписок в штатной сборке не измеряли: одну ссылку она отдаёт не чаще 60 раз в час, поэтому тест показал лишь, как быстро панель отказывает.
  • Один настольный процессор, одна машина с WSL2, три повтора. Прежде чем цитировать любую цифру, прочитайте раздел Чего эти цифры не говорят.

Таблицы «только панель»: замеры 9 и 10 октября 2026 года (Mistgate v0.1.32). Таблицы «один сервер»: перемерены 12 октября 2026 года (Mistgate v0.1.33, исправленный стенд). Числа — медианы трёх повторов, в скобках минимум и максимум, если в ячейке не указано n=1. Всё работало на одной настольной машине внутри WSL2. Скрипты, compose-файлы и сведённые результаты лежат в репозитории, в каталоге tools/bench.

Что измеряли#

Четыре панели, каждая установлена так, как описывает её документация, с настройками по умолчанию:

Панель Версия Как установлена База данных Процессов в измеряемой группе
Mistgate v0.1.32 для таблиц «только панель», v0.1.33 для таблиц «один сервер» (собрана из тега, без ключа релиза) один статический бинарник, mistgate setup, mistgate serve SQLite 1
3x-ui v3.9.0 готовый образ ghcr.io/mhsanaei/3x-ui:v3.9.0, compose-файл проекта SQLite 3: x-ui, ядро Xray под его управлением, fail2ban
Remnawave панель 3.4.5 официальный docker-compose-prod.yml (backend, Postgres 18.4, Valkey 9), версия образа зафиксирована PostgreSQL 22 в 3 контейнерах
PasarGuard v5.4.1 compose-файл проекта по умолчанию (SQLite) SQLite 1 (Python)

Два сценария:

  1. Только панель: небольшой хост для панели, 1 vCPU и 1 ГБ RAM без swap, без VPN-трафика. Память и процессор в простое, диск, время старта, создание 200 пользователей через API и нагрузка на подписки (50 клиентов, 60 с, 200 пользователей).
  2. Один сервер: панель, ядро VPN (или нода) и база панели на одной машине с 2 vCPU и 2 ГБ RAM без swap, 1000 пользователей, у всех панелей Hysteria2.

Результаты: только панель (1 vCPU, 1 ГБ)#

Главный показатель памяти — приватная память (anon). Рядом дан memory.current: его показывают docker stats и systemctl status, но в нём есть страничный кэш, а кэш засчитывается панелям неравномерно (см. примечания).

Mistgate 3x-ui PasarGuard Remnawave
Диск: образ или бинарник (МБ) 62 462 462 1551
Диск: данные после 200 пользователей (МБ) 5.2 4.8 4.6 61
Простой, 0 пользователей: anon (МиБ) 16 (16-16) 68 (68-68) 262 (262-262) 426 (425-430)
Простой, 0 пользователей: memory.current (МиБ) 27 (17-29) 186 (185-186) 358 (295-358) 599 (595-746)
Простой, 200 пользователей: anon (МиБ) 20 (20-21) 74 (72-74) 265 (265-265) 454 (452-458)
Простой, 200 пользователей: memory.current (МиБ) 27 (26-29) 154 (153-155) 299 (299-303) 644 (629-649)
Простой, 200 пользователей: процессор (% одного vCPU) 0.07 0.40 0.15 3.09
Процессов / контейнеров 1 / 0 3 / 1 1 / 1 22 / 3
200 пользователей созданы через API (с) 0.3 1.9 2.3 1.2
Холодный старт, пустые данные (с) 0.33 (0.25-0.33) 2.12 (2.07-2.12) 11.6 (11.0-11.9) 14.2 (14.1-15.0)
Холодный старт, 200 пользователей на диске (с) 0.13 1.51 (1.46-1.92) 6.7 (6.5-7.0) 12.9 (12.5-13.1)
Нагрузка на подписки: anon во время прогона (МиБ) 27 (27-28) 85 (85-86) 280 870 (856-874)
Нагрузка на подписки: пик memory.current (МиБ) 35 (34-37) 177 325 1024 (предел)

Примечания к таблице:

  • Холодный старт — время от команды запуска до первого ответа HTTP 200 на основном порту панели. У 3x-ui порт подписок (2096) поднимается чуть позже основного; в двух из трёх повторов проверка этого порта сразу после перезапуска получила отказ, поэтому замера памяти после перезапуска в этих двух повторах нет. Остальные числа этих повторов полные.
  • Remnawave упирается в предел 1 ГБ: в трёх повторах cgroup насчитала от 47 000 до 131 000 событий max (аллокации, которым пришлось вытеснять память), OOM-убийств не было. В единственном повторе только с квотой (ниже) один воркер был убит. Приватная память Remnawave под нагрузкой, около 870 МиБ, настоящая, это не кэш. В группу входят Postgres, Valkey и три процесса Node.js.
  • В измеряемой группе 3x-ui есть его ядро Xray (anon около 9–12 МиБ) и fail2ban (около 12 МиБ). Если сравнивать только процесс панели, их нужно вычесть: сам x-ui занимает около 55 МиБ.
  • У PasarGuard и Remnawave в измеряемой группе не работало ядро VPN (нода не подключена), у Mistgate не было подключённой ноды (агент, нужный один раз, чтобы подписка перестала быть пустой, удаляется до замера; запись о ноде остаётся).
  • Второй проход со сбросом страничного кэша перед каждым запуском (drop_caches, по одному повтору на панель) дал те же значения anon (Mistgate 16/20 МиБ, 3x-ui 62/75, PasarGuard 260/265, Remnawave 639/616 при 0/200 пользователей) и больший memory.current у Mistgate (30/38 МиБ): его бинарник тогда читается уже внутри измеряемой cgroup.

Нагрузка на подписки#

50 одновременных клиентов, 60 с, один и тот же User-Agent (v2rayN/7.5.0), 200 ссылок, keep-alive включён. Каждая панель возвращает одну ссылку на пользователя, в base64, тело ответа от 152 до 360 байт. Ссылки у панелей разные (Hysteria2 у Mistgate, VLESS Reality у 3x-ui, Shadowsocks у Remnawave и PasarGuard); см. примечания в конце.

Mistgate (штатная сборка) 3x-ui PasarGuard Remnawave
Запросов в секунду, статус 200 197 (ограничитель, см. ниже) 148 (147-153) 274 (268-284) 271 (166-307)
Задержка p50 / p95 / p99 (мс) 2.5 / 5.6 / 7.4 296 / 511 / 1230 166 / 378 / 519 113 / 230 / 1765
Ответов не 200 99.4 % 0 0 0
Средняя загрузка процессора (% одного vCPU) 98 41 100 99

Штатный Mistgate эту нагрузку по замыслу не обслуживает. Каждую ссылку подписки можно запросить 60 раз в час, на 61-й и следующие панель отвечает 429. В каждом повторе ровно 11 799 запросов получили 200 (по 60 на ссылку в час, минус один запрос прогрева на каждую ссылку и одна проверочная выдача перед нагрузкой) и от 1,6 до 2,0 млн получили 429. Путь ответа 429 дешёвый (от 27 000 до 34 000 ответов в секунду на одном vCPU), поэтому «запросов в секунду» в первой строке — это скорость отказов, а не раздачи. Результатом это не является. Лимит на адрес клиента у публичного листенера (10 запросов в секунду, всплеск 60) в этом прогоне ничего не решал: у каждой ссылки был свой адрес клиента через X-Forwarded-For за --trusted-proxy. Когда ограничение на ссылку отключили, этот лимит стал потолком, около 2 590 ответов в секунду (первый пробный прогон).

3x-ui использует 41 % одного vCPU при 148 запросах в секунду: его потолок — не процессор (он закрывает соединение после каждого ответа; причина не исследовалась).

Дополнительно: Mistgate без ограничений (изменённые сборки)#

Это не штатная сборка. В этих двух сборках изменены одна-две строки исходников v0.1.32 (diff: mistgate-bench-variants.diff), чтобы можно было сравнить стоимость запроса с другими панелями. Не цитируйте их как результат Mistgate.

без ограничений, кэш выдачи включён (nolimit) без ограничений, кэш выдачи выключен (nolimit-nocache)
Что изменено отключены лимит на ссылку и лимит на адрес то же, и кэш выдачи ссылки на 10 с отключён
Запросов в секунду, статус 200 19,580 (19,331-19,614) 1,630 (1,612-1,633)
p50 / p95 / p99 (мс) 2.4 / 5.4 / 7.2 4.2 / 6.6 / 1,702
Не 200 0 0
Средняя загрузка процессора 100 % 100 %

Без кэша каждый запрос читает базу (несколько запросов SQL, и SQLite разбирает каждый заново); с кэшем ссылка читается не чаще раза в 10 с. Профиль пути без кэша приведён в разделе Профилирование. P99 в 1,7 с на пути без кэша — это «голодание» небольшой доли запросов (около 25 в секунду, постоянно на протяжении прогона), а не пауза: при GOMAXPROCS=4 оно исчезает (p99 86 мс при той же пропускной способности).

Результаты: один сервер (2 vCPU, 2 ГБ, без swap), Hysteria2, 1000 пользователей#

Что у каждой панели работает внутри группы 2 vCPU / 2 ГБ:

Панель В группе
Mistgate панель (нативный процесс) и mistgate-node (контейнер)
3x-ui один контейнер: x-ui, ядро Xray, fail2ban
Remnawave backend, Postgres, Valkey и remnawave/node (внутри ядро Xray)
PasarGuard контейнер панели и pasarguard/node (внутри ядро Xray)

На каждом сервере самоподписанный сертификат, клиент его не проверяет (одинаково для всех). Управление перегрузкой — BBR с обеих сторон, полоса не объявлена, обфускации нет. Клиенты — hyload, небольшая программа на клиентской библиотеке Hysteria 2 (core/v2 v2.12.3); она работает вне группы, без ограничения по процессору, в отдельной cgroup, и занимала 150–340 % одного vCPU из 16 доступных, пока группа упиралась в свою квоту 200 %. Каждому контейнеру с ядром (нода Mistgate, ядра Xray остальных трёх) задан GOMAXPROCS, равный числу vCPU группы: так выбирает среда Go на настоящем сервере с таким числом vCPU; без этого контейнер выбирает GOMAXPROCS по 16 процессорам хоста (см. «После исправлений»). Официальным бинарником hysteria v2.13.0 проверили, что туннель работает (1,75 ГБ за 5 с).

Фазы: A — простой с 1000 пользователей (60 с на успокоение, окно 120 с); B — один туннель с 1, 16 и 64 параллельными загрузками (по 30 с) и маленьким запросом через тот же туннель каждые 100 мс (измерена, но не публикуется, см. ниже); C — 400 клиентов подключаются разом, маленький запрос каждые 2 с (60 с); D — «плохой день» на 120 с (64 потока, шквал запросов подписки 300 в секунду с 1000 адресов-источников, один пользователь добавляется и один удаляется в секунду через админский API); E — перезапуск панели (не ядра) на 20-й секунде 60-секундного прогона с 64 потоками.

Медианы трёх повторов, в скобках минимум и максимум. «Приватная память» — anon плюс память ядра для всей группы.

Чего нет в таблице. Фаза B (один туннель с 1, 16 и 64 потоками), «туннельная» половина фазы D (скорость, падение относительно B, маленький запрос через туннель) и скорость в фазе E убраны намеренно. На исправленном стенде одна и та же панель при 64 потоках показала три повтора подряд от 1,5 до 4,2 Гбит/с, и два самых медленных прогона совпали с тем, что клиент получил только 177–185 % процессора вместо примерно 320 %: другая работа на той же машине отнимала процессор у генератора нагрузки. Это помеха, а не свойство панелей, поэтому рейтинга скорости передачи нет, пока её не перемерят с группой на двух выделенных физических ядрах и клиентами на остальных. Цифры первой серии (все четыре в пределах 17 %) сняты до исправления стенда и отозваны. Всё ниже от скорости туннеля не зависит.

Mistgate 3x-ui Remnawave PasarGuard
A приватная память всей группы (МиБ) 57 (56-58) 74 (72-77) 761 (710-772) 297 (297-299)
A: memory.current (МиБ) 64 (62-64) 81 (79-84) 841 (790-852) 312 (312-315)
A: процессор (% одного vCPU, из 200) 0.26 0.68 4.34 0.39
A: процессов / контейнеров 2 / 1 3 / 1 36 / 4 3 / 2
1000 пользователей созданы через API (с) 49 (33-49) 24 (19-27) 24 (23-26) 45 (44-45)
C 400 клиентов: подключено / сбоев рукопожатия 400 / 0 400 / 0 400 / 0 400 / 0
C: рукопожатие p95 (мс) / запрос p95 (мс) 4 / 1.3 4 / 1.2 3 / 1.2 6 / 1.6
C: процессор группы (% одного vCPU) / приватная память (МиБ) 18 / 113 18 / 137 21 / 691 21 / 361
D оборвано потоков / переподключений туннеля 0 / 0 512 (384-523) / 6 (4-9) 0 / 0 0 / 0
D: шквал подписок обслужен (запросов/с из 300), только 200 300 94 (92-96) 250 (213-282) 51 (49-57)
D: подписка p95 (мс) 1 792 (791-816) 288 (211-308) 1814 (1522-1954)
D: админ: добавление пользователя p95 / удаление p95 (мс) 4 / 3 801 / 1138 51 / 83 2039 / 2216
D: админ: вызов списка p95 (мс) 4 502 79 1609
D: приватная память (МиБ) 80 129 1114 (1060-1130) 347
E перезапуск панели: админка снова отвечает через (с) 0.2 1.6 (1.5-1.6) 14.5 (11.1-15.4) 8.3 (7.4-11.7)
E: секунд без трафика в туннеле (из 60) / оборвано потоков 0 / 0 5 (4-8) / 256 (192-256) 0 / 0 8 (5-9) / 128 (128-192)
Перезапуск ядра при смене пользователей нет да нет нет

Как читать таблицу:

  • Память (A): 57 МиБ у панели и ноды Mistgate против 74, 297 и 761 МиБ. Remnawave — это 36 процессов в 4 контейнерах (backend с тремя воркерами Node.js, Postgres, Valkey, нода).
  • Создание пользователей: скрипт создаёт их по одному запросу к API и пережидает ответы 429. Админка Mistgate пропускает 30 запросов в секунду с запасом в 200, поэтому первые 200 пользователей создаются за доли секунды (0,3 с в тесте одной панели), а остальные 800 идут в темпе ограничения. 49 секунд — в основном это ожидание, а не стоимость создания пользователя.
  • Много клиентов (C): 400 одновременных туннелей, сбоев нет ни у кого, не больше 21 % одного vCPU. На таком размере панели этим не различаются. Единственный заметный разброс — рукопожатие p95 у PasarGuard, 4–20 мс.
  • «Плохой день» (D): по туннелю идут 64 потока, в это время 300 запросов подписки в секунду приходят с 1000 адресов, и каждую секунду добавляется и удаляется по пользователю. 3x-ui перезапускает ядро Xray при смене пользователей (в медиане оборвано 512 потоков, единственный клиент переподключался 6 раз за 120 с); остальные три добавляют и удаляют пользователей в работающем ядре без обрыва соединений. 3x-ui при этом обслуживает шквал подписок со скоростью 94 запроса в секунду (предложено было 300) и тратит 0,8–1,1 с на каждое изменение пользователя; PasarGuard обслуживает 51 в секунду и тратит 2,0–2,2 с; Remnawave обслуживает 250 в секунду при p95 288 мс, а его память в D доходит до 1114 МиБ (три воркера Node.js на тех же двух vCPU, что и ядро Xray). Mistgate обслуживает все 300 в секунду при p95 1 мс и меняет пользователя за 3–4 мс.
  • Перезапуск (E): перезапуск контейнера панели 3x-ui перезапускает его Xray (так устроено: ядро — дочерний процесс панели). Перезапуск контейнера панели PasarGuard тоже прервал туннель на 5–9 с: ядро ноды перезапускается, когда панель возвращается (наблюдение, механизм не исследован). Перезапуск backend Remnawave туннель не затронул, но админский API отвечал снова только через 11–15 с. Панель Mistgate перезапускается за 0,2 с, а её нода продолжает работать.
  • В фазе D группа упирается в квоту (183–197 % одного vCPU у всех четырёх), поэтому строки управляющей части сравнивают, сколько ресурса остаётся у каждой панели на собственную работу в загруженном ящике.

Самый дешёвый сервер: 1 vCPU, 1 ГБ RAM, без swap (один повтор)#

Те же фазы A, B, D и E, группа ограничена CPUQuota=100% и MemoryMax=1G, Mistgate v0.1.33 и исправленный стенд (GOMAXPROCS=1 в ядрах), прогон 12 октября. По одному повтору на панель, разброс считайте неизвестным. Как и в таблице выше, строки о туннеле из B и D (скорость, маленький запрос через туннель) не публикуются: этот прогон тоже делил машину с другой нагрузкой, а по одному повтору помеху от свойства панели не отличить.

Mistgate 3x-ui Remnawave PasarGuard
A приватная память (МиБ) 73 75 820 302
D: оборвано потоков 0 384 0 0
D: шквал обслужен (запросов/с из 300) / не 200 300 / 0 % 74 / 0 % 100 / 31.2 % 39 / 0 %
D: подписка p95 (мс) 6 1002 496 2603
D: админ: добавление пользователя p95 (мс) 9 1340 97 2921
E: админка снова отвечает через (с) 0.3 1.5 15.2 7.3
E: секунд без трафика в туннеле 0 4 2 5

На сервере с 1 ГБ Remnawave не хватает памяти в фазе D: ядро убило воркеры rw-api пять раз и rw-processor один раз (OOM kill, cgroup bench-box.slice, посчитано по журналу ядра за этот повтор), 31,2 % запросов подписки завершились отказом или сбросом соединения, и не удались 23 вызова админки. В покое он уже занимает 820 МиБ. Памяти ему не добавляли.

Память Mistgate в простое на этом сервере, 73 МиБ, выше, чем 54 МиБ в первой серии и чем его 57 МиБ на сервере с 2 vCPU. Разница — в агенте ноды, который работает в той же измеряемой группе: к концу окна простоя у процесса панели было 44 МиБ приватной памяти и здесь, и в прогонах на 2 vCPU, у агента — 28 МиБ здесь против 10-12 МиБ там (дальше не исследовали). Память группы растёт первые минуты после создания 1000 пользователей (в прогоне на 2 vCPU с 45 до 63 МиБ за четыре минуты, в этом — с 61 до 78 МиБ; конец роста в обоих уже приходится на фазу B, с трафиком через туннель), поэтому цифра простоя зависит от того, куда попало окно; здесь на успокоение дали 30 с, в прогонах на 2 vCPU — 60 с. Доля панели в этом росте — её куча Go, которая возвращается к рабочему размеру: при настройке пароль владельца хэшируется argon2id (64 МиБ в v0.1.33, позже 19 МиБ), после чего панель разом отдаёт системе всю свободную память, и окно простоя начинается ниже установившегося уровня. Отдельный замер (только панель, 1000 пользователей и нода, 15 минут): за первые минуты панель поднялась с 34-36 МиБ примерно до 40 МиБ и дальше не росла, живая куча держалась на 11 МБ. Тот же замер нашёл настоящую трату: эндпоинт подписок хранил последнюю отрисованную подписку каждого пользователя, который её запрашивал, хотя переиспользует её только 10 с, — около 1,5 КБ кучи на пользователя. После v0.1.33 она удаляется через эти 10 с: на 2,5 МиБ меньше при 1000 пользователей, на 14 МиБ — при 5000.

Профилирование (только Mistgate)#

Отдельный проход со сборками с net/http/pprof (подключены через go build -overlay, репозиторий не менялся): профили процессора по 25–30 с, профили heap, allocs, goroutine, mutex и block и GODEBUG=gctrace=1 для панели в простое, для шквала запросов подписки (штатная сборка и сборка без лимитов и кэша) и для фаз одного сервера B64, C и D у панели и ноды. На числа выше эти прогоны не влияют. Полный разбор лежит в results/profiling-summary.txt; сами профили не публикуются. Что нашлось:

  • Путь ответа 429 уже дешёвый. Около 37 мкс процессора на один отказ, 49 % из них — в системных вызовах сокета. Он выделяет 3,2 КБ на запрос; 41 % из этого — разбор запроса стандартной библиотекой, 19 % — две копии запроса, которые делает собственный middleware панели (withStart, WithClientIP), 25 % — карты заголовков ответа. Anon во время отказов был ровным, 27 МиБ (до этого 20 МиБ). 79 МиБ в начале каждого прогона появляются из-за хеширования пароля владельца argon2id (64 МиБ), которое выполняется один раз, а не из-за отказов.
  • Выдача подписки стоит 51 мкс процессора из кэша и 614 мкс без него (выделено 62 КБ и около 900 объектов). Половина процессорного времени без кэша уходит на то, что SQLite заново разбирает и планирует одни и те же запросы на каждый запрос (sqlite3_prepare_v2, 49,8 %); 16 % выделений приходится на strings.ToUpper, который применяется к целым строкам SQL, чтобы проверить префикс SELECT.
  • P99 в 1,7 с на пути без кэша — «голодание», а не пауза. Около 25 запросов в секунду ждут больше 500 мс, каждую секунду прогона. При GOMAXPROCS=4 медленные запросы исчезают (p99 86 мс, та же пропускная способность). В пуле на чтение 4 соединения, и database/sql отдаёт освободившееся соединение случайному ожидающему.
  • Нода: 52 % процессорного времени — UDP sendmsg/recvmsg (GSO используется), 5 % — AES-GCM, 19 % — сборка пакетов QUIC. Самая горячая аллокация (37 %) и конкурирующий мьютекс (QStream.Write) находятся в apernet/quic-go и ядре Hysteria.
  • GC: 2–6 сборок в секунду, самая длинная остановка мира 0,26 мс, когда группа работает на своём процессоре одна, и 46–49 мс, когда её держит квота в тесте одного сервера.

После исправлений#

Три изменения по этим находкам вошли в v0.1.33 (как и четвёртое, ниже):

  • Чтение из хранилища (perf(store)): запросы готовятся один раз на соединение и переиспользуются, к пулу из 4 соединений читатели допускаются в порядке очереди, оба пула держат все соединения открытыми, а проверка на SELECT больше не переводит весь запрос в верхний регистр.
  • Публичные запросы (perf(http)): один контекст запроса вместо двух копий, постоянные заголовки ответа из общих срезов, без strings.NewReplacer на каждый вызов, пул gzip-писателей для подписок Mihomo.
  • Смена пользователей и пароли (perf(fleet,auth)): дайджест учётных данных для ноды переиспользует один буфер на инбаунд (при 1000 пользователях примерно в 5 раз меньше выделений на изменение; проход по всем пользователям остался), а argon2id одновременно считается только один, поэтому параллельные входы больше не складывают по 64 МиБ.

Замер повторили 10 октября на той же машине теми же скриптами. В тот день машина работала на 5–25 % медленнее, чем во время основной серии, поэтому рядом с новой сборкой ещё раз измерили v0.1.32 (n=1), и сравнение идёт с этим контролем:

Mistgate, только панель (1 vCPU, 1 ГБ) v0.1.32, основная серия v0.1.32, контроль после исправлений (n=3)
Подписка без кэша, запросов в секунду (изменённая сборка, лимиты и кэш выключены) 1,630 1,296 3,034 (3,022-3,038)
Подписка без кэша, p50 / p95 / p99 4.2 ms / 6.6 ms / 1.70 s 5.1 ms / 8.4 ms / 2.14 s 2.1 ms / 4.1 ms / 0.92 s
С кэшем рендера, запросов в секунду (изменённая сборка) 19,580 13,481 15,416 (n=1)
Отказов (429) в секунду, штатная сборка 33,700 25,305 25,691 (n=1)
Приватная память в простое, 200 пользователей (MiB) 20 20 21
Mistgate, один сервер (2 vCPU, 2 ГБ) v0.1.32, основная серия v0.1.32, контроль после первых трёх исправлений (n=3) v0.1.33, исправленный стенд (n=3, 12 октября)
D: p95 подписки во время шторма (ms) 30 27 20 (20-20) 1 (1-1)
D: p95 добавления / удаления пользователя (ms) 25 / 16 39 / 32 32 / 30 4 / 3
A: приватная память (MiB) 57 57 57 57 (56-58)

Первые три столбца измерены стендом, который оставлял среде Go внутри контейнеров выбирать GOMAXPROCS по процессорам хоста (см. ниже), последний — исправленным стендом и с четвёртым исправлением, так что переход от 20 мс к 1 мс — сумма обоих изменений, разделить их не пытались. У трёх остальных панелей между сериями изменилось немногое (p95 подписки у 3x-ui 969 мс раньше и 792 мс теперь, у PasarGuard 1619 и 1814 мс), так что смена стенда не объясняет разницу между панелями. Строка о скорости в этой таблице убрана: она снята прежним стендом.

Путь без кэша стал примерно вдвое быстрее, и его p99 сократился вдвое, но хвост не исчез: 0,92 с вместо ожидавшихся 0,1 с (см. ниже). Шторм в фазе D обслуживается с p95 на 25–30 % ниже. Остальное (путь 429, память, перезапуск) в пределах разброса не изменилось, как и ожидалось: исправления этого не касались.

У оставшегося хвоста оказалась другая причина. Транзакция database/sql запускает по горутине на транзакцию и на запрос, а драйвер SQLite — ещё по одной на оператор, чтобы следить за отменяемым контекстом: около 25 на одну выдачу подписки. На одном процессоре они переполняли локальную очередь планировщика Go, и горутины уходили в глобальную, которую занятый процессор проверяет раз в 61 цикл. Соединение, чья горутина чтения HTTP попадала туда, ждало до секунды. Четвёртое изменение (perf(store), пакеты чтения на собственных соединениях без наблюдателей за контекстом) их убирает. Та же машина, та же сессия, вариант без кэша, 3 повтора:

Mistgate, только панель (1 vCPU, 1 ГБ) main до изменения (контроль) с изменением
Подписка без кэша, запросов в секунду 4,066 4,580
p50 / p95 / p99 1.6 / 3.1 / 682 ms 11.0 / 20.8 / 23.5 ms
Самый медленный запрос 780 ms 32 ms

В тот день машина работала быстрее, поэтому сравнивайте столбцы друг с другом, а не с таблицами выше. Рост p50 — это честная доля каждого из 50 клиентов, которые без пауз нагружают занятый сервер (50 / 4 580 в секунду = 10,9 мс). Раньше несколько зависших клиентов оставляли остальным свободный сервер.

Задержка маленького запроса внутри нагруженного туннеля (фаза B: p95 29 мс в двух повторах из трёх, 2,3 мс в третьем; 67 мс на 1 vCPU) оказалась следствием стенда, а не ноды. Квота группы «один сервер» задана на slice systemd, а контейнер со своим пространством имён cgroup её не видит. Поэтому среда Go на ноде выбирала GOMAXPROCS по числу процессоров хоста, тратила квоту в 2 CPU примерно за 80 мс каждого 100-миллисекундного периода и до конца периода стояла (19 % времени в трассе рантайма). Проба раз в 100 мс всегда попадала в одну и ту же фазу этого периода: то мимо паузы, то в неё, отсюда два уровня. Если GOMAXPROCS равен числу vCPU группы, как среда и выбирает на настоящем сервере, нода не останавливается. Тот же бинарник и та же сессия, 64 потока, проба раз в 97 мс, чтобы фаза смещалась: p95 было 14,7–18,2 мс, стало 3,3–3,6 мс, а 4,2 Гбит/с стали 5,4. Стенд в tools/bench теперь задаёт GOMAXPROCS = vCPU каждому контейнеру с ядром (ноде Mistgate и ядрам Xray других панелей). «Один сервер» перемерили 12 октября на v0.1.33 с этим стендом: все четыре панели, три повтора. Строки о памяти, многих клиентах, управляющей части «плохого дня» и перезапуске — в таблице выше. Строк о туннеле там нет: прогон делил машину с другой нагрузкой, результат одной и той же панели при 64 потоках скакал от 1,5 до 4,2 Гбит/с от повтора к повтору, а два самых медленных повтора — те, где генератор нагрузки получил только 177–185 % процессора. P95 маленького запроса у Mistgate при 64 потоках в том прогоне составил 7,9, 5,1 и 53 мс (медианы остальных: 3x-ui 4,2, Remnawave 4,3, PasarGuard 13) — это не стабильные 29 мс первой серии и не чистый результат. Во втором, коротком проходе с пробой раз в 97 мс (только фаза B, по одному повтору) у Mistgate при 64 потоках вышло 5,1 мс против 2,9–3,2 мс у остальных трёх, но его прогоны с 1 и 16 потоками в том проходе были нарушены (у клиента было 149 % процессора, p95 20 и 16 мс), так что по одному повтору выводов не сделать. Утверждений о задержке и скорости туннеля не делаем до тихого перемера из списка ниже.

Что осталось:

  1. Перемерить фазу B и «туннельную» часть фазы D для всех четырёх панелей на тихой машине: группа на двух выделенных физических ядрах (AllowedCPUs=0,1,8,9, CPUQuota=200%, GOMAXPROCS=2), клиенты и приёмник на остальных ядрах, три повтора, проба раз в 97 мс. Это же заменяет таблицу чувствительности первой серии (привязка к ядрам), снятую прежним стендом и удалённую.

Методика#

Машина#

AMD Ryzen 7 7800X3D (8 ядер, 16 потоков), 15,5 ГБ RAM, хост Windows 11, WSL2 с Ubuntu 24.04.4, Linux 6.6.87.2-microsoft-standard-WSL2, Docker 29.4.1 с Compose v5.1.3, драйвер cgroup systemd, cgroup v2. Во время прогонов в том же экземпляре WSL работало ещё несколько простаивавших контейнеров (средняя загрузка около 0,05–0,4; перед каждым прогоном записана в сыром JSON, поле background). Генератор нагрузки и приёмник работали в том же экземпляре WSL, вне ограниченных cgroup.

Ограничения#

  • Хост панели — один слайс systemd bench-panel.slice с CPUQuota=100%, MemoryMax=1G, MemorySwapMax=0, привязанный к одному ядру хоста (AllowedCPUs=15, выставлено на лету через systemctl set-property перед серией; генератор нагрузки в слайс не входил и не был привязан). Контейнеры Docker создаются с cgroup_parent: bench-panel.slice, а Mistgate запускается через systemd-run --slice=bench-panel.slice, поэтому все процессы одной панели (и все контейнеры Remnawave) делят один бюджет. Перед каждым повтором слайс останавливают и запускают заново, чтобы memory.peak начинался с нуля.
  • Один сервер: bench-box.slice, CPUQuota=200%, MemoryMax=2G, MemorySwapMax=0; для повтора на самом дешёвом сервере — 100 % и 1G. Каждому контейнеру с ядром (нода Mistgate, ядра Xray остальных панелей) задаётся GOMAXPROCS, равный числу vCPU слайса: контейнер не видит квоты слайса и иначе выбрал бы его по 16 процессорам хоста (см. «После исправлений»).
  • Привязка осталась от короткого сравнения, сделанного перед серией (только квота против квоты с привязкой); она оставалась включённой во всех прогонах только панели, в вариантах Mistgate, в проходе с холодным кэшем и в профилировании нагрузки на подписки, и заметили это уже потом. Она ближе к серверу с одним vCPU, чем одна квота (квота позволяет многопоточной среде выполнения всплесками работать на нескольких ядрах хоста, а потом душит её), но планировалась одна квота, поэтому для сравнения сделан один повтор без привязки:
Только панель, 1 повтор привязка + квота (серия выше) только квота
Mistgate: anon в простое, 200 пользователей (МиБ) / ответов 429 в секунду 20 / 33,700 24 / 15,750 (p99 77 мс против 7.4)
3x-ui: ответов 200 в секунду / p50 (мс) 148 / 296 162 / 278
PasarGuard: ответов 200 в секунду / p50 (мс) 274 / 166 102 / 467
Remnawave: ответов 200 в секунду / неудачных запросов 271 / 0 251 / 75 % (воркер убит пределом 1 ГБ, oom_kill 1)
Remnawave: anon под нагрузкой (МиБ) 870 786

Память в простое, диск, время старта и числа 3x-ui от привязки не зависят. У PasarGuard и Remnawave под нагрузкой зависят: при одной квоте их потоки расходятся по многим ядрам хоста, и квота их душит. Настоящая виртуальная машина с одним vCPU ведёт себя как случай с привязкой.

  • Прогоны одного сервера шли только с квотой (CPUQuota=200%, без привязки). Прогон с группой на двух физических ядрах и клиентами на остальных — в планах (см. конец раздела «После исправлений»); единственный привязанный повтор первой серии, снятый прежним стендом, удалён.

Измерения#

  • Память и процессор: файлы cgroup слайса, раз в секунду (memory.current, memory.stat anon/file/kernel, cpu.stat, pids). Показатели простоя: среднее за последние 30 с пятиминутного простоя (память) и за всё окно простоя (процессор). Пики — наибольший из секундных замеров в окне и memory.peak слайса из ядра.
  • Диск: сумма размеров docker images использованных образов (в распакованном виде) или размер бинарника; размер каталога данных после установки и после 200 пользователей.
  • Холодный старт: от команды запуска (docker compose up -d или systemd-run) до первого ответа HTTP 200 на основном порту (Remnawave: /api/auth/status, он читает базу).
  • Пользователи: создаются по одному через документированный API каждой панели (Mistgate — Connect-JSON с сессией владельца, 3x-ui — /panel/api/clients/add, Remnawave — /api/users, PasarGuard — /api/user). Всем панелям даны одинаковые выдуманные имена от user1 до user200 (в тесте одного сервера до user1000).
  • Нагрузка: loadgen (Go, только стандартная библиотека), 50 воркеров, ссылки по кругу, один прогревочный проход, 60 с, задержка считается только по ответам 200; всё остальное считается ошибкой.
  • Порядок: панели запускались по очереди, по три раза каждая, данные перед каждым повтором стирались.

Что настроено у каждой панели и чем это отличается от инструкций проекта#

  • Mistgate: собран из тега v0.1.32 (таблицы «только панель») или v0.1.33 (таблицы «один сервер») воспроизводимой сборкой проекта (release build) без ключа релиза, поэтому сам обновляться и ставить подписанные пакеты ноды он не может. Запущен как serve --listen 127.0.0.1:18080 --trusted-proxy 127.0.0.1 --agent-listen <адрес моста>. Агент ноды один раз пришлось подключить (в контейнере в изолированной сети), потому что у ссылки нет строки сервера, пока нет активной ноды; перед замером контейнер удаляется.
  • 3x-ui: готовый образ вместо build: из compose-файла; порты привязаны к 127.0.0.1; admin/admin по умолчанию; один inbound VLESS Reality.
  • Remnawave: официальный compose-файл, образ backend зафиксирован: вместо :3 указан 3.4.5; в каждый запрос добавлены X-Forwarded-For и X-Forwarded-Proto: https (backend отказывает запросам, не прошедшим через TLS-прокси); API-токен создан из сессии панели в браузере (x-remnawave-client-type: browser); профиль конфигурации по умолчанию (один inbound Shadowsocks) и сквад по умолчанию; один хост.
  • PasarGuard: compose-файл проекта по умолчанию (сеть хоста), образ зафиксирован на v5.4.1, UVICORN_HOST=127.0.0.1; конфигурация ядра по умолчанию (один inbound Shadowsocks); один хост и одна группа; владелец создан временным ключом из pasarguard-cli generate-temp-key.
  • Ничего не тюнилось. TLS не используется ни на одной панели (обычный HTTP на loopback), хотя в настоящей установке перед панелью стоит TLS-прокси.

Как повторить#

Скрипты, compose-файлы, diff изменённых сборок, генераторы нагрузки и сведённые результаты лежат в репозитории, в каталоге tools/bench, вместе с README: py/bench.py (только панель), py/onebox.py (один сервер), py/profile_run.py (профилирование), py/aggregate*.py, loadgen/, hyload/, compose/, profiling/ (overlay для pprof) и results/ (таблицы и CSV по повторам). Сырой JSON и посекундные замеры не публикуются. Скрипты создают и удаляют образы Docker и слайсы systemd: запускайте их на тестовой машине и сначала прочитайте README.

Чего эти цифры не говорят#

  • Один настольный процессор, одна виртуальная машина WSL2, три повтора. Разброс небольшой в таблицах «только панель» и в строках управляющей части таблицы «один сервер», но на другом процессоре, ядре или файловой системе абсолютные значения сдвинутся. Машина не была выделенной: во время серии «один сервер» на ней шла другая работа, поэтому строки о скорости туннеля убраны (см. примечание в том разделе).
  • Нагрузка замкнутая, с 50 клиентами: она показывает стоимость запроса, а не число пользователей, которых панель способна обслужить. Настоящий клиент запрашивает подписку раз в несколько часов.
  • Панели делают разную работу на запрос и отдают разные ссылки (Hysteria2, VLESS Reality, Shadowsocks). Remnawave и PasarGuard собирают строку Shadowsocks из сохранённого пароля; 3x-ui собирает строку VLESS Reality; Mistgate собирает строку Hysteria2 с закреплённым сертификатом.
  • Mistgate отвечает на запросы подписки из 10-секундного кэша данных пользователя; остальные так не делают (или не сообщают об этом). Вариант без кэша выше показывает эффект.
  • Базы разные: Remnawave требует PostgreSQL, остальные работают на SQLite. PasarGuard поддерживает также PostgreSQL и MySQL; измерена только его конфигурация по умолчанию.
  • Mistgate измеряли с записью о ноде, агента которой уже нет; остальных — без ноды. Сравнение, где учтено ядро, даёт тест одного сервера.
  • Страничный кэш: бинарник Mistgate копируется из другой cgroup, поэтому его файловый кэш при старте не засчитывается группе, а остальные панели читают свои файлы внутри группы. Поэтому главный показатель — anon. Второй проход со сбросом кэшей описан выше.
  • Не измерялось: скорость передачи данных VPN, кроме Hysteria2 на канале вроде loopback (приёмник и клиенты на одной машине, сети нет), другие протоколы, веб-интерфейс админки, боты Telegram, обновление и резервное копирование, много нод, каналы с большой задержкой или потерями, TLS перед панелью и всё, что длится дольше недели.

Править страницу на GitHub