负载均衡器
负载均衡器是一组具名的出站。路由到它的 tag 的方式与路由到出站相同,etemenanki-app 会为每条新流从中挑选一个成员。后台探测器以固定间隔对每个成员发起 TCP 连接检查,挑选时优先选择最近一次探测成功的成员。
如果你有不止一台上游代理服务器,希望其中一台不可达时流量仍能继续(failover),或者希望把连接分散到多台服务器上(round_robin),就可以使用负载均衡器。
两台 Trojan 服务器,primary 可达时优先使用它:
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "primary"protocol = "trojan"server = "proxy1.example.com"port = 443
[outbound.stream]network = "tls"
[outbound.settings]password = "replace-with-a-long-random-password"
[[outbound]]tag = "backup"protocol = "trojan"server = "proxy2.example.com"port = 443
[outbound.stream]network = "tls"
[outbound.settings]password = "replace-with-a-long-random-password"
# Failover: "primary" while it answers, "backup" otherwise.[[balancer]]tag = "proxy"outbounds = ["primary", "backup"]
[route]default = "proxy"
[[route.rule]]outbound = "direct"cidr = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]私有地址直连,其余流量都交给负载均衡器 proxy。只要到 proxy1.example.com:443 的 TCP 连接能建立,它就使用 primary,否则使用 backup。strategy 默认为 failover。每个成员的探测超时为 5 秒,上一次探测结束 30 秒后开始下一次。
这里特意显式设置了 [route].default。不设置时,默认出站是文件中的第一个 [[outbound]],在本例中就是 direct。负载均衡器永远不会成为隐式的默认出站。
每个 [[balancer]] 都是顶层的表数组条目,与 [[outbound]] 并列。它只接受下列键,其他任何键都是解析错误,例如 unknown field `probe_intervall`, expected one of `tag`, `outbounds`, `strategy`, `probe_interval`, `probe_timeout` 。
| 键 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
tag | string | 是 | — | 负载均衡器的名称。[route].default 和 [[route.rule]].outbound 引用它的方式与引用出站完全相同。它不能与任何 [[outbound]] 的 tag 重复,也不能与另一个负载均衡器的 tag 重复;两种情况都报 balancer tag <tag> collides with an outbound tag。 |
outbounds | array of strings | 是 | — | 成员出站,按 tag 精确匹配。至少一个(否则报 a balancer needs at least one outbound)。只接受 [[outbound]] 的 tag:不存在的 tag 或另一个负载均衡器的 tag 都报 balancer <tag> references unknown outbound tag: <member>。每个成员都必须有可供 TCP 连接探测的 server 和 port。hysteria2 成员一律被拒绝;freedom、blackhole 和 wireguard 成员通常没有 server 和 port,因此也被拒绝;两种情况都报 balancer <tag>: outbound <member> has no upstream a TCP health probe can reach, so it cannot be balanced。在 failover 策略下,列表顺序就是优先级。同一个 tag 写两次也会被接受,并按两个成员计算。 |
strategy | string (enum) | 否 | "failover" | 为每个新的流挑选健康成员的方式:failover 取 outbounds 顺序中第一个健康成员,round_robin 依次轮流使用各个健康成员。按原样匹配,区分大小写;其他值(例如 "roundrobin" 或 "Failover")报 unknown balancer strategy "<value>" (expected "failover" or "round_robin")。 |
probe_interval | u64 | 否 | 30 | 一个成员的一次健康探测结束后,等待多少秒再开始下一次。每个成员按各自的节奏探测,配置启动后立即进行第一次探测。0 也会被接受,此时探测一次接一次、没有间隔,会持续不断地向上游建立连接。 |
probe_timeout | u64 | 否 | 5 | 一次探测(包括域名解析)最多可用的秒数,超时即认为该成员不可用。0 也会被接受,但一次探测只剩到下一个定时器 tick(约 1 毫秒)为止的时间,除非连接几乎立即建立,否则成员会被标记为不可用。请至少设为 1。 |
probe_interval 和 probe_timeout 以整秒为单位,写成 TOML 整数。probe_interval = "30s" 会报 invalid type: string "30s", expected u64,负数会报 invalid value: integer `-1`, expected u64。
负载均衡器在流被分派给它时才挑选成员,而不是在构建路由时。因此成员宕机后,只要探测器发现,它就不再接收新流,无需重载。
failover(默认) |
round_robin |
|
|---|---|---|
| 选择 | 按 outbounds 顺序的第一个健康成员 |
轮转中的下一个健康成员 |
| 列表顺序 | 即优先级 | 只决定轮转顺序 |
| 优先成员恢复时 | 新流回到它上面 | 它重新加入轮转 |
| 典型用途 | 一台主服务器加一台或多台备用服务器 | 多台对等的服务器 |
| 所有成员都不可用 | outbounds 中的第一个成员 |
outbounds 中的第一个成员 |
flowchart TB
F["新的流被路由到负载均衡器"] --> H{"有健康的成员吗?"}
H -- 否 --> First["outbounds 中的第一个成员"]
H -- 是 --> S{"strategy"}
S -- failover --> FO["按列表顺序的第一个健康成员"]
S -- round_robin --> RR["轮转中的下一个健康成员"]
First --> D["通过选中的出站拨号"]
FO --> D
RR --> D
由此带来几点结果:
- 按流选择。 一条 TCP 连接会一直留在它建立时所用的成员上,直到关闭,即使该成员之后宕机,或者更高优先级的成员恢复。只有新连接会切换。
- 对 UDP 而言,选择发生在 UDP 关联通往负载均衡器的子链路打开时。 在该子链路结束前,这个关联一直使用同一个成员。参见 UDP 如何到达出站。
- 所有成员都不可用时,仍然使用第一个成员。 发往一个可能已经恢复的上游的流还有机会成功,被丢弃的流则毫无机会。负载均衡器不会因为探测短暂出问题就变成黑洞。
- 拨号失败不会换到其他成员重试。 拨号失败时,这条流已经交给了该成员的出站,无法在别处重放。客户端会看到失败,并自行重试。
round_robin使用一个共享计数器,在当时健康的成员之间轮转。健康成员的集合变化后,轮转在新集合上继续。同一个 tag 列出两次,它在轮转中就占两个位置。
每个负载均衡器为它的每个成员各运行一个后台任务进行探测:
- 用
[dns]中配置的解析器解析该成员的server(参见 DNS)。 - 向
server:port建立一个普通 TCP 连接,依次尝试每个解析出的地址,一旦连上就立即关闭。它不会应用该成员的[outbound.stream]设置。 - 如果在
probe_timeout秒内(包括域名解析)有一次连接成功,该成员就是健康的。解析失败、所有地址都拒绝连接或超时,都会使它被标记为不可用。 - 从这次探测结束时起等待
probe_interval秒,再进行下一次探测。
配置一启动就进行第一次探测。在成员的第一次探测完成之前,它被视为健康:如果启动时把所有成员都标记为不可用,整整一个间隔内流量将无处可去。
状态变化以 info 级别记录日志:
balancer member primary is now downbalancer member primary is now up最小示例中的故障切换过程如下:
sequenceDiagram participant P as 探测器 participant A as primary participant B as backup participant R as 负载均衡器 proxy P->>A: TCP 连接成功 Note over R: 新流发往 primary P->>A: TCP 连接被拒绝或超时 Note over P,R: 日志 - balancer member primary is now down Note over R: 新流发往 backup P->>A: TCP 连接再次成功 Note over P,R: 日志 - balancer member primary is now up Note over R: 新流回到 primary
流量多快切换
Section titled “流量多快切换”探测器只有在下一次探测时才能发现变化。从上游消失到负载均衡器把它标记为不可用之间,新流仍会发往它并失败。使用默认值时,这个窗口最长约 35 秒:30 秒的 probe_interval,如果上游静默丢包而不是拒绝连接,再加上最多 5 秒的 probe_timeout。恢复也按同样的节奏被发现。
调低 probe_interval 可以更快响应,代价是每个间隔内每个成员都要向各自的上游建立一个 TCP 连接。probe_interval = 1 和 probe_timeout = 1 是有实际意义的最小值。
还有几点细节:
- 探测忽略成员的
address_family。 它按解析器返回的顺序尝试所有地址,包括 IPv4 和 IPv6,第一个连上的地址就让该成员被视为健康。设为ipv4_only的成员可能通过 IPv6 探测为健康,而它的流量只能使用 IPv4。 - 一个慢地址就可能让整次探测失败。 各地址依次尝试,只受整体的
probe_timeout约束。如果第一个地址静默丢包,它的连接会耗尽全部超时时间,该成员被标记为不可用,即使后面的地址本可以响应。 - 上游服务器能看到探测。 每次探测都是一个不发送任何数据就关闭的 TCP 连接,每个成员每隔
probe_interval一次。 - 成员按负载均衡器区分。 同一个出站列在两个负载均衡器中会被探测两次,各由一个负载均衡器负责,每个负载均衡器各自维护对其健康状态的判断。
- 即使没有任何流量路由到负载均衡器,探测也会运行。
--test不进行探测。etemenanki-app --test -c config.toml检查负载均衡器的配置是否正确,而不检查其成员是否可达。
哪些出站可以作为成员
Section titled “哪些出站可以作为成员”成员必须有一个可供探测连接的 TCP 端点。
| 协议 | 可作为成员 | 原因 |
|---|---|---|
socks、http、trojan、vless、vmess、shadowsocks |
是 | 它们通过 TCP 拨号到 server 和 port |
freedom(direct)、blackhole(block) |
否 | 没有可探测的上游 server 和 port |
wireguard |
否 | 它没有 server 和 port;其设置中的对端 endpoint 是 UDP |
hysteria2(hysteria、hy2) |
否,一律拒绝 | 它有 server,但只监听 UDP,TCP 探测会让它永远被标记为不可用 |
被拒绝的成员会导致配置无法加载。etemenanki-app --test 报告:
configuration invalid: balancer proxy: outbound direct has no upstream a TCP health probe can reach, so it cannot be balanced这项检查看的是成员有没有 server 和 port,而不是它的协议,只有 hysteria2 是按名称拒绝的。带有多余 server 和 port(这些协议本身会忽略它们)的 freedom、blackhole 或 wireguard 出站会被接受,并按该地址探测,而它的流量仍然直连、被丢弃或发往 WireGuard 对端。不要依赖这一行为。
在路由中使用负载均衡器
Section titled “在路由中使用负载均衡器”凡是能用出站 tag 的地方,都能用负载均衡器的 tag:
- 在
[route].default中,使它成为未匹配任何规则的流的兜底出站; - 在
[[route.rule]].outbound中,把匹配的流发给它。
它不能出现在另一个负载均衡器的 outbounds 中:负载均衡器不能嵌套。路由中引用的 tag 如果既不是出站也不是负载均衡器,会报 route references unknown outbound tag: <tag>。规则如何匹配参见 路由。
下面的示例把 example.com 的流量轮流分散到两台 VLESS 服务器上,并比默认值更频繁地探测它们:
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "edge-a"protocol = "vless"server = "proxy1.example.com"port = 443
[outbound.stream]network = "ws"security = "tls"
[outbound.stream.ws]path = "/ws"
[outbound.settings]id = "11111111-2222-3333-4444-555555555555"
[[outbound]]tag = "edge-b"protocol = "vless"server = "proxy2.example.com"port = 443
[outbound.stream]network = "ws"security = "tls"
[outbound.stream.ws]path = "/ws"
[outbound.settings]id = "11111111-2222-3333-4444-555555555555"
[[balancer]]tag = "spread"outbounds = ["edge-a", "edge-b"]strategy = "round_robin"probe_interval = 10probe_timeout = 3
[[route.rule]]outbound = "spread"domain_suffix = ["example.com"]direct 仍是默认出站,因为它是第一个出站,且没有设置 [route].default。
当文件内容发生变化,且新配置能够解析并构建成功时,就会发生重载。每次这样的重载都会像配置的其他部分一样重建所有负载均衡器。旧的探测器随旧配置停止,新的探测器随新配置启动,因此每个成员都重新从健康状态开始,并立即被探测。如果新配置失败,旧配置会连同其负载均衡器及健康状态继续运行。
日志中的重载摘要只列出入站、出站、路由和日志的变化。因此只修改了 [[balancer]] 的重载会记录为 config reload: no changes,尽管新设置已经生效,并且与其他任何重载一样,旧配置的连接会被关闭。参见 热重载。
从 Xray 迁移
Section titled “从 Xray 迁移”[[balancer]]是顶层表,不属于路由部分。outbounds列出精确的 tag,没有 Xrayselector那样的前缀匹配。- 只有两种策略,
failover和round_robin,必须一字不差地这样写。Xray 的random、leastPing和leastLoad没有对应项。 - 没有
fallbackTag。所有成员都不可用时,负载均衡器使用它的第一个成员。 - 健康检查是内置的,直接在负载均衡器上配置。没有单独的 observatory,探测是一次 TCP 连接,而不是经由代理发出的 HTTP 请求。
其余差异参见 从 Xray 迁移。
以下错误都会导致配置无法加载。etemenanki-app --test -c config.toml 会记录 configuration invalid: 加上错误消息,正常启动时则记录 failed to start: 加上错误消息。重载时,错误消息跟在 reload: parse failed, keeping current config: (缺少字段的错误及其他 TOML 错误)或 reload: build failed, keeping current config: (其余错误)之后,旧配置继续运行。
| 消息 | 原因 | 解决方法 |
|---|---|---|
balancer tag <tag> collides with an outbound tag |
负载均衡器的 tag 与某个出站的 tag 或另一个负载均衡器的 tag 重复 |
给它起一个唯一的名称 |
balancer <tag> references unknown outbound tag: <member> |
某个成员拼写错误、未定义,或者本身是负载均衡器 | 只列出 [[outbound]] 的 tag |
balancer <tag>: outbound <member> has no upstream a TCP health probe can reach, so it cannot be balanced |
hysteria2 成员,或者 freedom、blackhole、wireguard 成员(通常没有 server 和 port) |
把它移出负载均衡器,改为直接路由到它 |
a balancer needs at least one outbound |
outbounds = [] |
至少列出一个成员 |
unknown balancer strategy "<value>" (expected "failover" or "round_robin") |
策略拼写错误或大小写不对 | 准确写成 failover 或 round_robin |
missing field `outbounds` / missing field `tag` |
缺少必填键 | 补上该键 |
route references unknown outbound tag: <tag> |
某条规则或 [route].default 引用了不存在的负载均衡器 |
修正 tag,或定义该负载均衡器 |