跳到内容

7.3 负载均衡算法、健康检查与重试边界

请求涌到信标塔入口后,单个后端很快排起长队,值守员请你们设计分流和故障摘除规则。

Load balancer 在一组 backend 中选择 request 或 connection 的去向。它的目标不是让每台 machine 每秒绝对平均,而是在 capacity、latency、availability、affinity 和 cost 约束下得到可控的 service behavior。

1. L4 与 L7 看到的 information 不同

维度L4L7
主要观测source/destination address、port、transport connectionHTTP method、host、path、field,gRPC method 等
常见转发单位connection / packet flowrequest / RPC / stream,取决于 protocol
TLS可 passthrough要按 encrypted application data routing 时通常需 termination
能力NAT/DSR、connection distributioncontent 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。

python
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:

  1. 将 backend 标记为 not ready / draining;
  2. 停止分配新 request/connection;
  3. 等待 in-flight work 完成,设置最长 grace period;
  4. 取消或迁移仍未完成的 long-lived stream;
  5. 再退出 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. 验收问题

  1. HTTP/2/gRPC workload 中,least connections 为什么可能选错 backend?
  2. 区分 liveness、readiness 和 deep dependency check。
  3. 为一次 deployment 设计 slow start 与 connection draining。
  4. 两层各 retry 3 次会产生多少 potential attempt?如何用 retry budget 约束?
  5. 什么 workload 适合 rendezvous hashing,什么 workload 更适合 least requests?

参考

Built with VitePress | Software Systems Atlas