Бенчмарки
Память, процессор, диск, время старта и нагрузка на подписки у Mistgate, 3x-ui, Remnawave и PasarGuard на одном небольшом сервере, а также тест, где панель, ядро VPN и трафик Hysteria2 работают на одной машине. Методика, версии и ограничения.
На этой странице
- Коротко
- Что измеряли
- Результаты: только панель (1 vCPU, 1 ГБ)
- Нагрузка на подписки
- Дополнительно: Mistgate без ограничений (изменённые сборки)
- Результаты: один сервер (2 vCPU, 2 ГБ, без swap), Hysteria2, 1000 пользователей
- Самый дешёвый сервер: 1 vCPU, 1 ГБ RAM, без swap (один повтор)
- Профилирование (только Mistgate)
- После исправлений
- Методика
- Машина
- Ограничения
- Измерения
- Что настроено у каждой панели и чем это отличается от инструкций проекта
- Как повторить
- Чего эти цифры не говорят
Коротко#
- Главное отличие — память. Одна панель 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 vCPU и 1 ГБ RAM без swap, без VPN-трафика. Память и процессор в простое, диск, время старта, создание 200 пользователей через API и нагрузка на подписки (50 клиентов, 60 с, 200 пользователей).
- Один сервер: панель, ядро 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 мс), так что по одному повтору выводов не сделать. Утверждений о задержке и скорости туннеля не делаем до тихого перемера из списка ниже.
Что осталось:
- Перемерить фазу 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.statanon/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 перед панелью и всё, что длится дольше недели.