Симптом
RAM ядра xray (Go-процесс ноды) линейно растёт на сотни мегабайт в час, watchdog рубит ноду каждые 1–8 часов, xrayUptime не держится. Частый диагноз — «транспорт плохой, сокеты висят», и люди меняют протокол. Зря: за симптомом стоят три независимых корня, и каждый лечится точечно на уровне инбаунда или policy, без смены архитектуры.
Корень 1. Rate-limit через per-user token-bucket
Самый жёсткий. В policy.levels.0 включены uplinkOnly/downlinkOnly — лимит скорости на пользователя. Xray реализует их через token-bucket на каждого юзера, который держится в памяти процесса и не очищается ни по idle, ни при дисконнекте — только при reload. Тысяча пользователей плюс мобильный churn — тысячи бакетов, каждый со своим mutex и state.
Фикс: убрать uplinkOnly/downlinkOnly из policy.levels.0. Эффект наступает за 5–15 минут, RAM падает на 80–89% — в замере с 1.10 ГБ до 187 МБ. Если rate-limit действительно нужен, закладывайте обязательный рестарт xray раз в ~12 часов. Альтернатива против перекачки и торрентов — torrent-blocker плюс маршрут служебных портов BitTorrent в DIRECT мимо туннеля.
Корень 2. CLOSE-WAIT в TCP-Reality: keepalive на инбаунде выключен
На xray-инбаунде TCP keepalive по умолчанию отключён (в отличие от outbound). Когда мобильный клиент теряет сеть без FIN, сервер держит ESTABLISHED-сокет до ~2 часов. Каждый такой «живой» с точки зрения ядра сокет — метаданные в памяти. Это не утечка в коде: память законно держится под зомби-сокеты. Исправлять нужно ядро, через sockopt инбаунда:
"sockopt": {
"tcpKeepAliveIdle": 30,
"tcpKeepAliveInterval": 15,
"tcpUserTimeout": 10000,
"tcpFastOpen": true,
"tcpcongestion": "bbr"
}
Главный гвоздь — tcpUserTimeout: 10000 (RFC 5482): нет ACK на отправленные данные 10 секунд — рвём сокет. Замер: ESTABLISHED с 13134 до 765, RSS с 1006 МБ до 115 МБ. Применимо только к network: tcp + reality.
Корень 3. Висячие HTTP/2-стримы в XHTTP: sockopt бесполезен
В XHTTP один TCP-сокет несёт N логических HTTP/2-стримов. Стрим повис (клиент пропал), а TCP под ним живой — mux его переиспользует, keepalive ОС видит живой TCP и не вмешивается. Копятся зомби-стримы: в замере 3660 ESTABLISHED на 9 пользователей. Лечится на уровне xray, через xhttpSettings.extra:
"extra": {
"scMaxBufferedPosts": 30,
"scStreamUpServerSecs": "20-80",
"xmux": {
"maxConcurrency": "16-32",
"hMaxRequestTimes": "600-900",
"hMaxReusableSecs": "1800-3000",
"hKeepAlivePeriod": 0,
"cMaxLifetimeMs": 0
}
}
scMaxBufferedPosts — убийца висячих стримов: превышение буфера, и клиент дисконнектится. hMaxReusableSecs принудительно рециклит «вечные» TCP-соединения каждые 30–50 минут. scStreamUpServerSecs шлёт padding, чтобы CDN не обрезал idle-стрим. Замер: ESTABLISHED на пользователя с ~407 до ~81, RSS с 979 до 340 МБ, throughput вырос на 131%. Полный эффект — через 30–50 минут после рестарта. sockopt при этом добавляют как вторую линию защиты физического TCP.
Подводные камни
- Три корня не взаимозаменяемы. sockopt не лечит rate-limit-state;
xhttp.extraне лечит основной TCP-Reality; sysctl-тюнинг — вообще про производительность, не про память. Полумеры тратят дни. - Обёртки API нередко фильтруют
sockoptиextra— применяйте через прямой PATCH профиля и проверяйте, что поля реально дошли. --max-old-space-sizeне действует — это флаг Node.js, а ядро ноды написано на Go.- Всегда снапшот профиля до PATCH. После — контрольный замер:
ss -ant | awk 'NR>1{print $1}' | sort | uniq -cи RSS через 30 минут. Главный признак фикса — ESTABLISHED стабилизировался, а не растёт линейно.
Вывод. Линейный рост RAM у xray-ноды — не «плохой транспорт», а один из трёх конкретных дефектов конфигурации: per-user token-bucket rate-limit, выключенный keepalive на TCP-инбаунде или висячие HTTP/2-стримы в XHTTP. Диагностируйте по числу ESTABLISHED на пользователя и типу транспорта, лечите точечно — и рестарты по watchdog прекращаются.