Xray API 节点自动切换,核心并不是简单地“发现一个节点失效后换下一个节点”,而是把节点状态、流量统计、主动探测、切换动作和故障回退组织成一条可重复执行的流程。Xray 的 StatsService 可以读取入站与出站的流量计数,HandlerService 可以在运行过程中管理部分处理器;脚本则负责把统计数据和主动健康检查组合起来,最终决定是否切换当前节点。
本文以 Xray-core 25.x 的常见 API 配置方式为基础,使用本机 127.0.0.1:10085 作为 gRPC API 监听地址,节点标签使用 proxy-a、proxy-b 和 proxy-c。示例重点放在可维护性:API 只监听本机、统计标签保持稳定、连续失败才触发切换、切换后保留冷却时间,并且在所有候选节点都不可用时回退到上一个已知可用节点。
本文适合已经能够编写 Xray JSON 配置、理解入站与出站标签,并希望用脚本实现节点健康检查和自动切换的开发者。读完可以完成 StatsService 开启、统计查询、节点探测、切换决策与故障回退,同时理解流量统计不能代替延迟检测这一边界。
Xray API 与 StatsService 的职责边界
Xray API 通常通过独立的 api 入站暴露 gRPC 服务。它不是给浏览器或普通代理流量使用的端口,而是管理核心状态的控制入口。配置中的 api.tag 用于标记这条管理入站,api.services 则声明需要启用的服务。读取流量计数时,至少需要启用 StatsService;如果还要动态增删入站或出站,则需要根据实际操作启用 HandlerService。
StatsService 返回的是累计计数,而不是某一条连接的实时质量评分。例如,outbound>>>proxy-a>>>traffic>>>uplink 表示标签为 proxy-a 的出站累计上传字节数。脚本连续读取两次计数,使用差值除以采样间隔,便可以得到近似吞吐量;如果计数长时间不增长,只能说明最近没有有效流量,不能直接证明节点已经失效。
实际部署时,API 服务至少要满足三个安全条件。第一,监听地址使用 127.0.0.1 或 Unix Socket,不要直接绑定公网地址。第二,防火墙只允许管理脚本访问 API 端口。第三,脚本运行账户应尽量限制文件读取、核心重载和日志访问权限。API 一旦暴露给不可信网络,攻击者可能读取运行状态、修改处理器,甚至影响整个代理进程。
结论:统计负责观测,探测负责判活
StatsService 适合回答“这个出站最近传输了多少数据”,不适合单独回答“这个节点当前是否能访问目标网站”。自动切换至少应同时使用累计流量差值、TCP 或 HTTPS 主动探测、连续失败次数三个信号。
配置 API、统计与节点标签
下面是一段可以嵌入 Xray 主配置的最小示例。示例保留三个节点出站,并使用一个专门的 API 入站承载 gRPC 管理请求。节点本身的地址、UUID、Reality 公钥等参数需要替换为实际订阅内容;不要为了测试 API 而复制一份过期节点参数。
{
"log": {
"loglevel": "warning"
},
"api": {
"tag": "api",
"services": [
"StatsService",
"HandlerService",
"LoggerService"
]
},
"stats": {},
"inbounds": [
{
"tag": "api-in",
"listen": "127.0.0.1",
"port": 10085,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
},
{
"tag": "socks-in",
"listen": "127.0.0.1",
"port": 10808,
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": true
}
}
],
"outbounds": [
{
"tag": "proxy-a",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "node-a.example",
"port": 443,
"users": [
{
"id": "11111111-1111-1111-1111-111111111111",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality"
}
},
{
"tag": "proxy-b",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "node-b.example",
"port": 443,
"users": [
{
"id": "22222222-2222-2222-2222-222222222222",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality"
}
},
{
"tag": "proxy-c",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "node-c.example",
"port": 443,
"users": [
{
"id": "33333333-3333-3333-3333-333333333333",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality"
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": [
"socks-in"
],
"outboundTag": "proxy-a"
},
{
"type": "field",
"inboundTag": [
"api-in"
],
"outboundTag": "api"
}
]
}
}
这里有一个容易忽略的配置关系:stats 对象本身可以是空对象,但出站标签必须稳定,脚本才能通过固定名称查询计数。若订阅更新后把 proxy-a 改成随机标签,脚本会得到“统计项不存在”,随后可能误判节点无流量并触发错误切换。建议在订阅转换或配置生成阶段建立稳定的内部标签,不要直接把显示名称当作统计键。
API 入站
- 协议
- dokodemo-door
- 监听地址
- 127.0.0.1
- 监听端口
- 10085
- 管理标签
- api
只供本机脚本访问,不应绑定 0.0.0.0。
节点出站
- 标签
- proxy-a / proxy-b
- 协议
- VLESS
- 传输
- TCP + Reality
- 统计键
- outbound>>>标签>>>traffic
标签一旦交给脚本使用,就应避免频繁改名。
用命令查询累计流量与速率
Xray-core 自带的命令行工具可以通过 API 查询统计项。不同发行包的可执行文件名称可能是 xray 或带平台后缀的核心文件;先执行 xray version 确认命令可用,再查询 API。下面的命令查询 proxy-a 的上行和下行累计字节数:
xray api statsquery \
--server=127.0.0.1:10085 \
-pattern="outbound>>>proxy-a>>>traffic>>>"
xray api statsquery \
--server=127.0.0.1:10085 \
-pattern="outbound>>>proxy-b>>>traffic>>>"
返回结果通常会包含统计名称和整数值。脚本不应只读取一次结果,因为累计值没有时间维度。更可靠的做法是每隔 5 秒记录一次快照,计算两次快照的差值。如果 uplink 与 downlink 都没有增长,再结合 HTTPS 探测失败,才把节点视为可疑。
#!/usr/bin/env bash
API="127.0.0.1:10085"
NODE="proxy-a"
xray api statsquery \
--server="$API" \
-pattern="outbound>>>$NODE>>>traffic>>>"
如果返回“找不到统计项”,先确认核心启动日志中没有 JSON 语法错误,再检查是否启用了 StatsService,最后核对出站标签是否完全匹配。标签区分大小写,proxy-a、Proxy-A 和 proxy_a 是三个不同的名称。另一个常见原因是节点尚未产生任何流量,某些版本不会提前创建对应的统计记录。
健康检查与节点评分模型
自动切换不应该使用单一指标。延迟最低的节点不一定稳定,流量最高的节点也不一定当前可用。建议为每个节点保存以下状态:最近一次 HTTPS 探测耗时、连续失败次数、最近一次成功时间、5 秒采样窗口内的上下行增量,以及当前是否处于冷却期。
- 主动可达性:通过指定代理出站访问一个稳定的 HTTPS 地址,设置 3 至 5 秒超时,验证 TCP、TLS 和 HTTP 状态码。
- 延迟:记录完整请求耗时,不要把本地 DNS 查询时间单独当成节点延迟。
- 连续失败:建议连续失败 3 次才切换,避免一次临时丢包造成抖动。
- 恢复确认:被标记为故障的节点至少连续成功 2 次后,才重新加入候选。
- 冷却时间:切换后等待 30 至 60 秒,不要立即再次选择刚刚失败的节点。
一个简单的评分模型可以把延迟、失败惩罚和近期吞吐结合起来。例如将延迟控制在 50 至 2000 毫秒范围内,使用较低延迟获得较高分;连续失败时直接扣除 100 分;近期没有统计增量时不立刻扣分,而是交给主动探测决定。这样可以避免“空闲节点因为没有流量而被误判”的问题。
能够覆盖 TCP 建连、TLS 握手、代理出站和目标 HTTP 响应,适合承担主要判活职责。
适合:周期性健康检查、自动切换
读取成本低,可以判断出站是否持续承载流量,但无法证明没有流量就是节点故障。
适合:辅助评分、容量观察
实现简单,但服务器可能屏蔽 ICMP,且 ICMP 可达不代表代理协议、TLS 和目标网站可用。
适合:粗略网络基线
动手编写节点切换脚本
为了让示例不依赖某个客户端的专有界面,下面使用“生成当前节点配置并重载核心”的方式演示。脚本先对候选节点执行探测,再根据连续失败阈值选择得分最高者;切换动作写入一个状态文件,并调用外层服务管理器重启或重载 Xray。生产环境中可以把最后一步替换成现有的进程管理命令,但不要在脚本中无条件杀掉所有名为 Xray 的进程。
-
固定节点标签
为每个出站保留稳定标签,例如
proxy-a、proxy-b和proxy-c,并在脚本数组中维护标签与显示名称的对应关系。 -
准备探测地址
选择响应稳定的 HTTPS 地址,使用小响应体即可。探测地址应与实际业务相近,避免只测试一个在特定网络中被缓存或拦截的域名。
-
记录失败次数
为每个节点保存成功时间和连续失败次数,单次超时只增加计数,不立即执行切换。
-
计算候选评分
剔除冷却期节点,再按探测延迟、失败次数和近期流量增量排序,选择最高分节点。
-
应用新配置
只有候选节点与当前节点不同,并且达到切换条件时才写入状态文件,随后执行受控重载。
下面的 Bash 示例使用本地 HTTP 代理端口 10809 做实际探测。它假设不同节点已经分别配置了临时代理入口,或者配置生成器会根据 ACTIVE_NODE 选择对应出站。若现有配置只有一个固定的 SOCKS 入站,则不能仅修改环境变量就让已经建立的连接改变出站;已存在的 TCP 连接通常需要自然结束或由应用重新建立。
#!/usr/bin/env bash
set -euo pipefail
STATE_FILE="/var/lib/xray/active-node"
FAIL_DIR="/var/lib/xray/failures"
CHECK_URL="https://example.com/"
COOLDOWN=45
MAX_FAIL=3
mkdir -p "$FAIL_DIR"
nodes=("proxy-a" "proxy-b" "proxy-c")
check_node() {
local node="$1"
local start end elapsed
start=$(date +%s%3N)
if curl --proxy "http://127.0.0.1:10809" \
--connect-timeout 3 \
--max-time 5 \
--silent --show-error --fail \
"$CHECK_URL" >/dev/null 2>&1; then
end=$(date +%s%3N)
elapsed=$((end - start))
echo "$elapsed"
return 0
fi
return 1
}
current=""
if [[ -f "$STATE_FILE" ]]; then
current=$(cat "$STATE_FILE")
fi
best=""
best_latency=999999
for node in "${nodes[@]}"; do
if latency=$(check_node "$node"); then
fail_file="$FAIL_DIR/$node"
rm -f "$fail_file"
if (( latency < best_latency )); then
best="$node"
best_latency="$latency"
fi
else
fail_file="$FAIL_DIR/$node"
count=0
[[ -f "$fail_file" ]] && count=$(cat "$fail_file")
echo $((count + 1)) > "$fail_file"
fi
done
if [[ -z "$best" ]]; then
echo "no healthy node; keep current=$current" >&2
exit 0
fi
if [[ "$best" == "$current" ]]; then
echo "keep node=$current latency=${best_latency}ms"
exit 0
fi
echo "$best" > "$STATE_FILE"
# 由配置生成器读取 ACTIVE_NODE,再执行受控重载。
export ACTIVE_NODE="$best"
/usr/local/bin/render-xray-config --active "$ACTIVE_NODE"
/usr/bin/systemctl reload xray
echo "switched $current -> $best latency=${best_latency}ms"
这个示例展示了决策骨架,但仍需要补上“每个节点独立探测”的实现。若所有探测请求都经过当前活动节点,脚本只能判断当前出站,不能公平比较备用节点。常见做法是为每个节点建立独立的临时出站或临时入站,再让探测请求明确指定对应标签;另一种做法是使用独立的 Xray 测试进程,加载同一份节点参数但只启用一个候选出站。不要把多个节点都写入同一个代理入口,却在脚本中假设 curl 会自动选择指定标签。
切换动作、重载与故障回退
Xray API 的动态能力并不意味着所有配置项都适合在运行中修改。HandlerService 更适合管理入站和出站处理器;路由规则、DNS、传输层和 Reality 参数的复杂变更,通常更适合由脚本生成完整 JSON 后进行配置校验,再通过服务管理器受控重载。对于桌面端 v2rayN 或 v2rayNG,自动修改核心配置还可能被订阅更新覆盖,因此应把脚本放在配置生成层,而不是直接编辑客户端维护的原始订阅文件。
如果使用完整配置重载,至少要执行三项保护。第一,写入临时文件后运行 xray run -test -config 或当前版本支持的配置校验命令,校验通过后再替换正式文件。第二,保存上一个已知可用配置,切换后的首次探测失败时可以立即恢复。第三,设置最短切换间隔,例如 45 秒,防止两个节点之间反复来回切换。
tmp="/etc/xray/config.json.next"
current="/etc/xray/config.json"
backup="/etc/xray/config.json.last-good"
render-xray-config --active "$ACTIVE_NODE" > "$tmp"
if xray run -test -config "$tmp"; then
cp "$current" "$backup"
mv "$tmp" "$current"
systemctl reload xray
else
echo "config validation failed; keep current configuration" >&2
rm -f "$tmp"
exit 1
fi
正常切换
- 触发条件
- 连续失败 ≥ 3 次
- 候选条件
- 成功探测 ≥ 2 次
- 冷却时间
- 45 秒
- 配置动作
- 校验后重载
切换前后都记录节点标签、延迟和失败计数。
全部失败
- 当前节点
- 保持不变
- 备用状态
- 等待下一轮探测
- 回退文件
- last-good
- 告警条件
- 连续 5 轮失败
不要在所有节点失败时随机写入一个未经验证的配置。
日志、权限与验证方法
完成配置后,先验证 API 是否真正可用,再验证节点切换。可以从三个层次观察结果:核心层查看 API 入站与服务初始化日志;统计层查询固定标签的累计计数;业务层通过指定代理访问测试地址。三层结果必须相互印证,不能只看到脚本打印“switch success”就认为流量已经切换。
- API 层:确认
127.0.0.1:10085正在监听,并且脚本账户具备读取权限。 - 统计层:切换前后分别查询
proxy-a、proxy-b的 uplink 与 downlink,确认新节点计数能够增长。 - 连接层:使用代理访问测试地址,记录 HTTP 状态码和完整请求耗时。
- 回退层:人为暂停一个候选节点,确认连续失败 3 次后切换,并且不会在每个轮询周期重复重载。
报错: failed to dial api server: connection refused
原因与解法:API 入站没有监听、端口填写错误或核心尚未启动;先检查 10085 的监听状态和 Xray 启动日志,再核对脚本中的 --server 参数。
报错: Failed to query stats: not found
原因与解法:统计服务未启用、统计键拼写错误,或该出站尚未产生流量;检查 StatsService、stats 对象和出站标签,并先产生一次测试流量。
报错: xray run -test failed
原因与解法:生成的新 JSON 存在语法错误、重复标签或字段类型不正确;保留临时文件,使用带行号的 JSON 工具定位错误,不要直接覆盖当前可用配置。
报错: health check succeeded but traffic did not switch
原因与解法:探测成功只代表探测路径可用,当前已有连接仍可能继续使用旧出站;确认路由规则、活动节点状态和新连接是否重新建立。
最后需要注意连接生命周期。自动切换通常只影响新建连接,已经建立的 TCP、WebSocket 或长连接不会因为统计标签改变而自动迁移。对于浏览器短连接,切换效果可能很快显现;对于下载任务、数据库连接和长轮询业务,则应由上层应用实现重连。脚本应记录切换时间、旧节点、新节点、探测延迟和触发原因,方便区分“代理切换成功但应用没有重连”和“新节点本身仍然不可用”。