7.3 负载均衡算法、健康检查与重试边界
请求涌到信标塔入口后,单个后端很快排起长队,值守员请你们设计分流和故障摘除规则。
Load balancer 在一组 backend 中选择 request 或 connection 的去向。它的目标不是让每台 machine 每秒绝对平均,而是在 capacity、latency、availability、affinity 和 cost 约束下得到可控的 service behavior。
1. L4 与 L7 看到的 information 不同
| 维度 | L4 | L7 |
|---|---|---|
| 主要观测 | source/destination address、port、transport connection | HTTP method、host、path、field,gRPC method 等 |
| 常见转发单位 | connection / packet flow | request / RPC / stream,取决于 protocol |
| TLS | 可 passthrough | 要按 encrypted application data routing 时通常需 termination |
| 能力 | NAT/DSR、connection distribution | content routing、header policy、auth integration、request retry |
| 成本与风险 | application parsing 少 | 更多 CPU/memory、protocol state 与 config complexity |
L4/L7 不是 product 的绝对分类,一条 production path 可以同时有 L4 frontend 和 L7 proxy。TLS passthrough 下 L7 proxy 看不到 encrypted HTTP path;如果用 SNI 做 limited routing,也不等于已经拥有全部 HTTP semantics。
2. Scheduling algorithm 要和 workload 匹配
Round robin / weighted round robin
实现简单,适合 backend capacity 和 request cost 接近的情况。它不知道某台 backend 上的上一批 request 是否很慢。Weight 可表示 capacity 差异,但 weight 不是 realtime load sensor。
Least connections / least requests
用 active work 数作 signal,对 duration 差异大的 workload 比 pure round robin 更合理。但一个 connection 可承载多个 HTTP/2/gRPC stream,所以 least connections 不一定反映 request load。同样,active requests 也没表示每个 request 的 CPU/IO cost。
Latency-aware / EWMA
使用 recent latency 的 moving estimate 区分 slow backend。必须防止 low-traffic node 因 sample 少而被过度优待,也要将 error、in-flight work 和 warm-up 放进 decision。
Power of two choices
随机选两个 candidate,再从中选 load 更小的一个。它在大 backend pool 中可用很小的查询成本避免最差 hotspot,但效果仍取决于 load metric 是否有意义。
Consistent / rendezvous hashing
用 stable key 让同一 tenant、cache key 或 shard key 尽量落在同一 backend,并在 membership 变化时减少 remapping。它是 affinity/sharding tool,不是默认的负载最小算法。Hot key 会制造 hotspot,节点 capacity 不同时还需 weighted design。
3. Rendezvous hashing 的最小实验
Rendezvous hashing(highest-random-weight hashing)对每个 key/node pair 计算 stable score,选分数最高的 node。移除 node 时,原先不属于它的 key 不需要 remap。
from collections import Counter
import hashlib
def score(key: str, node: str) -> int:
digest = hashlib.sha256(f"{key}\0{node}".encode()).digest()
return int.from_bytes(digest, "big")
def choose(key: str, nodes: list[str]) -> str:
if not nodes:
raise ValueError("nodes must not be empty")
return max(nodes, key=lambda node: score(key, node))
keys = [f"tenant-{i}" for i in range(10_000)]
before_nodes = ["a", "b", "c"]
after_nodes = ["a", "c"]
before = {key: choose(key, before_nodes) for key in keys}
after = {key: choose(key, after_nodes) for key in keys}
remapped = {key for key in keys if before[key] != after[key]}
assert all(before[key] == "b" for key in remapped)
print("before:", Counter(before.values()))
print("after: ", Counter(after.values()))
print("remapped:", len(remapped) / len(keys))Hash result 不等于每个 node 精确获得 1/N key,而 key 数量均匀也不等于 traffic/cost 均匀。Production design 要看 key popularity、node weight、replication 和 failure behavior。
4. Health check 不是“process 活着”
Active check
Load balancer 主动调用 health endpoint。应分开:
- liveness:process 是否需要 restart;
- readiness:是否可以接收新 traffic;
- deep dependency check:用于 diagnostic,不一定适合直接决定 routing。
如果 readiness 对每个 shared downstream 都做强依赖,downstream 抖动可能让所有 backend 同时不 ready,把 partial degradation 变成 total outage。
Passive check / outlier detection
根据真实 request 的 connection error、status 和 latency 暂时 eject anomalous backend。需要 minimum sample、ejection cap 和 recovery policy,否则 load balancer 可在全局 overload 时把 backend 一台台剔除,加速崩溃。
5. Startup、shutdown 和 membership change
New backend 通过 health check 后不一定已经 warm:JIT、connection pool、cache 和 lazy initialization 可使它的早期 latency 很高。Slow start 逐步提高 traffic weight,避免一上线就被洪峰击穿。
Planned removal 不应立即 reset existing connection:
- 将 backend 标记为 not ready / draining;
- 停止分配新 request/connection;
- 等待 in-flight work 完成,设置最长 grace period;
- 取消或迁移仍未完成的 long-lived stream;
- 再退出 process。
WebSocket/gRPC long-lived connection 会让“停止新 connection”与“停止新 application operation”不再等价,需要 protocol-aware draining 和 reconnect signal。
6. Timeout、retry 与 load amplification
Load balancer retry 可以隐藏 transient failure,也可以将一次 request 放大成多次 backend work。多层 retry 叠加时,attempt 数可乘法增长。
Retry policy 要和以下内容一起审查:
- operation 是否 idempotent,是否有 idempotency key;
- overall deadline 和 per-try timeout;
- 哪些 failure 在 request body/response header 已部分传输后仍可 retry;
- max attempts、backoff、jitter 和 retry budget;
- Circuit breaker 和 load shedding;
- Original attempt 是否可能仍在 backend 运行。
Hedged request 会在 slow attempt 未失败时就发第二份,只适合严格受控的 idempotent read,并需要 percentile threshold 与 extra-load budget。不应把它当作通用 tail-latency 开关。
7. Session affinity 是 migration cost,不是 free feature
Cookie、source IP 或 consistent hash 可以将 caller 粘到 backend,但会导致 uneven load、failure remap 和 scale-in difficulty。Source IP 在 NAT/proxy 后可让大量 user 看起来是同一 client。
优先将 durable session state 放在 shared/stateful subsystem,让 stateless compute 能被任意调度。确实需要 affinity 时,定义 TTL、backend failure behavior、rebalance 和 data consistency,别把它留成 implicit property。
8. 验收问题
- HTTP/2/gRPC workload 中,least connections 为什么可能选错 backend?
- 区分 liveness、readiness 和 deep dependency check。
- 为一次 deployment 设计 slow start 与 connection draining。
- 两层各 retry 3 次会产生多少 potential attempt?如何用 retry budget 约束?
- 什么 workload 适合 rendezvous hashing,什么 workload 更适合 least requests?