Theory · Microservice · Load Balancing

负载均衡

拿到健康实例列表之后,请求到底给谁 —— 策略从"平均分"进化到"按能力分"

一句话本质

把流量按实例的处理能力分配,最大化吞吐与利用率;难点不在"分",在信息少(不知道实例此刻的真实负载)

策略光谱

静态(轮询/加权/哈希)→ 动态(最少连接/EWMA)→ 双样本采样 P2C(微服务内最常用的高性价比答案)

必考坑

gRPC 长连接粘滞、重试放大、长尾请求——负载不均往往不是算法问题,而是连接与故障语义问题

这份 deck 回答"请求分给谁":三种部署形态 → L4/L7 → 静态策略 → 动态策略(重点 P2C/EWMA)→ 一致性哈希 → 健康摘除 → 负载不均的真相(连接粘滞/重试放大/长尾)→ gRPC/K8s 实践 → QA。核心观点:负载均衡的难点是负载信息不足,所以现代方案(P2C+EWMA+被动健康检查)都围绕"用最少信息做最优决策"。

Topologies

三种部署形态:LB 放在哪决定了它的能力

负载均衡三种部署形态 形态一中间代理:请求先到中心负载均衡器再转发后端。形态二边缘层:DNS 或 Anycast 把流量分散到多入口。形态三客户端内嵌:调用方进程内置负载均衡逻辑,配合服务发现直连实例,无中间跳。 A · 中间代理 MIDDLE PROXY —— Nginx/云LB/K8s Service(默认形态) 客户端 LB 代理 后端 ① 后端 ② 后端 ③ 集中式:简单通用、语言无关;代价:多一跳延迟、LB 自身容量与单点治理 B · 边缘层 EDGE TIER —— DNS 轮询/Anycast/多入口(面向全球或高防) 客户端 入口 北京 入口 上海 DNS/Anycast 按地理与健康分流;粒度粗(IP 级),配合形态 A 使用 C · 客户端内嵌 CLIENT-SIDE —— gRPC 内置 LB / 微服务 SDK / Sidecar(Mesh 形态) 调用方进程 SDK:发现+LB+熔断 实例 ① 实例 ② 无中间跳、请求级决策、策略最丰富;代价:SDK 与语言绑定(Mesh 用 Sidecar 把 SDK 挪走)
三种形态来自 Google SRE 论文的经典分类:中间代理(集中式,简单通用,多一跳)、边缘层(DNS/Anycast 地理分流,粒度粗)、客户端内嵌(无中间跳、请求级决策,SDK 与语言绑定)。实战常组合:边缘层做地理入口 + 中间代理做七层网关 + 客户端内嵌做服务间调用。Mesh 的本质是把 C 的 SDK 挪进 Sidecar。

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 eBPFNginx/Envoy/APISIX、云 ALB、Mesh gateway
典型位置入口第一跳(扛量)+ K8s 内 Service 转发网关层 + 服务间 Mesh

关键结论:gRPC 在 L4 下必失衡

L4 是连接级分流——gRPC 一条 HTTP/2 长连接跑所有并发请求,建连时绑定的那个后端会吃下全部流量。要么升 L7(请求级),要么客户端 LB(每个客户端自己按请求挑实例)——这就是 K8s ClusterIP(L4)+ gRPC 的经典失衡链路。

标准组合拳

公网入口:L4(扛连接与 DDoS)→ L7 网关(路由/灰度/认证)→ 服务;K8s 内部:Service L4(简单场景)或 headless+客户端 LB / Mesh L7(治理场景)。层数按需叠加,每层都有成本。

L4/L7 对比表六行,核心差异一句话:L4 看得到连接看不到请求,连接级分流;L7 看得到请求语义,请求级分流。关键结论:gRPC 长连接在 L4 下必然失衡(一连接绑一后端),必须升 L7 或客户端 LB。标准组合:公网 L4 扛量 → L7 网关治理 → 服务;K8s 内按需选 Service L4 或 headless+客户端 LB。

Static Policies

静态策略:不看现场,按规则分

策略机制适用与坑
轮询 RR依次分发实例同质时够用;请求耗时差异大时失真(慢请求堆积在"轮到"的实例)
加权轮询 WRR按权重比例分发异构硬件/灰度权重;权重静态,不感知实时负载
随机 Random随机挑一个实现最简、无状态;大样本下趋近均匀——Nginx 社区实测常不输轮询
源地址哈希hash(client_ip)会话保持;但客户端 IP 分布不均 → 后端流量不均,且扩缩容会重排

静态策略的共同缺陷

假设"每个请求等价、每个实例等价"。现实是请求耗时方差大(混合了 1ms 的查缓存和 200ms 的写库)——均匀地"分个数"不等于均匀地"分负载"。这引出动态策略的动机:按实例当前状态决策。

会话保持的正确姿势

需要"同一用户打到同一实例"(有状态缓存)时,优先用 一致性哈希(下一页)而不是源地址哈希;更优解是把状态外置(Redis),让任何实例都能服务——有状态是不得已,无状态是架构追求。

面试一句话:"静态策略解决'怎么把个数分均匀',动态策略解决'怎么把负载分均匀'——请求耗时的方差越大,静态策略失真越严重。"

静态策略四种:轮询、加权轮询、随机(大样本趋近均匀,Nginx 社区实测常不输轮询)、源地址哈希(会话保持但有分布不均与扩缩重排问题)。共同缺陷是假设请求与实例同质,请求耗时方差大时"分个数"不等于"分负载"。会话保持的正解是一致性哈希或状态外置。

Dynamic Policies

动态策略:最少连接 · EWMA · P2C(重点)

1 最少连接 LC

挑当前活跃连接/在途请求最少的实例。利用"在途数≈负载"的代理指标。

盲区:请求耗时不均时失真——挂 3 个慢请求可能比挂 10 个快请求更忙。

2 EWMA 延迟感知

指数加权移动平均记录每实例的 RTT,挑"加权代价"最小的:代价 = 延迟 × 在途数惩罚。

ewma = α*cost + (1-α)*ewma
// go-zero P2C 内部即用
// EWMA 记录实例延迟

α 衰减系数兼顾敏感与平滑(防抖)。

3 P2C 幂之两种选择

随机挑两个实例,选负载较低的那个。一次 O(1) 采样,近似全局最优。

理论:单随机大概率平凡,两里选优显著优于平均——无需全局排序、无中心协调。

为什么 P2C 成为微服务 LB 事实标准

① 决策 O(1),无需全局优先队列;② 信息需求最小:每实例一个指标(在途数或 EWMA);③ 数学上两随机点选优已是压倒性收益;④ gRPC/Envoy/go-zero 全内置。指标常配 EWMA——"正在变慢"的实例自动降权。

动态策略的信息来源

客户端可感知:在途数、最近 RTT(EWMA)、失败率——免费实时。服务端上报:CPU/QPS(Envoy LRS)——更准但要链路。实践首选客户端本地指标,服务端上报作增强。

动态策略三连:最少连接用"在途数"代理负载但被慢请求欺骗;EWMA 平滑记录 RTT 抓住"正在变慢"的实例;P2C 随机两选优——O(1) 决策、信息需求最小、数学上显著优于单点随机,因此成为 gRPC/Envoy/go-zero 的事实标准。信息来源讲客户端本地指标(在途数/EWMA/失败率)优先于服务端上报。

Consistent Hashing

一致性哈希:扩缩容时别把所有映射打乱

一致性哈希环与虚拟节点 左图普通哈希:节点数变化时几乎所有 key 的映射被重算打乱。右图哈希环:节点与 key 都映射到环上,key 顺时针归属最近节点;节点下线只影响其弧段内的 key;虚拟节点把每个物理节点展开为多个环上点,解决节点分布不均问题。 A · 普通取模 hash(key) % N N = 3 → N = 4 key → hash % 3 ≠ hash % 4 ≈ 全部 key 重新洗牌 缓存场景 = 缓存雪崩(全量回源) 分库分表场景 = 数据全量迁移 B · 一致性哈希环 + 虚拟节点 hash 空间(0 → 2^32) A B C D key₁ key₂ key 顺时针归属最近节点 虚拟节点 A → A1 A2 … A100 物理节点少时环上点 稀疏 → 数据倾斜 vnode 平滑归属并 支持按机器性能加权 (Redis Cluster 槽、 Ketama 同思想) 结论:节点增删只影响相邻弧段的 key(理想情况 1/N),而不是全部洗牌——代价是需要 vnode 修均匀、需要故障转移逻辑(弧段 key 迁移到下一个节点)。
一致性哈希讲三件事:普通取模在节点数变化时全部 key 洗牌(缓存全量回源=雪崩);哈希环让节点增删只影响相邻弧段(理想 1/N);虚拟节点解决小集群倾斜并支持加权。应用四大场景:缓存分片、分库分表、LB 会话保持、Redis Cluster 槽模型。追问常问"数据倾斜怎么办"——答 vnode 数量与加权。

Health

健康检查与摘除:LB 的"负向决策"同样重要

主动健康检查(out-of-band)

  • LB/SDK 周期探测实例(TCP 探活 / HTTP /healthz / gRPC Health Protocol)
  • 连续 N 次失败 → 摘除;恢复 → 延迟放量回归(防抖动)
  • 优点:故障发现早(不等真实请求);缺点:探测本身有成本与误判(探测通了不代表业务通)

被动健康检查(in-band / outlier detection)

  • 统计真实请求的失败率/延迟,异常实例被逐出(Envoy 的 outlier detection:连续 5xx 即 eject 一段时间)
  • 优点:反映真实业务视角("能建立 TCP 但业务 500"也能抓到);缺点:以牺牲部分真实请求为代价
  • 两者组合:主动探测兜底冷启动故障,被动统计精确打击真实异常——Envoy/Mesh 的标准做法

摘除三纪律

① 全局摘除要有比例上限(如最多摘 50%)——防止网络抖动把全集群摘光,服务反而自愈;② 恢复要渐进放量;③ 摘除是"减少尝试"不是"拒绝服务"——客户端仍要保留重试到其他实例的能力

权重联动

健康度不只 0/1:Envoy 按健康分数调权重(health score → weight),go-zero 按成功率调权——"半健康"实例少接流量而不是直接踢掉,容量利用率更高

与注册中心的关系

注册中心摘除(心跳超时)是粗粒度拓扑维护;LB 层摘除(探测/统计)是细粒度流量决策——两层叠加才有"秒级发现 + 真实视角"

健康检查两模式:主动探测(发现早,可能误判)、被动统计(真实视角,牺牲部分请求),Envoy 的 outlier detection 是被动代表。摘除三纪律必讲:摘除比例上限(防网络抖动摘光集群)、恢复渐进放量、摘除是减少尝试而非拒绝服务。权重联动的"半健康降权"是进阶答案。与注册中心摘除的分工:拓扑维护 vs 流量决策。

Root Causes

负载不均的三个真相(不是算法选错了)

1 连接粘滞

HTTP/2 多路复用让"一个连接 = 全部并发",L4 建连分流后流量固化。表现:轮询策略下 Pod CPU 依然 90/10/5/2。

解法:请求级 LB(L7/客户端)> 连接周期重建(缓解)> headless+SDK。

2 重试放大

下游抖动 → 失败重试 → 请求量翻倍 → 更多失败 → 雪崩。重试让"慢实例"接到的流量反而更多

解法:重试预算(重试流量 ≤ 10-20% 总量)+ 失败率动态降权 + 幂等约束(详见容错/幂等 deck)。

3 长尾请求

P99 的慢请求把实例"占满",轮询计数器还在按个数分发。均匀个数 ≠ 均匀占用。

解法:按在途数/延迟分发(LC/P2C)天然补偿长尾;慢请求限并发(舱壁隔离,见容错 deck)。

诊断思路(面试讲这个 = 工程经验)

① 先看 QPS 分布还是 CPU 分布不均——QPS 均、CPU 不均 → 请求耗时方差问题(策略改动态/P2C);② QPS 就不均 → 连接粘滞/哈希倾斜(升级 LB 层级);③ 抖动后不均 → 重试放大(查重试配置与预算)。先分类现象,再对号入座,别一上来就说换算法。

客户端缓存路由的次生问题

P2C/EWMA 依赖"实例指标",若客户端长时间不刷新实例列表,已下线实例还在候选集(打到死);反之新实例还没热就满流量(冷启动抖动)。解法:列表 watch 秒级更新 + 新实例预热权重(Nacos 权重/Envoy slow start 模式)。

负载不均三大根因:连接粘滞(HTTP/2 复用 + L4 分流)、重试放大(慢实例反而接更多流量)、长尾请求(个数均匀不等于占用均匀)。诊断思路是这页的灵魂:先分清 QPS 不均还是 CPU 不均,对号入座再动策略。次生问题讲实例列表过期与冷启动,解法是 watch 秒级更新 + 预热权重。

Practice

gRPC 与 K8s 场景的落地方案梯度

gRPC 的四档方案(由简到繁)

档位做法代价
① pickfirst(默认)解析一个地址建一条连接完全不分流——只适合单实例场景
② headless + 内置 rr/p2cDNS 返回全部 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。

K8s 里最常踩的三个开关

  • headless Service(clusterIP: None)让 DNS 返回全部 Pod——gRPC 客户端 LB 的前提
  • dnsPolicy: ClusterFirst + 短 TTL / watch endpoint 消除 DNS 缓存窗口
  • HPA 新 Pod:客户端尽快感知(watch EndpointSlice),否则空转

Go 实现要点(自研 LB 模块)

// 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。

gRPC 四档方案梯度:pickfirst(默认,不分流)→ headless+内置 rr/p2c → 自定义 Resolver+Balancer(对接注册中心,P2C/EWMA)→ xDS/Mesh。K8s 三个开关:headless、DNS 缓存消除、扩容感知。Go 自研 LB 讲 gRPC Balancer 接口三件事:SubConn 管理、Picker 决策、状态回传。实践结论按规模与方差选档位。

Interview QA

高频 QA

Q1 为什么 P2C 优于轮询/最少连接?

O(1) 决策信息最少抗慢请求

轮询假设请求同质;最少连接的"在途数"会被慢请求扭曲。P2C 随机两实例选优:决策 O(1)、每实例只需一个本地指标、数学上"两随机点较差者"显著优于单点期望;配合 EWMA 延迟还能捕捉"正在变慢"。gRPC/Envoy/go-zero 全内置——微服务 LB 的默认答案。

Q2 一致性哈希解决了什么?代价是什么?

1/N 迁移倾斜与 vnode

解决扩缩容时的全量重映射:普通取模在 N 变化时几乎全部 key 洗牌,一致性哈希只影响相邻弧段(理想 1/N)。代价:节点少时环上分布倾斜(需虚拟节点)、故障转移逻辑要自己写(弧段 key 归下一个节点)、故障切换期间该弧段短暂不可用。

Q3 L4 和 L7 负载均衡怎么选?

连接级 vs 请求级

按"是否需要请求语义"选:纯扛量/TLS 卸载/长连接透传 → L4(快一个量级,内核转发);要路由/灰度/限流/熔断/gRPC 治理 → L7。标准组合 L4 在前扛量、L7 在后治理。gRPC 长连接在纯 L4 下必失衡,必须 L7 或客户端 LB。

Q4 服务扩容后新实例没流量/老实例打挂?

冷启动列表刷新

两个独立问题:① 新实例没流量——客户端实例列表刷新慢(DNS 缓存/watch 延迟),解法 headless+endpoint watch;② 新实例接满流量被打挂——冷启动没有预热(JIT/缓存/连接池),解法慢启动权重(Nacos weight 递增/Envoy slow start,按运行时长线性放大权重)。

Q5 负载均衡和重试怎么配合才不会放大故障?

重试预算失败降权

重试会让失败请求"再找别的实例",放大全链路流量。配齐三件:重试预算(重试量占总请求 ≤10-20%,超预算直接快速失败)、失败实例动态降权(被动健康)、只重试幂等请求。单独看 LB 或单独看重试都行不通,两者是一体设计。

Q6 微服务内部用网关做 LB 行不行?

多一跳规模瓶颈

能行但通常不这么做:内部调用走中心网关多一跳 RTT、网关容量成为全站瓶颈、治理策略受限(网关难做按调用方区分的细策略)。内部标准做法是客户端 LB(SDK/Mesh);网关留给南北向流量。例外:强管控诉求(统一审计/安全域隔离)的少量内部接口可过网关。

六题:P2C 胜出逻辑、一致性哈希代价(倾斜与转移)、L4/L7 选择、扩容冷启动(列表刷新与预热是两个独立问题)、重试与 LB 的一体设计(预算+降权+幂等)、内部是否过网关(多一跳与瓶颈,标准是客户端 LB)。

Related & References

相关知识点与参考

本领域相关 deck

跨领域相关 deck

  • Redis Cluster —— 槽模型 = 虚拟节点思想的产品化
  • TCP 连接管理 —— 连接粘滞与 L4 分流的网络层基础
  • —— 全局"最少连接"实现的最小堆数据结构

参考链接(一手来源)

收尾链接:领域内是发现(输入)、RPC(粘滞根源)、容错(重试预算)、网关(南北向)、Mesh(下沉);跨领域对照 Redis Cluster 槽(vnode 产品化)与 TCP 连接管理。参考一手来源:Google SRE 的 LB 三形态与 overload 处理、gRPC LB 文档、Envoy arch overview、Karger 1997 原始论文。