Theory · Microservice · Load Balancing
拿到健康实例列表之后,请求到底给谁 —— 策略从"平均分"进化到"按能力分"
把流量按实例的处理能力分配,最大化吞吐与利用率;难点不在"分",在信息少(不知道实例此刻的真实负载)
静态(轮询/加权/哈希)→ 动态(最少连接/EWMA)→ 双样本采样 P2C(微服务内最常用的高性价比答案)
gRPC 长连接粘滞、重试放大、长尾请求——负载不均往往不是算法问题,而是连接与故障语义问题
Topologies
L4 vs L7
| 维度 | L4(传输层) | L7(应用层) |
|---|---|---|
| 决策依据 | IP + 端口 + 协议(TCP/UDP) | HTTP path/header/方法/body 语义、gRPC :path |
| 分流粒度 | 连接级(建连时定,整条连接绑死后端) | 请求级(每个请求可换后端) |
| 性能 | 极快(内核转发,如 IPVS/eBPF、GSO) | 要解析协议,开销高一个量级 |
| 高级能力 | 无(不懂 HTTP) | 路由重写、灰度、限流、熔断、压缩、WAF |
| 典型实现 | LVS/IPVS、云 NLB、kube-proxy iptables/IPVS、Cilium eBPF | Nginx/Envoy/APISIX、云 ALB、Mesh gateway |
| 典型位置 | 入口第一跳(扛量)+ K8s 内 Service 转发 | 网关层 + 服务间 Mesh |
L4 是连接级分流——gRPC 一条 HTTP/2 长连接跑所有并发请求,建连时绑定的那个后端会吃下全部流量。要么升 L7(请求级),要么客户端 LB(每个客户端自己按请求挑实例)——这就是 K8s ClusterIP(L4)+ gRPC 的经典失衡链路。
公网入口:L4(扛连接与 DDoS)→ L7 网关(路由/灰度/认证)→ 服务;K8s 内部:Service L4(简单场景)或 headless+客户端 LB / Mesh L7(治理场景)。层数按需叠加,每层都有成本。
Static Policies
| 策略 | 机制 | 适用与坑 |
|---|---|---|
| 轮询 RR | 依次分发 | 实例同质时够用;请求耗时差异大时失真(慢请求堆积在"轮到"的实例) |
| 加权轮询 WRR | 按权重比例分发 | 异构硬件/灰度权重;权重静态,不感知实时负载 |
| 随机 Random | 随机挑一个 | 实现最简、无状态;大样本下趋近均匀——Nginx 社区实测常不输轮询 |
| 源地址哈希 | hash(client_ip) | 会话保持;但客户端 IP 分布不均 → 后端流量不均,且扩缩容会重排 |
假设"每个请求等价、每个实例等价"。现实是请求耗时方差大(混合了 1ms 的查缓存和 200ms 的写库)——均匀地"分个数"不等于均匀地"分负载"。这引出动态策略的动机:按实例当前状态决策。
需要"同一用户打到同一实例"(有状态缓存)时,优先用 一致性哈希(下一页)而不是源地址哈希;更优解是把状态外置(Redis),让任何实例都能服务——有状态是不得已,无状态是架构追求。
面试一句话:"静态策略解决'怎么把个数分均匀',动态策略解决'怎么把负载分均匀'——请求耗时的方差越大,静态策略失真越严重。"
Dynamic Policies
挑当前活跃连接/在途请求最少的实例。利用"在途数≈负载"的代理指标。
盲区:请求耗时不均时失真——挂 3 个慢请求可能比挂 10 个快请求更忙。
指数加权移动平均记录每实例的 RTT,挑"加权代价"最小的:代价 = 延迟 × 在途数惩罚。
ewma = α*cost + (1-α)*ewma // go-zero P2C 内部即用 // EWMA 记录实例延迟
α 衰减系数兼顾敏感与平滑(防抖)。
随机挑两个实例,选负载较低的那个。一次 O(1) 采样,近似全局最优。
理论:单随机大概率平凡,两里选优显著优于平均——无需全局排序、无中心协调。
① 决策 O(1),无需全局优先队列;② 信息需求最小:每实例一个指标(在途数或 EWMA);③ 数学上两随机点选优已是压倒性收益;④ gRPC/Envoy/go-zero 全内置。指标常配 EWMA——"正在变慢"的实例自动降权。
客户端可感知:在途数、最近 RTT(EWMA)、失败率——免费实时。服务端上报:CPU/QPS(Envoy LRS)——更准但要链路。实践首选客户端本地指标,服务端上报作增强。
Consistent Hashing
Health
/healthz / gRPC Health Protocol)① 全局摘除要有比例上限(如最多摘 50%)——防止网络抖动把全集群摘光,服务反而自愈;② 恢复要渐进放量;③ 摘除是"减少尝试"不是"拒绝服务"——客户端仍要保留重试到其他实例的能力
健康度不只 0/1:Envoy 按健康分数调权重(health score → weight),go-zero 按成功率调权——"半健康"实例少接流量而不是直接踢掉,容量利用率更高
注册中心摘除(心跳超时)是粗粒度拓扑维护;LB 层摘除(探测/统计)是细粒度流量决策——两层叠加才有"秒级发现 + 真实视角"
Root Causes
HTTP/2 多路复用让"一个连接 = 全部并发",L4 建连分流后流量固化。表现:轮询策略下 Pod CPU 依然 90/10/5/2。
解法:请求级 LB(L7/客户端)> 连接周期重建(缓解)> headless+SDK。
下游抖动 → 失败重试 → 请求量翻倍 → 更多失败 → 雪崩。重试让"慢实例"接到的流量反而更多。
解法:重试预算(重试流量 ≤ 10-20% 总量)+ 失败率动态降权 + 幂等约束(详见容错/幂等 deck)。
P99 的慢请求把实例"占满",轮询计数器还在按个数分发。均匀个数 ≠ 均匀占用。
解法:按在途数/延迟分发(LC/P2C)天然补偿长尾;慢请求限并发(舱壁隔离,见容错 deck)。
① 先看 QPS 分布还是 CPU 分布不均——QPS 均、CPU 不均 → 请求耗时方差问题(策略改动态/P2C);② QPS 就不均 → 连接粘滞/哈希倾斜(升级 LB 层级);③ 抖动后不均 → 重试放大(查重试配置与预算)。先分类现象,再对号入座,别一上来就说换算法。
P2C/EWMA 依赖"实例指标",若客户端长时间不刷新实例列表,已下线实例还在候选集(打到死);反之新实例还没热就满流量(冷启动抖动)。解法:列表 watch 秒级更新 + 新实例预热权重(Nacos 权重/Envoy slow start 模式)。
Practice
| 档位 | 做法 | 代价 |
|---|---|---|
| ① pickfirst(默认) | 解析一个地址建一条连接 | 完全不分流——只适合单实例场景 |
| ② headless + 内置 rr/p2c | DNS 返回全部 Pod IP,客户端逐请求挑 | DNS 缓存延迟(headless 53s TTL);实现简单 |
| ③ 自定义 Resolver+Balancer | 对接注册中心,P2C/EWMA 客户端 LB | 自研成本;最灵活(go-zero/Kratos 即此路) |
| ④ xDS / Mesh | 控制面下发实例与策略,请求级 L7 分流 | 引入 Mesh/控制面组件(见网格 deck) |
档位按治理需求递增:headless+rr 是最低成本的解失衡方案;需 P2C/EWMA 才写自定义 Balancer;要灰度/全局观测才上 xDS/Mesh。
dnsPolicy: ClusterFirst + 短 TTL / watch endpoint 消除 DNS 缓存窗口// Balancer 三件事:
// 1. SubConn 管理(连接池)
// 2. Picker: 每 RPC 调 Pick()
// p2c: rand 两实例比 ewma/inflight
// 3. 状态回传: 失败率/EWMA
base.Balancer{Name:"p2c_ewma", Build:..., Pick:...}
实践结论:同质无状态服务 + 请求耗时方差小 → headless+rr 就够;耗时方差大或异构硬件 → P2C+EWMA;要灰度/熔断/全局观测 → xDS/Mesh。不要为 10 个实例的简单服务上 Mesh。
Interview QA
轮询假设请求同质;最少连接的"在途数"会被慢请求扭曲。P2C 随机两实例选优:决策 O(1)、每实例只需一个本地指标、数学上"两随机点较差者"显著优于单点期望;配合 EWMA 延迟还能捕捉"正在变慢"。gRPC/Envoy/go-zero 全内置——微服务 LB 的默认答案。
解决扩缩容时的全量重映射:普通取模在 N 变化时几乎全部 key 洗牌,一致性哈希只影响相邻弧段(理想 1/N)。代价:节点少时环上分布倾斜(需虚拟节点)、故障转移逻辑要自己写(弧段 key 归下一个节点)、故障切换期间该弧段短暂不可用。
按"是否需要请求语义"选:纯扛量/TLS 卸载/长连接透传 → L4(快一个量级,内核转发);要路由/灰度/限流/熔断/gRPC 治理 → L7。标准组合 L4 在前扛量、L7 在后治理。gRPC 长连接在纯 L4 下必失衡,必须 L7 或客户端 LB。
两个独立问题:① 新实例没流量——客户端实例列表刷新慢(DNS 缓存/watch 延迟),解法 headless+endpoint watch;② 新实例接满流量被打挂——冷启动没有预热(JIT/缓存/连接池),解法慢启动权重(Nacos weight 递增/Envoy slow start,按运行时长线性放大权重)。
重试会让失败请求"再找别的实例",放大全链路流量。配齐三件:重试预算(重试量占总请求 ≤10-20%,超预算直接快速失败)、失败实例动态降权(被动健康)、只重试幂等请求。单独看 LB 或单独看重试都行不通,两者是一体设计。
能行但通常不这么做:内部调用走中心网关多一跳 RTT、网关容量成为全站瓶颈、治理策略受限(网关难做按调用方区分的细策略)。内部标准做法是客户端 LB(SDK/Mesh);网关留给南北向流量。例外:强管控诉求(统一审计/安全域隔离)的少量内部接口可过网关。
Related & References