Утечка файловых дескрипторов в rw-node: fd растут непрерывно, conntrack стоит на месте

Утечка файловых дескрипторов в rw-node: fd растут непрерывно, conntrack стоит на месте

Окружение:

  • Образ: remnawave/node:latest
  • Xray Core: 26.7.28 (go1.26.5 linux/amd64)
  • Хост: Ubuntu, ядро 6.8.0-139-generic, KVM, 6 vCPU / 9 ГБ RAM
  • Инбаунд: один VLESS XHTTP + Reality на :443
  • ~67 одновременных пользователей
  • Включённые плагины: Torrent-Blocker, Egress Filter

Проблема:
Процесс rw-core накапливает сокетные файловые дескрипторы и не освобождает
их. Как следствие, RSS растёт примерно на 1 ГБ в сутки.

Замеры за 4 часа (снимок каждые 10 минут):

время rss_mb fd conntrack
11:30 7077 103559 83911
12:30 7266 104391 86124
13:30 7267 104997 83179
14:30 7268 105459 81914
15:10 7268 106395 80996
– перезапуск контейнера –
15:20 204 1396 6144

Ключевое наблюдение: conntrack держится на месте (~83k, колеблется в
пределах нормы), а количество fd только растёт — примерно +750 в час.
Ядро эти соединения уже освободило, rw-core свои сокеты не закрыл.

Подтверждается через /proc/1/net/sockstat внутри контейнера, примерно
через час после чистого перезапуска, когда установленных входящих
соединений всего 877:

sockets: used 2252
TCP: inuse 602 orphan 1 tw 334 alloc 1819 mem 0

alloc (1819) сильно превышает inuse (602) — сокеты выделены и не
возвращены, и разрыв со временем растёт.

Состав дескрипторов до перезапуска:
103429 socket:
28 pipe:
2 anon_inode:

Состояния сокетов внутри контейнера (/proc/net/tcp) до перезапуска:
103131 ESTABLISHED
534 TIME_WAIT
6 CLOSE_WAIT

Обратите внимание: сокеты числятся как ESTABLISHED, а не CLOSE_WAIT —
то есть это не похоже на пропущенный Close() при разрыве со стороны пира.

После перезапуска: 1396 fd, 204 МБ RSS, 877 установленных входящих
соединений со 171 уникального адреса, что соответствует реальному числу
пользователей. То есть те ~106k дескрипторов — накопленный за 4 дня
аптайма мусор, а не реальная нагрузка.

Воспроизводится на двух независимых нодах у разных хостеров, так что от
конкретного хоста не зависит.

Временное решение: перезапуск контейнера раз в сутки.

Подскажите, что полезнее всего снять дальше? Готов запустить с включённым
pprof или собрать goroutine dump, если образ это поддерживает.

Не относится к Remnawave. rw-core это алиас для Xray Core.

        "sockopt": {
          "tcpFastOpen": true,
          "tcpKeepAliveIdle": 30,
          "tcpKeepAliveInterval": 30
        }
# Keep-Alive на уровне ядра (обнаружение мёртвых клиентов)
net.ipv4.tcp_keepalive_time = 30
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 3

# Быстрое отбрасывание мёртвых соединений
# tcp_retries2: было 15 (по умолчанию, ~13-30 мин ожидания). Ставим 5 (~6-25 сек)
net.ipv4.tcp_retries2 = 5
# Для сокетов без процесса-владельца (orphaned)
net.ipv4.tcp_orphan_retries = 2

# Борьба с утечками сокетов
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_tw_buckets = 65536

# Conntrack: таймауты (КРИТИЧНО!)
# По умолчанию established = 432000 (5 ДНЕЙ!) — вот почему зомби живут так долго
net.netfilter.nf_conntrack_tcp_timeout_established = 1800
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 15
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 15
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 15

# Лимиты
net.netfilter.nf_conntrack_max = 1048576
fs.file-max = 2097152
net.ipv4.ip_local_port_range = 1024 65535
#!/bin/bash
MAX_IDLE_MS=600000
killed=0
while read -r line1; do
    read -r line2 || break
    peer=$(echo "$line1" | awk '{if ($NF ~ /^timer:/) print $(NF-1); else print $NF}')
    lastrcv=$(echo "$line2" | grep -oP 'lastrcv:\K\d+' || echo 0)
    lastsnd=$(echo "$line2" | grep -oP 'lastsnd:\K\d+' || echo 0)
    
    if (( lastrcv > MAX_IDLE_MS && lastsnd > MAX_IDLE_MS )); then
        idle_sec=$(( (lastrcv < lastsnd ? lastrcv : lastsnd) / 1000 ))
        ss -K dst "$peer" >/dev/null 2>&1
        echo "$(date '+%H:%M:%S') Убито: $peer (неактивно ${idle_sec}с)"
        killed=$((killed + 1))
    fi
done < <(ss -tnoi state established '( sport = :443 )' | tail -n +2)
(( killed > 0 )) && echo "$(date '+%H:%M:%S') Итого убито зомби: $killed"