feat(reverseproxy): add upstream balancing and failover

This commit is contained in:
wjqserver 2026-04-02 14:40:56 +08:00
parent 59f190ce3a
commit 919236665b
4 changed files with 1394 additions and 116 deletions

View file

@ -59,7 +59,11 @@ r.ANY("/api/*path", touka.ReverseProxy(touka.ReverseProxyConfig{
```go
type ReverseProxyConfig struct {
Target *url.URL
Target *url.URL
Targets []string
LoadBalancing ReverseProxyLoadBalancingConfig
PassiveHealth ReverseProxyPassiveHealthConfig
Transport http.RoundTripper
FlushInterval time.Duration
@ -78,12 +82,115 @@ type ReverseProxyConfig struct {
### `Target`
必填。表示后端目标地址,至少需要提供 `scheme``host`
`Targets` 二选一。表示单个后端目标地址,至少需要提供 `scheme``host`
```go
target, _ := url.Parse("http://backend:9000")
```
### `Targets`
可选。用于配置多个后端目标地址。
- `Target``Targets` 互斥,只能使用其中一种
- `Targets` 的每一项都必须是完整 URL
- 每个 target 仍然可以自带 base path 和 query
```go
r.ANY("/api/*path", touka.ReverseProxy(touka.ReverseProxyConfig{
Targets: []string{
"http://127.0.0.1:9001/base?from=a",
"http://127.0.0.1:9002/base?from=b",
},
}))
```
这意味着不同 upstream 仍然可以保留各自的路径前缀和固定查询参数。
### `LoadBalancing`
用于配置 upstream 选择策略和重试行为。
```go
type ReverseProxyLoadBalancingConfig struct {
Policy ReverseProxyLBPolicy
Retries int
TryDuration time.Duration
TryInterval time.Duration
}
```
当前内置策略:
- `touka.LBRandom()`
- `touka.LBRoundRobin()`
- `touka.LBFirst()`
- `touka.LBLeastConn()`
- `touka.LBIPHash()`
- `touka.LBClientIPHash()`
- `touka.LBURIHash()`
- `touka.LBHeader("X-Upstream", fallback)`
- `touka.LBQuery("tenant", fallback)`
其中:
- `LBFirst()` 适合主备/故障转移顺序
- `LBHeader` / `LBQuery` 只有在对应 header/query **缺失**时才会走 fallback
- 如果 `LBHeader` / `LBQuery` 没有显式 fallback则默认回退到 `LBRandom()`
```go
r.ANY("/api/*path", touka.ReverseProxy(touka.ReverseProxyConfig{
Targets: []string{
"http://127.0.0.1:9001",
"http://127.0.0.1:9002",
},
LoadBalancing: touka.ReverseProxyLoadBalancingConfig{
Policy: touka.LBHeader("X-Upstream", touka.LBFirst()),
Retries: 1,
},
}))
```
重试说明:
- 只对未开始收到上游响应的失败进行重试
- 默认仅对 RFC 定义的安全方法(`GET` / `HEAD` / `OPTIONS` / `TRACE`)重试
- `Retries` 表示额外重试次数
- `TryDuration` 表示总尝试时间预算;如果配置了它,会优先于重试次数控制停止时机
- `TryInterval` 表示两次重试之间的等待间隔
### `PassiveHealth`
用于配置被动健康检查。它不会后台探测 upstream而是根据真实代理请求的失败结果临时把某个 upstream 视为不健康。
```go
type ReverseProxyPassiveHealthConfig struct {
FailDuration time.Duration
MaxFails int
UnhealthyStatus []int
}
```
- `FailDuration > 0` 时启用被动健康跟踪
- `MaxFails <= 0` 时默认按 `1` 处理
- `UnhealthyStatus` 中的状态码会被记为一次失败,但当前请求仍会先收到该响应;后续请求才会绕过这个 upstream
```go
r.ANY("/api/*path", touka.ReverseProxy(touka.ReverseProxyConfig{
Targets: []string{
"http://127.0.0.1:9001",
"http://127.0.0.1:9002",
},
LoadBalancing: touka.ReverseProxyLoadBalancingConfig{
Policy: touka.LBFirst(),
},
PassiveHealth: touka.ReverseProxyPassiveHealthConfig{
FailDuration: time.Minute,
UnhealthyStatus: []int{http.StatusServiceUnavailable},
},
}))
```
### `Transport`
可选。用于自定义底层转发所使用的 `http.RoundTripper`
@ -150,6 +257,8 @@ r.ANY("/api/*path", touka.ReverseProxy(touka.ReverseProxyConfig{
在请求真正发往后端前,对出站请求做最后修改。
如果启用了多 upstream 重试,`ModifyRequest` 可能会在同一个客户端请求里被调用多次:每一次实际发往 upstream 的尝试都会重新构造一份请求并再次执行它。因此,这个回调最好保持幂等,不要依赖“只会执行一次”的副作用。
常见用途:
- 覆盖 `Host`