VPN·HUB·CRACK
Почему ноды перестают работать: память, логи, дескрипторы · VPN HUB CRACK
Почему ноды перестают работать: память, логи, дескрипторы
Почему ноды перестают работать: память, логи, дескрипторы
Нода отвалилась ночью. В панели красная, клиенты жалуются, по SSH зайти удаётся не с первого раза. Перезагрузили — заработало. Через неделю повторилось.
Дальше три причины, по которым это происходит, и способ отличить их друг от друга за одну минуту. Ни одна не связана с нагрузкой: сервер с двадцатью клиентами умирает так же, как с двумя сотнями.
Память
Xray-core растёт по RAM и не всегда отдаёт её обратно. Это свойство ядра, а не дефект конкретной панели — в апстриме обсуждают годами.
Свежий повод: начиная с Xray-core v25.10.15 поменялись значения xmux для транспорта
XHTTP. Параметр maxConcurrency был 32–16, стал 1. Каждый запрос теперь открывает своё
соединение, и на активной ноде это тысячи одновременных сессий вместо десятков. Память
растёт вместе с ними.
Что происходит дальше, зависит от одной строчки в конфигурации.
Без лимита ядро зовёт OOM killer, и тот выбирает жертву по «badness» — грубо говоря,
по объёму потребления. Обычно это Xray. Но перед убийством система успевает поболтаться
в свопе, и скорость проседает у всех клиентов сразу. А жертвой может оказаться dockerd
или sshd, и тогда вы теряете не сервис, а доступ к серверу.
С лимитом контейнер живёт в своей cgroup со своим потолком. Xray упирается в него,
ядро убивает только его, restart: always поднимает обратно за секунду. SSH и Docker
целы, а счётчик показывает, сколько раз это случилось.
Лимит не лечит утечку. Он превращает «сервер отвалился, непонятно почему» в «сервис перезапустился за секунду, вот счётчик».
Как проверить за минуту
Счётчик убийств по памяти лежит в cgroup процесса:
PID=$(docker inspect -f '{{.State.Pid}}' remnanode)
grep oom_kill /sys/fs/cgroup$(awk -F: '$1=="0"{print $3}' /proc/$PID/cgroup)/memory.events
Ненулевое число означает, что сервис уже убивали. Рядом полезно посмотреть, сколько раз контейнер перезапускался:
docker inspect -f '{{.RestartCount}} {{.State.OOMKilled}}' remnanode
Тридцать четыре перезапуска и true во втором поле закрывают вопрос.
У 3x-ui выигрыш от лимита больше, чем кажется. Панель сама читает cgroup-лимит и
выставляет Go-шный GOMEMLIMIT в 90% от него, то есть сборщик мусора начинает работать
до того, как сработает OOM. Без лимита этого механизма нет: Go считает, что памяти
бесконечно.
Сколько давать
Системе нужно примерно фиксированное количество памяти: dockerd, sshd, systemd,
немного page cache. На 512 МиБ это около 190 МиБ. Отсюда формула вместо лестницы из
процентов:
запас = ограничить(192 MiB … 20% RAM … 1024 MiB)
лимит = RAM − запас
| RAM хоста | Лимит сервиса | Остаётся системе |
|---|---|---|
| 512 MiB | 320 MiB | 192 MiB |
| 1 GiB | 820 MiB | 204 MiB |
| 2 GiB | 1.6 GiB | 409 MiB |
| 8 GiB | 7.0 GiB | 1 GiB |
Лимит ниже рабочего пика опаснее отсутствия лимита: сервис уйдёт в цикл перезапусков. Сначала посмотрите пик потребления, потом берите значение с запасом над ним.
Ставить лимит правкой docker-compose.yml не надо. Установщики панелей регенерируют
этот файл при обновлении, и настройка исчезнет молча. Кладите отдельный
docker-compose.override.yml рядом — Compose подхватит его сам, а откат сведётся
к удалению одного файла.
Логи
Docker с драйвером json-file пишет без ротации. Лог контейнера растёт, пока не
кончится диск.
У 3x-ui лог самой панели ротируется встроенным lumberjack: 10 МБ, пять файлов, семь дней. Тут порядок. А вот access.log самого Xray пишется ядром напрямую, и не ротируется ничем — ни Xray, ни панелью. В трекере это issue #1234, «Free disk space is constantly decreasing».
Дальше цепочка неочевидная. Диск кончается, SQLite отвечает database is locked, панель
перестаёт работать. Человек ищет проблему в базе, а она на диске.
Проверить:
du -h $(docker inspect -f '{{.LogPath}}' remnanode)
df -h /
Четыре гигабайта в первой строке встречаются чаще, чем хотелось бы.
Лечится ротацией на уровне сервиса:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Для нативной установки 3x-ui нужен logrotate, причём с copytruncate. Xray держит файл
открытым и не переоткрывает его по сигналу, поэтому обычный create отправит запись
в никуда. Если IP-limit не используется, access log проще выключить совсем: в свежих
версиях 3x-ui механизм лимита его больше не читает.
Дескрипторы
Тут разница между панелями заметная.
Remnawave в официальном compose уже прописывает nofile: soft 1048576 / hard 1048576.
Делать нечего.
3x-ui не задаёт ничего: ни install.sh, ни x-ui.service, ни Dockerfile. Xray
запускается дочерним процессом внутри x-ui и наследует то, что дал systemd по
умолчанию. Для сравнения, официальный установщик XTLS/Xray-install ставит своему юниту
LimitNOFILE=1000000.
Под нагрузкой это выглядит как too many open files и обрывы у части клиентов при живом
и здоровом на вид процессе.
cat /proc/$(pgrep -f xray | head -1)/limits | grep 'open files'
Тысяча двадцать четыре в первой колонке — приговор для ноды, которая держит хоть сколько-нибудь клиентов.
Заодно про BBRv3, которого нет
Раз уж речь о тюнинге. В сети много статей 2026 года про «BBRv3 в ядре 6.x». Все они путают v3 с v1.
BBRv3 в mainline не влит. Проверяется за минуту по net/ipv4/tcp_bbr.c в master
torvalds/linux: там код 2016 года и только BBR v1. Разработчик Google Нил Кардвелл
подтверждал это в рассылке bbr-dev в марте 2025.
В стоковых Debian 12/13 и Ubuntu 22.04/24.04/26.04 доступен только bbr первой версии.
Он включается одним sysctl, ничего не стоит и на маршрутах с потерями даёт заметный
выигрыш. Гнаться за v3 через кастомное ядро имеет смысл только на трансграничных
маршрутах с реальной перегрузкой, и цена там высокая: ядра XanMod не подписаны для
Secure Boot, а apt autoremove умеет снести старое ядро и оставить сервер без запасного
пункта в GRUB.
Проверять надо не имя в sysctl, а версию модуля:
modinfo tcp_bbr | grep -i version
Имени bbr3 может не быть вообще: у части сборок v3 подменяет собой имя bbr.
И про файрвол, пока не поздно
Отдельная категория мёртвых нод — те, где файрвол настроили правильно, а проверили неправильно.
Правила применили, из той же SSH-сессии проверили доступ, всё работает, сессию закрыли. Больше на сервер зайти не удалось.
Открытая сессия живёт по ct state established и продолжает работать, даже когда новые
подключения уже отбиваются. Она ничего не доказывает. Проверять надо вторым
подключением, не закрывая первое.
Второе правило: ставьте откат по таймеру до того, как применяете правила.
systemd-run --on-active=300 --unit=fw-revert --collect nft delete table inet myfw
Не подтвердили за пять минут — правила снялись сами. Без консоли у провайдера это не перестраховка, а единственная страховка.
И третье: не смешивайте ufw с нативными правилами nftables. Документация Ubuntu Security запрещает это прямым текстом — ufw работает через утилиты iptables, и поверх него nft-правила дают «правило есть, а трафик идёт мимо». Отдельная версия той же ловушки: Docker публикует порты через цепочку FORWARD и обходит ufw полностью.
Что делать со всем этим сразу
Перечисленное закрывается руками за вечер. Если серверов больше одного, вечер превращается в неделю.
Мы собрали это в один скрипт: HUBTune. Он находит ноду сам по меткам Docker, показывает план до применения и откатывается одной командой. Опасные вещи требуют доказательств: правила файрвола подтверждаются из нового подключения, пароли SSH не отключатся, пока скрипт не найдёт другой способ войти, ядро не поставится на машину, где оно не загрузится.
curl -fsSL https://raw.githubusercontent.com/qwe8nxtroud/HUBTune/main/hubtune.sh -o hubtune.sh
less hubtune.sh
sudo bash hubtune.sh status
Команда status только читает и показывает, что не так прямо сейчас. С неё и стоит
начать, даже если ставить ничего не собираетесь.