Xray-core 的自动切换不能简单理解为“定时给所有节点测速,然后选择延迟最低的一条”。在无头服务器、旁路由或容器环境中,真正可靠的高可用逻辑至少要同时处理四件事:通过 gRPC API 读取节点运行状态,使用独立探针确认端到端可用性,对连续失败的节点执行摘除,并在恢复后避免立即把不稳定节点重新放回生产流量。
本文面向能够阅读 Xray JSON 配置、理解 gRPC 调用并维护脚本的技术用户。示例使用 Xray-core 的 API 入站、StatsService 与 HandlerService,监听地址统一设为本机 127.0.0.1:10085,本地 SOCKS 入站使用 127.0.0.1:10808。节点切换采用“固定代理出站标签 + 动态修改出站内容”的方式,便于路由规则始终指向同一个标签。
本文从 Xray API 配置开始,解释 StatsService 如何提供流量统计、为什么延迟测试不能只看单次结果,并给出基于失败阈值、恢复阈值和冷却时间的自动摘除与恢复方案。读完后可以把同一套逻辑部署到无头 Linux 主机、旁路由或容器服务中。
先建立 Xray API 的管理边界
Xray 的 API 通过专用的 api 入站暴露 gRPC 服务。它不是 HTTP JSON 接口,也不应直接暴露到公网。常用服务包括 HandlerService、StatsService、RoutingService 和日志相关服务。本文只依赖前两个:HandlerService 负责运行时增删或修改入站、出站,StatsService 负责读取指定标签的流量计数。
一个容易维护的配置,应把 API 入站、统计对象和业务入站分开。下面是可作为基础合并到主配置中的片段。api 标签必须同时出现在 inbounds 和 routing.rules 中,否则 API 请求可能被错误地送入代理出站。
{
"log": {
"loglevel": "warning"
},
"api": {
"services": [
"HandlerService",
"StatsService"
],
"tag": "api"
},
"inbounds": [
{
"tag": "api",
"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
}
}
],
"routing": {
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"outboundTag": "api"
}
]
},
"outbounds": [
{
"tag": "api",
"protocol": "freedom"
},
{
"tag": "proxy-active",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "node-a.example.net",
"port": 443,
"users": [
{
"id": "00000000-0000-0000-0000-000000000000",
"encryption": "none",
"flow": "xtls-rprx-vision"
}
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"serverName": "www.example.com",
"fingerprint": "chrome",
"publicKey": "REPLACE_PUBLIC_KEY",
"shortId": "REPLACE_SHORT_ID"
}
}
}
]
}
示例中的 UUID、公钥、短 ID、服务器名称和地址均为占位值,不能直接使用。实际部署时,脚本只应修改节点池中明确允许修改的字段。VLESS、VMess、Trojan 和 Shadowsocks 的 JSON 结构不同,不能把一个协议的 settings 对象直接套用到另一个协议上。
StatsService 能说明什么,不能说明什么
StatsService 记录的是 Xray 内部观察到的流量计数。要让出站统计有意义,出站必须拥有稳定的 tag,并且统计配置要打开出站流量计数。常用统计名称格式如下:
outbound>>>proxy-active>>>traffic>>>uplink:代理出站发送字节数。outbound>>>proxy-active>>>traffic>>>downlink:代理出站接收字节数。inbound>>>socks-in>>>traffic>>>uplink:SOCKS 入站接收的上行流量。inbound>>>socks-in>>>traffic>>>downlink:SOCKS 入站返回的下行流量。
可以使用 Xray 自带的 API 命令查询单项统计。不同发行版的命令参数可能略有差异,先运行 xray api --help 确认本机版本支持的语法。典型查询方式如下:
xray api statsquery \
--server=127.0.0.1:10085 \
-pattern='outbound>>>proxy-active>>>traffic'
统计值适合回答“这个出站最近是否真正承载流量”,不适合直接回答“这个节点当前延迟是多少”。如果某节点没有用户流量,计数长时间不增长并不代表节点故障;如果连接已经建立但远端持续阻塞,计数也可能继续增加。因此,自动切换必须另行发起主动探测。
主动探测最好通过本地 SOCKS 入站完成,而不是在脚本进程中直接连接节点地址。这样测试路径会经过 Xray 的真实路由、DNS 和传输配置。Linux 环境可使用 curl 进行 HTTP 探测:
curl --proxy socks5h://127.0.0.1:10808 \
--connect-timeout 5 \
--max-time 12 \
--retry 0 \
-fsS https://www.example.com/health.txt
使用 socks5h 可以让域名解析交给 SOCKS 代理路径,减少脚本自身 DNS 结果与 Xray 路径不一致的问题。探测目标应选择内容稳定、响应体较小且允许频繁访问的地址。不要只使用 ICMP ping,也不要把公共测速站的大文件下载作为每分钟一次的健康检查,否则测速本身会消耗节点流量并影响正常业务。
节点池、状态机与切换条件
自动切换的核心不是“找到最小延迟”,而是维护每个节点的状态。建议至少使用 healthy、suspect、down 和 cooldown 四种状态。单次超时只进入 suspect;连续三次失败才进入 down;节点恢复时连续两次成功后才重新标记 healthy。这样可以避免偶发丢包造成频繁抖动。
推荐方案:控制面与数据面分离
控制面
- 监听 127.0.0.1:10085
- 读取 StatsService
- 维护失败与恢复计数
- 调用 HandlerService
数据面
- 固定 proxy-active 标签
- SOCKS 端口 10808
- 路由始终指向活动出站
- 切换后重新建立新连接
业务路由不需要知道当前节点地址,脚本只负责替换活动出站并记录切换原因。
节点选择可以采用加权评分,而不是单纯按延迟排序。一个实用的评分公式是:成功探测得分为 延迟毫秒 + 丢包惩罚 + 最近切换惩罚。例如延迟为 180 毫秒、最近一次失败次数为 1 的节点,可能比延迟 150 毫秒但刚刚恢复的节点更适合继续观察。生产环境中稳定性通常比几十毫秒的差异更重要。
| 状态 | 进入条件 | 业务处理 | 离开条件 |
|---|---|---|---|
| healthy | 连续两次探测成功 | 允许选为活动节点 | 单次失败进入 suspect |
| suspect | 出现一次超时或 5xx | 继续观察,不立即切换 | 成功回 healthy,连续失败进 down |
| down | 连续三次失败 | 禁止新连接使用 | 冷却结束后进入恢复探测 |
| cooldown | down 节点等待 60 秒 | 低频探测,不参与选主 | 连续两次成功回 healthy |
切换还需要设置最短保持时间。例如当前节点刚完成切换后的 120 秒内,即使另一个节点只快了 20 毫秒,也不要再次切换。只有当前节点探测失败、返回连续错误,或候选节点的评分明显低于当前节点时,才允许执行切换。这个“滞回”设计是避免生产环境频繁重连的关键。
结论:先防抖,再追求低延迟
自动切换系统最常见的问题不是不会找到可用节点,而是切换过于频繁。将失败阈值、恢复阈值和最短保持时间写成明确参数,比继续增加测速频率更能提高可用性。
用脚本完成探测、摘除与恢复
下面给出一套便于改造成生产脚本的实现流程。脚本可以使用 Python 的 gRPC 生成代码调用 HandlerService,也可以由现有运维程序封装 API 调用。由于 Xray API 是 protobuf gRPC 服务,不能把它误写成普通的 POST /api/stats 请求。部署前应准备与当前 Xray-core 版本匹配的 API protobuf 代码,并固定依赖版本。
固定出站标签
让所有业务路由指向
proxy-active,节点池中的真实地址、端口、用户标识和传输参数保存在脚本可读的配置文件中。执行代理探测
每 30 秒通过
socks5h://127.0.0.1:10808请求小型健康检查资源,记录耗时、HTTP 状态、异常类型和时间戳。更新节点状态
单次失败只增加失败计数,连续 3 次失败才标记 down;恢复探测连续成功 2 次后才重新允许选主。
调用运行时 API
通过 HandlerService 的
AlterOutbound修改proxy-active的出站配置,成功后重新发起一次探测确认生效。记录切换结果
将旧节点、新节点、触发原因、API 返回结果和切换耗时写入结构化日志,便于回溯而不是只记录“切换成功”。
伪代码可以先把状态机验证清楚,再接入具体的 gRPC stub。以下逻辑强调三个细节:当前活动节点失败时才触发切换;候选节点必须已经通过恢复阈值;切换失败不能直接把节点标记为健康。
INTERVAL = 30
FAIL_LIMIT = 3
RECOVER_LIMIT = 2
COOLDOWN = 60
MIN_HOLD = 120
for node in node_pool:
result = probe_through_xray(node)
if result.ok:
node.fail_count = 0
node.success_count += 1
if node.state in ("down", "cooldown") and node.success_count >= RECOVER_LIMIT:
node.state = "healthy"
else:
node.success_count = 0
node.fail_count += 1
if node.fail_count >= FAIL_LIMIT:
node.state = "down"
node.cooldown_until = now + COOLDOWN
current = state.active_node
if current.state == "down" and now - state.last_switch >= MIN_HOLD:
candidate = choose_lowest_score(
node for node in node_pool
if node.state == "healthy" and node.cooldown_until <= now
)
if candidate:
payload = build_outbound_config(candidate)
alter_outbound("proxy-active", payload)
confirm = probe_through_xray(candidate)
if confirm.ok:
state.active_node = candidate.name
state.last_switch = now
write_event("switch", current.name, candidate.name)
AlterOutbound 修改的是运行中的出站配置,不会神奇地把已经建立的 TCP 连接迁移到新节点。切换完成后,旧连接可能继续维持,也可能因旧链路故障而报错;新建连接才会使用新出站。因此,如果业务要求快速恢复,应在切换后让上层连接池或应用主动重试,而不是期待 Xray 把既有连接无损搬迁。
如果当前 Xray-core 版本或封装语言不方便调用 AlterOutbound,可以采用“生成完整 JSON、校验、受控重启”的兜底方案,但这会中断所有连接,恢复时间也更长。无论使用哪种方式,切换前都应保存当前配置快照,并先进行 JSON 语法检查。不要让脚本直接覆盖唯一配置文件,更不要在多个并发探测任务中同时重载核心。
生产环境中的权限、并发与回滚
API 端口只监听回环地址是最低要求。若 Xray 运行在容器中,127.0.0.1 的含义是容器内部回环地址,宿主机上的脚本可能无法访问;此时应使用同一容器内的控制进程,或通过受限的容器网络连接,不要把 10085 直接映射到公网。
脚本需要加入单实例锁。一次探测尚未完成时,下一轮不能再次发起切换,否则两个任务可能分别选择不同候选节点,最后状态文件与 Xray 实际出站不一致。可以使用文件锁、systemd 服务的单进程约束,或在脚本内部使用互斥锁。节点状态文件也应采用临时文件写入后原子替换,避免主机断电留下半截 JSON。
日志至少记录以下字段:
- 时间:使用带时区的 ISO 8601 时间,不要只记录本地时分秒。
- 节点:记录本地别名和稳定标识,不要把 UUID、订阅凭据或完整敏感链接写入日志。
- 结果:区分 DNS 失败、连接超时、TLS 错误、HTTP 错误和 API 错误。
- 动作:记录 probe、mark-down、switch、recover 和 rollback 等事件。
- 耗时:记录探测耗时与 API 调用耗时,便于发现控制面阻塞。
切换失败时应立即回滚到上一个已验证配置,而不是继续遍历节点并快速重载核心。若旧节点仍能建立连接,可以保留它作为临时回退;若旧节点已经确认 down,则应进入“无可用节点”状态并报警。报警阈值可以设置为连续 5 分钟没有健康节点,或 3 次切换均未通过切换后的确认探测。
常见问题与故障判断
StatsService 没有返回 proxy-active 统计怎么办?
先确认出站标签确实是 proxy-active,再检查统计配置是否启用了出站流量计数。没有真实流量时计数可能不存在或保持为零,先通过 SOCKS 入站发起一次测试请求。
节点延迟很低,为什么仍然被脚本摘除?
延迟只代表探测请求的耗时。脚本还会检查 DNS、TLS、HTTP 状态和完整响应;如果返回 5xx、证书错误或响应体校验失败,节点仍应进入 suspect 或 down。
调用 AlterOutbound 后旧连接没有断开正常吗?
正常。运行时修改主要影响新建连接,已经建立的连接不会自动迁移。需要快速恢复时,让应用连接池重建连接,或者在明确接受中断的情况下重启对应服务。
恢复节点刚上线又马上被切走怎么办?
提高恢复阈值并增加冷却时间,例如连续成功 2 次、冷却 60 秒、最短保持 120 秒。恢复探测应与生产探测分开,不能一次成功就立即参与选主。