9SRE 工作手册

负载均衡

从 DNS 到应用层

一致性哈希P2C健康检查全局负载均衡

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第9章 负载均衡

从 DNS 到应用层

章节导读

2017 年 2 月 28 日,AWS S3 发生了长达四小时的大规模故障,起因是一位工程师在执行运维命令时输错了参数,移除了远超预期的服务器容量。但真正让这次事故成为经典案例的,是它的传播方式:S3 的故障导致其内部依赖的状态子系统恢复缓慢,而全球数十万个网站的状态页、监控工具、甚至 AWS 自己的服务状态仪表盘都托管在 S3 上——包括那个用来告诉用户「S3 出问题了」的状态页本身。流量在故障期间涌向这些依赖组件,负载均衡和容量设计的缺陷被放大到极致。这次事故揭示了一个朴素而深刻的真理:在现代分布式系统中,负载均衡不是一个可选的优化项,而是系统可靠性的骨架——它决定了流量如何在组件间分配、故障如何被隔离、容量如何被利用。蓝皮书第 9 章系统地讨论了这个骨架应该如何搭建。负载均衡的本质是什么?最直观的回答是「把请求分发到多台服务器」。但这个答案遗漏了更重要的维度。负载均衡实际上在同时解决四个问题:可扩展性(通过水平分发让系统容量随机器数增长)、可用性(后端故障时把流量导向健康节点)、资源效率(让所有后端均匀工作而不是几台累死几台闲死)、以及故障隔离(防止局部故障通过流量传导放大为全局故障)。理解这四个目标之间的张力,是理解所有负载均衡设计决策的钥匙——例如「资源效率」与「可用性」的冲突:为了效率你希望后端负载尽量满,为了可用性你需要预留容量应对节点故障,两者不可兼得,只能权衡。负载均衡发生在多个层次,每一层有不同的机制和权衡。DNS 层:通过域名解析把用户导向不同的数据中心或入口 IP,这是全局负载均衡(GLB)的主要手段。DNS 层的优势是天然的全局视野和极低的成本,劣势是缓存导致的控制延迟——DNS 记录的 TTL 决定了你「切流」的生效速度,TTL 设 300 秒意味着故障后最长 5 分钟还有用户被导向坏节点(实际上由于各级 DNS 缓存不遵守 TTL,可能更久)。L4 层(传输层):基于 IP 和端口转发,不解析应用协议,性能极高(单机可达数百万并发连接),Google 的 Maglev 就是这一层的杰作——用网络硬件和一致性哈希实现的分布式负载均衡器,支撑着 Google 全部对外流量。L7 层(应用层):解析 HTTP/gRPC 等应用协议,能基于 URL、Header、Cookie 做智能路由,能实现请求级的负载均衡(而非连接级),代价是更高的处理开销。Google 的 GFE(Google Front End)工作在 L7,除了负载均衡还承担 TLS 终结、缓存、安全防护等边缘职能。应用层内部:服务网格和 RPC 框架里的客户端负载均衡——调用方直接从名字服务获取后端列表,自己决定每个请求发给谁,省去了中间跳数,这是微服务架构的主流模式。负载均衡算法是本章的技术核心,每种算法对应不同的场景。轮询(Round Robin)最简单:请求依次分给每个后端。它在「请求成本均匀」的假设下工作良好,但现实往往不均匀——有的请求查缓存 1ms 返回,有的请求做复杂计算 1s 才返回,轮询会让慢请求在后端堆积。加权轮询按后端能力分配权重,缓解但不根治这个问题。最少连接(Least Connections)动态选择当前活跃连接最少的后端,自动适应请求成本差异,但需要维护连接计数,且在「羊群效应」下多个负载均衡器实例可能同时选中同一个「最闲」节点。最少请求(Least Requests)是它的轻量变体,用采样近似。一致性哈希(Consistent Hashing)基于请求的某个键(用户 ID、URL)哈希到后端,保证相同键的请求总是到达同一后端——这对缓存友好(后端本地缓存命中率高)和有状态服务(会话数据在固定节点)至关重要。经典一致性哈希在节点增减时只影响相邻节点的映射,配合虚拟节点(每个物理节点对应哈希环上多个虚拟位置)可以解决负载不均。Google 在 Maglev 中使用了「有界负载的一致性哈希」:允许节点负载超过平均值一个上限后溢出到环上的下一个节点,兼顾缓存亲和与负载均衡。P2C(Power of Two Choices,两随机选择)是大规模系统的最爱:随机选两个后端,把请求发给其中负载较低的那个。这个看似偷懒的算法有深刻的数学基础——纯随机选择的负载不均衡度是 O(log n / log log n),而 P2C 把它指数级降到 O(log log n),且实现极其简单、无需全局状态。Envoy、gRPC、Finagle 等主流框架都提供 P2C。健康检查决定「哪些后端可以接收流量」,分为主动和被动两类。主动健康检查是负载均衡器周期性探测后端(HTTP GET /healthz、TCP 连接、gRPC Health Checking Protocol),连续失败则摘除节点。被动健康检查(异常点检测,outlier detection)从真实流量中学习:某后端的错误率或延迟显著劣于同伴时将其临时驱逐。两者互补:主动检查发现「死透了」的节点(进程崩溃、网络不通),被动检查发现「半死不活」的节点(进程活着但 GC 卡顿、磁盘变慢、依赖超时)——后者恰恰是生产中最常见也最难检测的故障形态。健康检查的设计细节充满陷阱:检查频率与摘除速度的权衡(太灵敏会因瞬时抖动误摘节点引发流量震荡,太迟钝让坏节点持续伤害用户);检查端点的语义(/healthz 应该检查什么?只查进程活着,还是连数据库连通性一起查?查得太深,数据库抖动会导致所有节点同时被摘——健康检查的级联故障);摘除与恢复的滞回设计(连续 N 次失败摘除、连续 M 次成功恢复,N 和 M 不对称以防抖)。连接管理是负载均衡的另一个深水区。TCP 连接的建立成本(三次握手加 TLS 握手可达数百毫秒)决定了连接复用的重要性:客户端连接池、负载均衡器与后端间的长连接、HTTP/2 的多路复用都在减少握手开销。但长连接引入了新问题——连接级的负载均衡在长连接场景失效:一个 HTTP/1.1 长连接上的所有请求都固定在同一后端,如果连接分布不均(比如后端扩容后,新节点拿不到存量连接的流量),负载就会倾斜。这就是为什么 gRPC(基于 HTTP/2 单连接多路复用)特别需要请求级的客户端负载均衡,以及为什么「定期重建连接」(connection draining 加最大连接寿命)是长连接系统的标配。全局负载均衡(GLB)把视野从单数据中心扩展到全球。Google 的 GLB 体系分三层:DNS 把用户导向最近的 Anycast IP,Maglev 在数据中心内把流量分发到 GFE,GFE 再根据后端服务的全球负载和健康状况做最终路由。这套体系支撑了 Google 的「用户无感故障切换」:某数据中心整体故障时,DNS 和 Anycast 在分钟级把流量导向其他区域,用户最多感受到短暂延迟上升。GLB 的核心权衡是「就近接入」(延迟最低)与「负载导向」(容量最富余的区域接流量)——极端情况下两者冲突:最近的区域过载了,是把用户导向更远但空闲的区域(延迟升高但可用),还是让近端区域硬扛(可能雪崩)?Google 的答案是分级:正常时就近,容量水位超阈值时按预设顺序溢出到次近区域,这个「容量感知的路由策略」需要全球实时的容量水位数据支撑。本章最后讨论几个经典陷阱。热点(hotspot):某个键的流量远超其他键(明星微博、秒杀商品),一致性哈希会把这个键的全部流量压到单个后端——解法是热点键的特殊处理(拆分、缓存、专属资源池)。惊群(thundering herd):大量客户端在故障恢复瞬间同时重连重试,把刚恢复的系统再次打垮——解法是重试抖动、渐进式恢复、预热。会话亲和(session affinity)的诱惑与代价:把用户会话粘在固定后端简化了状态管理,但牺牲了负载均衡的灵活性,且节点故障时会话丢失——现代架构的方向是「无状态服务加外部会话存储」,让亲和性成为优化手段(缓存命中)而非正确性依赖。负载均衡是那种「平时看不见、出事就是大事」的基础设施。它的设计质量决定了你的系统在故障时是优雅降级还是雪崩,在增长时是线性扩展还是处处瓶颈。接下来的内容将深入每个主题:各层次的机制细节、算法的数学直觉与工程实现、健康检查的参数调优、连接管理策略、GLB 架构,以及 Google Maglev 和 GFE 的设计案例。

核心概念

  • 一致性哈希
  • P2C
  • 健康检查
  • 全局负载均衡

要点精析

负载均衡的层次体系:DNS、L4、L7 与应用层。现代系统的负载均衡是分层协作的体系,每层解决不同粒度的问题。DNS 层工作在域名解析阶段,通过返回不同的 IP 地址实现粗粒度的流量调度——地理就近(把亚洲用户导向东京机房)、容量调度(按权重分配不同机房)、故障切换(摘除故障机房的解析记录)。DNS 的独特优势是「在任何连接建立之前」就完成调度,成本几乎为零;独特劣势是控制力弱:TTL 缓存让切流指令的生效延迟从数分钟到数小时不等(部分运营商 DNS 不遵守 TTL),且 DNS 无法感知单个请求的健康状况。因此 DNS 层只适合做「机房级」的粗调度,精细调度必须下沉。L4 层基于 IP 加端口转发,不解析应用内容,可以用 DSR(直接服务器返回,响应不经过负载均衡器)和 Anycast 实现极高的吞吐——Google Maglev 单机集群支撑数百万 QPS,靠的就是 L4 转发加一致性哈希。L4 的局限是看不见 HTTP 语义:无法按 URL 路由、无法感知请求级错误(TCP 连接成功但 HTTP 500)、长连接场景下无法做请求级均衡。L7 层解析应用协议,能力最丰富:基于路径和 Header 的路由、请求级负载均衡、重试与超时控制、灰度发布(把带特定 Header 的请求导向新版本)、限流熔断。代价是必须终结 TCP 连接(两段式:客户端到 LB、LB 到后端),处理开销和延迟增加。Google 的 GFE 是 L7 边缘层的典范,除负载均衡外还统一承担 TLS 终结、缓存、DDoS 防护——把横切关注点收敛到边缘层,后端服务保持简单。应用层(客户端负载均衡)是微服务时代的主流:调用方从名字服务(Consul、Kubernetes Service、gRPC name resolver)获取后端列表,在客户端进程内直接做每请求的均衡决策,省去了中间代理的跳数和单点。Envoy 作为 sidecar 代理是折中方案:逻辑上是客户端均衡(每个 Pod 一个代理),工程上把均衡逻辑从业务代码剥离。分层的实践原则:DNS 做区域调度,L4/L7 做入口流量管理,应用层做服务间调用均衡——每层各司其职,不要指望一层解决所有问题。

轮询、加权轮询与最少连接:基础算法的适用边界。轮询(Round Robin)是最直观的算法:维护一个后端列表指针,每个请求依次后移。它的隐含假设是「所有请求成本相同、所有后端能力相同」,此时轮询达到完美均衡。现实中两个假设常被打破:请求成本差异(缓存命中 1ms vs 复杂查询 1s)和后端能力差异(新旧机器混部、虚拟机规格不同)。加权轮询(Weighted RR)解决后者:按后端容量分配权重,容量大的多接流量。权重怎么定是关键——按 CPU 核数比例是粗糙起点,更准确的做法是压测得出各规格机型的单实例安全 QPS,按 QPS 比例设权重。最少连接(Least Connections)解决前者:每个请求发给当前活跃连接数最少的后端,慢请求自然堆积在少数后端时,新请求会流向空闲后端,形成自适应均衡。它的变体「加权最少连接」用「连接数除以权重」做比较,兼顾能力差异。最少连接的陷阱是「羊群效应」(herd effect):当多个负载均衡器实例独立决策时,它们可能同时观察到某个后端最闲、同时把请求砸过去——瞬间过载后该后端又变成「最忙」,流量再集体逃离,形成震荡。缓解手段是引入随机性:最少请求算法(Least Requests)随机采样 K 个后端(通常 K=2),选其中连接最少的——这正是 P2C 的思想,采样破坏了全局同步性。选择决策树:请求成本均匀且后端同构,用轮询(最简单、无状态);后端异构,用加权轮询;请求成本差异大,用最少连接或 P2C;有多个独立均衡器实例,优先 P2C 类随机化算法。一个常被忽视的点:这些算法都假设「负载均衡器知道后端的实时状态」,状态数据的采集延迟(连接计数的更新频率、健康状态的传播速度)决定了算法的实际效果——在毫秒级流量波动面前,秒级的状态更新意味着算法永远在追逐过去。

P2C:大规模系统的均衡算法之王。P2C(Power of Two Choices)的算法描述简单到令人起疑:随机挑两个后端,把请求发给负载较低的那个。但它的数学性质极其优美——这是「选择的权力」理论的著名结果。纯随机分配下,n 个后端中最忙节点的负载期望超出平均值 O(log n / log log n);而每次只在两个随机候选中选较优者,最大负载的偏差骤降到 O(log log n)——指数级的改善。直观理解:随机选择给了「运气不好」的可能(连续把请求砸向同一节点),而两次选择让「连续坏运气」的概率平方级衰减。P2C 的工程优势同样突出。第一,无需全局状态:每个均衡器实例独立随机采样,不需要中心协调器维护全局后端负载表——这在有成千上万个客户端实例做均衡的微服务环境里是决定性优势(全局状态的同步成本会压垮系统)。第二,天然抵抗羊群效应:随机性破坏了多实例的决策同步,即使所有实例同时决策,它们的候选对也各不相同。第三,负载信号可以任意组合:比较的「负载」可以是活跃请求数、CPU 使用率、EWMA(指数加权移动平均)延迟、或它们的组合——Envoy 的 LEAST_REQUEST 加 active request bias 就是 P2C 家族的实现。P2C 的已知弱点及修补:其一,「两次都抽到忙节点」时仍会做出次优选择,改进版 P2C+d(power of d choices)增大采样数,但收益递减,d=2 已是性价比最优点;其二,在后端负载信息滞后时(比如用秒级更新的 CPU 指标),P2C 可能集体涌向「刚变空闲」的节点,修补方法是负载信号加随机扰动或使用衰减历史(EWMA 天然平滑);其三,对「新扩容节点」不友好——新节点负载为零,P2C 会持续选中它直到追平,造成冷启动过载(缓存未预热、连接池未建立),修补方法是 slow start(新节点权重在预热期内线性爬升,Envoy 和 gRPC 都支持)。Google、Netflix、Twitter 的负载均衡栈里都能找到 P2C 或其变体,它是「简单算法加深刻数学」的典范——理解 P2C 也就理解了为什么大规模系统偏爱无状态的局部决策而非精确的全局优化。

一致性哈希:缓存亲和与有状态路由。一致性哈希(Consistent Hashing)解决的问题是:如何让「相同的键总是路由到相同的后端」,同时让「后端增减时只影响最小范围的键映射」。经典哈希(hash(key) mod N)在后端数 N 变化时几乎所有键都要重新映射——对缓存集群意味着全量缓存失效、瞬间回源打垮数据库。一致性哈希把后端和键都映射到一个哈希环上,键顺时针找到的第一个后端就是目标;增删节点只影响环上相邻区段的键。工程实现的三个关键点。第一,虚拟节点:物理节点少时,环上的位置分布不均导致负载倾斜——每个物理节点生成 100 到 200 个虚拟位置(hash(node_id + i))均匀散布在环上,负载偏差可控制在 5% 以内。第二,有界负载(bounded load):即使有虚拟节点,热点键仍会让某节点过载——Google 在 Maglev 论文中描述的算法给每个节点设负载上限(如平均负载的 1.25 倍),超限的键溢出到环上的下一个节点,用少量亲和性损失换取负载均衡保证。第三,哈希函数的选择:必须分布均匀且计算快,xxHash、MurmurHash 是主流,MD5/SHA 太重,简单的字符串哈希容易倾斜。一致性哈希的适用场景:分布式缓存(Memcached、Redis Cluster 的键路由)、需要本地状态的服务(会话、聚合窗口)、对象存储的分片定位。不适用场景:无状态服务的普通负载均衡——强行用一致性哈希会牺牲均衡性换取毫无价值的「亲和性」。与 P2C 的组合使用是高级技巧:P2C 选出的两个候选若都基于同一哈希环的相邻区段,可以在均衡的同时保留部分缓存亲和(gRPC 的 ring_hash 策略加 locality weighting 就是这类混合设计)。运维上的一致性哈希陷阱:扩容时的缓存预热——新节点接管区段后缓存全空,回源流量可能打垮后端数据库,正确做法是渐进扩容(一次加一个节点)、预热流量(先影子复制读请求)、或双读回退(新节点未命中时回退读旧节点)。

健康检查:主动探测与被动检测的组合艺术。健康检查回答一个问题:这个后端现在能不能接流量?答案的质量直接决定可用性——误判「健康」让坏节点持续伤害用户,误判「不健康」会摘除好节点减少容量甚至引发全体摘除的雪崩。主动健康检查(active)由负载均衡器周期发起探测:HTTP GET /healthz 期望 200、TCP 建连测试、gRPC Health Checking Protocol。设计要点在检查端点的语义分层:liveness(进程活着吗)与 readiness(能服务请求吗)必须分开——Kubernetes 的 livenessProbe 失败会重启容器,readinessProbe 失败只摘流量;把数据库依赖放进 liveness 是经典错误(数据库抖动导致所有 Pod 被重启,人为制造事故)。readiness 检查的深度也要克制:检查过深(验证所有下游依赖)会让局部故障放大为全局摘除,Google 的建议是 readiness 只检查「本实例服务请求的必要条件」(本地资源、关键初始化完成),下游依赖的故障交给熔断和被动检测处理。被动健康检查(passive,Envoy 称 outlier detection)从真实流量学习:统计每个后端的 5xx 率、连接失败率、延迟劣化,显著偏离同伴时驱逐。它的独特价值是发现「半死不活」的节点——进程活着、/healthz 返回 200,但 GC 频繁卡顿、磁盘 IO 劣化、某个依赖连接池耗尽,这类节点主动检查完全测不出来,却是生产延迟毛刺的头号来源。被动检测的参数设计:连续 N 次失败触发驱逐(N 太小对瞬时抖动过敏)、驱逐时长随驱逐次数指数增长(反复被驱逐的节点隔离更久)、最大驱逐比例上限(如 50%——即使所有后端都「看起来坏」,也保留一半接流量,宁可慢不可全断,这是防雪崩的最后防线)。主动与被动的组合策略:主动检查用较低频率(10 到 30 秒)兜底发现死节点,被动检测实时发现劣化节点;主动检查失败立即摘除(明确的死),被动检测驱逐用较短时长加自动重试探(可能是瞬时劣化)。监控健康检查系统自身同样重要:当前健康后端数、驱逐事件流、健康检查本身的错误率——「多少节点正在被摘除」应该是一条 page 级告警,因为它往往是大面积故障的最早信号。

连接管理与连接池:长连接时代的新问题。TCP 连接建立的成本(一次 RTT 的三次握手,加 TLS 的 1 到 2 个 RTT,跨洋场景可达数百毫秒)决定了连接复用是性能刚需:客户端连接池、服务端 keep-alive、HTTP/2 多路复用、连接预热都在摊薄握手成本。但长连接给负载均衡带来了一个结构性难题——连接级均衡与请求级均衡的分裂。HTTP/1.1 时代,负载均衡器把「连接」分给后端,连接上的所有请求都固定在该后端;连接寿命越长,均衡器对流量分布的控制力越弱。典型事故场景:后端从 10 台扩容到 20 台,存量连接仍固定在旧的 10 台上,新节点零流量——扩容了但容量没涨,旧节点继续过载。解法组合:连接最大寿命(max connection age,如 1 小时强制优雅重建,gRPC 服务端可主动发 GOAWAY 驱动客户端重连)、连接排空(drain:摘除节点时停止新连接但让存量连接自然结束或限期迁移)、以及最彻底的——请求级负载均衡(HTTP/2 多路复用下单连接可承载并发请求流,配合客户端 P2C 每请求选后端)。gRPC 是这个问题最尖锐的场景:默认每个客户端对每个后端只建一条 HTTP/2 连接,L4 负载均衡器看到的「连接分布」完全无法反映「请求分布」——这就是为什么 gRPC 官方推荐客户端负载均衡或 L7 代理(Envoy),而不能简单套用 TCP 负载均衡。连接池的参数设计同样充满权衡:池太小,高峰期频繁建连(延迟毛刺加 TIME_WAIT 堆积);池太大,后端维持海量空闲连接消耗内存和文件描述符,且连接分布倾斜加剧。经验公式:池大小 = 峰值并发请求数 × 平均请求耗时(秒)× 1.5 冗余,并用「池等待队列长度」作为饱和告警指标——请求在池里排队等连接,是容量不足的早期信号。还有一个跨层陷阱:连接空闲超时必须逐层递减对齐——客户端 idle timeout 应小于负载均衡器的,负载均衡器的应小于后端的;若反序(客户端以为连接还活着,LB 已悄悄关闭),就会出现「复用已关闭连接」的周期性报错,这类 bug 的特征是低流量时段错误率反而升高(连接空闲更久),排查时值得牢记。

全局负载均衡:跨数据中心的流量调度。全局负载均衡(GLB)把调度视野扩展到地理尺度,解决三个问题:就近接入(降低网络延迟)、容灾切换(数据中心故障时流量迁移)、容量调度(把流量导向资源富余的区域)。Google 的 GLB 是业界最精密的实现,架构分三层。第一层 DNS:根据用户 Local DNS 的位置(或 EDNS Client Subnet 携带的真实客户端网段)返回最优接入点的 Anycast IP。第二层 Anycast 加 Maglev:同一 Anycast IP 在全球多个机房宣告,BGP 路由天然把用户导向网络拓扑最近的机房;Maglev(L4 分布式负载均衡器)在机房内把流量分发到 GFE 集群。第三层 GFE 加后端选择:GFE(L7)终结连接后,根据后端服务的全球实时负载、健康状态、区域容量水位做最终路由——必要时把请求转发到其他区域的后端(跨区域转发比让用户重新接入更快,因为 Google 骨干网的延迟远低于公网)。这套体系的核心数据基础是「全球容量水位的实时可见性」:每个区域每个服务的可用容量、当前负载、健康后端数汇聚到中央调度决策——没有这个数据面,GLB 就是盲人摸象。容灾切换的分级策略值得学习:单后端故障由本地健康检查处理(秒级);单集群故障由 Maglev/GFE 重新分发(秒到分钟级);单机房故障由 DNS 加 Anycast 收敛流量到其他机房(BGP 收敛分钟级,DNS 缓存可能拖到小时级)——层级越高、生效越慢,因此低层机制必须尽量兜住故障,高层切换是最后手段。GLB 的经典权衡是延迟与容量的冲突:最近区域过载时,是把用户导向更远但空闲的区域(延迟加 50ms 但服务可用),还是让近端硬扛(可能雪崩)?标准答案是容量感知的溢出策略:每个区域设负载水位线(如 70%),超过后新流量按预设的「次近区域」顺序溢出,且溢出决策要防震荡(滞回:水位降到 60% 以下才回切)。另一个实践要点是切流演练:DNS 权重调整、区域排空的流程必须定期演练(Google 会真实地把某机房的流量切走做维护)——没演练过的容灾切换方案,在真实故障时的成功率要打对折。

热点、惊群与会话亲和:三大经典陷阱。热点(hotspot):流量在键维度上的分布几乎从不是均匀的——社交平台的明星账号、电商的秒杀商品、新闻应用的头条事件,单键流量可达平均值的千倍。一致性哈希把单键固定路由到单节点,热点键就意味着热点节点:一台后端被打满,其他节点围观。应对策略分层:检测(实时统计键级流量分布,Top-K 热点键识别)、缓存(热点内容推到边缘 CDN 或多级缓存,让请求根本到不了后端)、拆分(热点键分片:把明星微博的读请求散列到 N 个副本节点,写仍走主节点)、专属池(识别出的超级热点键路由到专用资源池,与普通流量隔离)。Google 处理「Flash Crowd」(突发事件流量洪峰)的经验是自动化热点检测加动态复制——人工响应永远赶不上热点出现的速度。惊群(thundering herd):故障恢复的瞬间,积压的重试请求同时涌向刚恢复的系统,把它再次打垮,形成「恢复-打垮-恢复」的震荡。三个来源三种解法:客户端重试风暴(解法是指数退避加全抖动,尊重 Retry-After,客户端设重试预算——重试流量不超过正常流量的 10%);缓存集体失效(解法是过期时间加随机抖动、 stale-while-revalidate 返回旧值同时异步刷新、单飞(singleflight)合并对同一键的并发回源);连接集体重建(解法是重连抖动、服务端渐进接受连接)。会话亲和(session affinity):把同一用户的请求粘在同一后端。它简化了有状态设计(会话存本地内存),但代价沉重:亲和性破坏均衡(用户流量不均导致后端负载不均)、节点故障即会话丢失(可用性下降)、扩缩容困难(新节点分不到存量用户的流量)。现代架构的共识是「无状态服务加外部会话存储」(Redis、Spanner 存会话),亲和性降级为纯优化手段——比如把同一用户路由到同一节点以提高本地缓存命中率,但任何节点都能服务任何用户(正确性不依赖亲和)。判断你的系统是否过度依赖亲和的测试:随机杀掉一个后端节点,用户是「无感知」还是「被登出/丢购物车」?后者说明状态设计需要重构。这三个陷阱的共同教训是:负载均衡的正确性依赖「流量均匀」的假设,而真实世界的流量充满倾斜、突发和状态依赖——设计时要显式回答「假设被打破时系统如何表现」。

负载均衡器的容量与自身可靠性:谁来负载均衡负载均衡器。负载均衡器位于所有流量的关键路径上,它自身的容量和可用性是系统的天花板——这个「元问题」常被忽视直到出事。负载均衡器的容量规划有三个特殊点。第一,它处理的是「全量流量」:后端服务可以按业务分片各自规划容量,负载均衡器必须扛住入口峰值总和,且故障切换时可能承受多个区域的流量叠加(某机房挂了,其流量涌向其他机房的 LB——LB 容量必须按 N+1 甚至 N+2 规划)。第二,连接数与吞吐是双重约束:L4 LB 的瓶颈常在并发连接表大小和新建连接速率(每秒新建连接数),L7 LB 的瓶颈在 CPU(TLS 加解密、HTTP 解析)——压测必须同时覆盖两个维度。第三,LB 的性能与配置复杂度相关:路由规则、Header 改写、限流规则越多,单请求处理成本越高,容量模型必须反映生产真实配置而非裸转发。负载均衡器自身的高可用设计按层次演进:单机多进程(进程崩溃自动拉起);同机房集群(VRRP/keepalived 虚拟 IP 漂移,或 ECMP 把 VIP 流量散列到多台 LB,Google Maglev 用 ECMP 加一致性哈希实现集群内无状态均衡——任何一台 Maglev 挂了,其流量被其余节点接管且连接映射保持一致);跨机房 Anycast(机房级故障由 BGP 自动收敛)。Maglev 的设计细节值得借鉴:每台 LB 独立计算相同的查找表(输入是后端健康状态的共识视图),表的一致性保证流量在 LB 集群内任意转发都到达相同后端——LB 集群本身没有内部状态同步的瓶颈。运维实践清单:LB 集群的容量水位告警要严于普通服务(70% 即告警,因为它是全局单点);LB 配置变更必须金丝雀(先改一台观察错误率,配置错误是 LB 事故的头号原因);LB 的健康检查风暴要评估(几百台 LB 各自探测几百台后端,探测流量本身可能压垮后端的健康检查端点——协调探测频率或用探测代理);以及最重要的——定期演练「LB 全挂」的预案:DNS 直接解析到后端 VIP 的应急通道是否存在、是否演练过。

负载均衡与 SLO:把均衡质量变成可度量的指标。负载均衡做得好不好,不能靠感觉,要落到与 SLO 挂钩的指标体系上。第一类指标是均衡质量本身:后端负载分布的变异系数(各后端 QPS 或连接数的标准差除以均值,理想值小于 0.1,大于 0.3 说明分布严重不均);最热与最冷后端的负载比(P2C 类算法正常应小于 1.5);扩容后新节点流量爬升到均值的时间(衡量 slow start 与连接重建的效率)。这些指标劣化的常见原因:算法与流量特征不匹配(长连接场景用了连接级均衡)、健康检查误摘导致剩余节点倾斜、热点键击穿一致性哈希。第二类指标是均衡对 SLO 的贡献:后端摘除期间的用户可见错误率(健康检查加故障转移是否真的无缝);切流/扩容期间的延迟毛刺幅度(连接排空和预热是否平滑);区域故障时 GLB 切换的实际耗时与 SLO 消耗(容灾承诺是否兑现)。第三类是负载均衡器自身的 SLI:LB 的可用性、LB 引入的附加延迟(P99 应小于 1ms 量级,L7 加 TLS 场景小于 5ms)、LB 的 5xx 率中「自身产生」与「后端透传」的拆分——这个拆分至关重要,否则 LB 会替后端的故障背锅,掩盖真实问题。把均衡质量纳入 SLO 体系的实践方法:为「负载分布均匀度」设定内部 SLO(如 95% 的时间变异系数小于 0.15),违约触发算法或配置的审查;每次扩容、切流、故障后自动生成分布报告,累积成「均衡质量基线」——偏离基线就是回归信号。Google 的经验教训是:负载均衡的退化往往是渐进的(新机型混部引入权重失配、某客户端版本升级改变了连接行为),没有持续度量就只能在用户投诉后被动排查。让均衡质量可见、可度量、可回归测试,是把负载均衡从「玄学调参」变成「工程学科」的关键一步。

算法选择决策框架:一张综合决策表。面对具体系统如何选择负载均衡算法,可以按以下决策路径。第一步问状态:请求路由需要键亲和吗(缓存、会话、分片数据)?需要则进入一致性哈希分支,并追问热点风险——有热点键可能则必须有界负载加热点处理机制;不需要亲和则进入无状态分支。第二步问流量特征:请求成本均匀吗?均匀且后端同构,轮询足够(简单、可预测、无状态);不均匀(长短请求混合),进入负载感知分支。第三步问架构形态:单一负载均衡器实例还是多实例/客户端均衡?多实例独立决策时,确定性算法(最少连接)会羊群震荡,必须用随机化算法(P2C/最少请求采样);客户端均衡(gRPC、服务网格)天然多实例,P2C 是默认答案。第四步问后端动态性:频繁扩缩容吗?频繁则重视 slow start(新节点保护)和连接重建机制(max connection age);容器环境秒级扩缩,这两项是必配。第五步问性能预算:L4 场景追求极致转发性能,Maglev 式的一致性哈希查找表(O(1) 查表)是标杆;L7 场景 CPU 富余时可以用更精细的负载信号(EWMA 延迟加活跃请求数的组合权重)。几个典型系统的标准答案:无状态 Web API 入口,L7 加 P2C/最少请求;分布式缓存集群,一致性哈希加虚拟节点加有界负载;gRPC 微服务间调用,客户端 P2C 加 slow start 加 outlier detection;全球服务入口,DNS 加 Anycast 加 L4(Maglev 模式)加 L7(GFE 模式)的分层组合;有状态流处理任务分配,一致性哈希加状态迁移协议。最后的元原则:算法选择的成本远低于「配套机制」的成本——健康检查参数、连接管理、热点处理、慢启动,这些配套决定了任何算法的实际效果;只调算法不做配套,等于给漏水的船换帆。决策表应该成为组织内的共享知识:新服务设计评审时,负载均衡方案对照决策表说明选择理由,这比「抄上一个项目的配置」可靠得多。

代码示例

/* 示例 1:Nginx 负载均衡配置(加权 + 健康检查 + 连接管理) */
upstream backend_api {
    /* 加权轮询:按机型容量分配权重 */
    server 10.0.1.11:8080 weight=5 max_fails=3 fail_timeout=10s;
    server 10.0.1.12:8080 weight=5 max_fails=3 fail_timeout=10s;
    server 10.0.1.13:8080 weight=3 max_fails=3 fail_timeout=10s;  /* 旧机型 */
    server 10.0.1.14:8080 backup;  /* 备用节点,主节点全挂才启用 */
    /* 最少连接算法:注释 weight 行改用下面这行 */
    /* least_conn; */
    keepalive 64;              /* 到后端的长连接池,每 worker 64 条 */
    keepalive_timeout 60s;     /* 空闲连接保活时间 */
    keepalive_requests 1000;   /* 单连接最多复用 1000 次请求后重建 */
}
server {
    listen 443 ssl http2;
    location /api/ {
        proxy_pass http://backend_api;
        proxy_http_version 1.1;
        proxy_set_header Connection "";     /* 启用后端 keepalive 必需 */
        proxy_connect_timeout 2s;
        proxy_read_timeout 10s;
        /* 失败重试:最多 2 次,只重试幂等错误 */
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
        proxy_next_upstream_timeout 5s;
    }
}
这份 Nginx 配置浓缩了 L7 负载均衡的核心要素。weight 参数实现加权轮询——注意权重应来自压测得出的各机型单实例安全 QPS 比例(示例中新旧机型 5:3),而非拍脑袋;max_fails=3 加 fail_timeout=10s 是被动健康检查:10 秒窗口内失败 3 次则摘除该节点 10 秒,到期自动放回试探,这个「短摘除加自动恢复」的设计防止瞬时抖动导致长期误摘。backup 节点是容量兜底:只有全部主节点被摘才启用,平时零流量——注意 backup 节点容量通常远小于主集群,它救急不救穷。keepalive 64 建立到后端的连接池,配合 proxy_http_version 1.1 和清空 Connection 头(HTTP/1.0 默认短连接,这两个配置是长连接生效的前提,漏配是 Nginx 性能问题的头号原因);keepalive_requests 1000 强制连接定期重建——这正是解决「长连接负载倾斜」的机制:扩容后新节点能通过连接重建逐步分到流量。proxy_next_upstream 定义了重试语义:只对 error/timeout/502/503 重试(这些都是「请求大概率未被处理」的错误),绝不重试 500(业务逻辑错误,重试无益且可能重复执行副作用);tries=2 和 timeout=5s 限制重试放大——重试是双刃剑,后端过载时的激进重试会雪上加霜,生产环境的重试预算(重试流量占比)应控制在 10% 以内。
/* 示例 2:Envoy 负载均衡与异常点检测配置(YAML) */
clusters:
  - name: payment_service
    connect_timeout: 0.5s
    type: STRICT_DNS
    lb_policy: LEAST_REQUEST     /* P2C 家族:随机选2个,取请求数少者 */
    least_request_lb_config:
      choice_count: 2
    /* 慢启动:新节点 60 秒内权重线性爬升,防冷启动过载 */
    slow_start_config:
      slow_start_window: 60s
    /* 被动健康检查(异常点检测) */
    outlier_detection:
      consecutive_5xx: 5               /* 连续 5 次 5xx 驱逐 */
      interval: 10s                    /* 每 10 秒分析一次 */
      base_ejection_time: 30s          /* 首次驱逐 30 秒 */
      max_ejection_percent: 50         /* 最多驱逐 50% 后端,防雪崩 */
      enforcing_consecutive_5xx: 100
    /* 主动健康检查 */
    health_checks:
      - timeout: 1s
        interval: 10s
        unhealthy_threshold: 3         /* 连续 3 次失败摘除 */
        healthy_threshold: 2           /* 连续 2 次成功恢复 */
        http_health_check:
          path: /healthz/ready         /* readiness 而非 liveness */
          expected_statuses: [{ start: 200, end: 200 }]
    load_assignment:
      cluster_name: payment_service
      endpoints:
        - lb_endpoints:
            - endpoint: { address: { socket_address: { address: 10.0.2.11, port_value: 8080 } } }
            - endpoint: { address: { socket_address: { address: 10.0.2.12, port_value: 8080 } } }
Envoy 这份配置展示了工业级负载均衡的完整机制栈。lb_policy 的 LEAST_REQUEST 加 choice_count: 2 就是标准 P2C——Envoy 的实现是随机抽 2 个健康后端、比较活跃请求数、选少者,无需全局状态即可在多 sidecar 实例环境下抵抗羊群效应。slow_start_config 解决扩容冷启动问题:新加入的节点在 60 秒窗口内权重从零线性爬升到正常值,给缓存预热、连接池建立、JIT 编译留出时间——没有慢启动,P2C 会因新节点「负载最低」而把流量瞬间砸过去,造成新节点延迟尖峰甚至 OOM。outlier_detection 是被动健康检查的教科书配置:consecutive_5xx=5 用「连续」而非「比例」做判据(对低流量后端更稳健);base_ejection_time 30s 且随重复驱逐次数倍增(问题节点隔离越来越久);max_ejection_percent=50 是防雪崩的生命线——即使全部后端都在返回 5xx(可能是下游整体故障而非节点问题),也保留一半节点接流量,宁可慢不可断,避免「全部摘除导致 100% 失败」的最坏结局。主动健康检查的参数体现滞回设计:unhealthy_threshold=3(30 秒才摘除,抗抖动)而 healthy_threshold=2(20 秒即恢复,快速回补容量)——摘除慢、恢复快的不对称是刻意选择。path 用 /healthz/ready 而非 /healthz/live:readiness 失败只摘流量,liveness 语义(该不该重启)是 Kubernetes 探针的职责,两者混淆是健康检查体系的常见架构错误。
/* 示例 3:一致性哈希(Ring Hash)配置与虚拟节点(Envoy YAML) */
clusters:
  - name: cache_cluster
    lb_policy: RING_HASH
    ring_hash_lb_config:
      minimum_ring_size: 1024        /* 环上最少虚拟节点总数 */
      maximum_ring_size: 8388608
    /* 每个物理后端生成 ring_size/后端数 个虚拟节点,
       哈希函数 xxHash,键取自请求 */
    health_checks:
      - timeout: 1s
        interval: 5s
        unhealthy_threshold: 2
        http_health_check: { path: /healthz }
    load_assignment:
      cluster_name: cache_cluster
      endpoints:
        - lb_endpoints:
            - endpoint: { address: { socket_address: { address: 10.0.3.21, port_value: 11211 } } }
            - endpoint: { address: { socket_address: { address: 10.0.3.22, port_value: 11211 } } }
            - endpoint: { address: { socket_address: { address: 10.0.3.23, port_value: 11211 } } }
---
/* 路由侧:指定哈希键为 URL 路径(缓存场景按资源键路由) */
route_config:
  virtual_hosts:
    - name: cache_vhost
      routes:
        - match: { prefix: "/static/" }
          route:
            cluster: cache_cluster
            hash_policy:
              ring_hash_flag: true
              header: { header_name: ":path" }
              terminal: true
Ring Hash 是 Envoy 对一致性哈希的实现,这份配置的关键参数与哈希键选择值得细读。minimum_ring_size=1024 决定虚拟节点总数:3 个物理后端时每个后端约 342 个虚拟位置,负载偏差可控制在个位数百分比;后端数很少(如 3 台)时虚拟节点尤其重要——没有它们,哈希环上三个物理节点的区段长度差异可能达到 2 倍以上。Envoy 的实现细节是「有界负载一致性哈希」的近似:查找表预计算,O(1) 路由,后端健康状态变化时增量重建环。hash_policy 指定哈希键为 :path(URL 路径)——缓存集群的标准选择:相同 URL 的请求总是到达同一台缓存节点,本地缓存命中率最大化;如果键选错(比如按客户端 IP 哈希),同一资源的请求会散到不同节点,缓存命中率崩塌且总内存占用翻倍。哈希键选择的三原则:分布均匀(用户 ID 好,「省份」这种低基数键灾难)、语义正确(缓存按资源键、会话按用户键、分片存储按分片键)、稳定不变(键值在请求生命周期内不变化)。运维上最重要的注意事项是扩缩容的缓存失效半径:Ring Hash 下增加 1 个节点(3 到 4)只迁移约 25% 的键映射,且迁移的键集中在新节点接管区段——但 25% 的缓存集体失效仍可能造成回源洪峰,正确做法是低峰期扩容、配合上游回源限流、或采用双读降级(新节点未命中时回源读旧节点数据)。监控上要为 Ring Hash 集群单独埋点:各节点缓存命中率(偏差大说明环不均或热点键)、单节点 QPS 与均值的比值(有界负载是否频繁触发溢出)。
/* 示例 4:P2C 算法实现(TypeScript 伪代码) */
interface Backend {
  id: string;
  activeRequests: number;   /* 活跃请求数,请求发出加一、完成减一 */
  weight: number;           /* 静态权重(机型容量) */
  healthy: boolean;
  joinedAt: number;         /* 加入时间,用于慢启动 */
}
function pickBackend(backends: Backend[], now: number): Backend {
  const alive = backends.filter((b) => b.healthy);
  if (alive.length === 0) throw new Error("no healthy backend");
  if (alive.length === 1) return alive[0];
  /* 随机抽取两个候选 */
  const i = Math.floor(Math.random() * alive.length);
  let j = Math.floor(Math.random() * (alive.length - 1));
  if (j >= i) j += 1;                    /* 保证 j 不等于 i */
  const a = alive[i];
  const b = alive[j];
  /* 负载分数 = 活跃请求数 / (权重 * 慢启动系数) */
  return loadScore(a, now) <= loadScore(b, now) ? a : b;
}
function loadScore(b: Backend, now: number): number {
  /* 慢启动:加入后 60 秒内有效权重线性爬升 */
  const warmup = Math.min((now - b.joinedAt) / 60000, 1);
  const effectiveWeight = b.weight * (0.1 + 0.9 * warmup);
  return b.activeRequests / effectiveWeight;
}
/* 请求生命周期维护计数 */
async function sendRequest(req: Request, backends: Backend[]) {
  const b = pickBackend(backends, Date.now());
  b.activeRequests += 1;
  try {
    return await b.send(req);
  } finally {
    b.activeRequests -= 1;   /* 无论成败都释放计数 */
  }
}
这是 P2C 加慢启动的完整参考实现,每一处细节都对应生产教训。核心 pickBackend 的逻辑:过滤健康节点、随机抽两个不同候选(j >= i 时加一是标准的「无放回抽两个」技巧)、比较负载分数选低者——O(1) 时间、零全局状态、天然抗羊群。loadScore 的设计有三个要点。第一,用活跃请求数而非 CPU 或延迟做负载信号:它是请求发出即加一、完成即减一的本地计数,零采集延迟——CPU 和延迟指标有秒级滞后,P2C 拿滞后信号决策会集体涌向「刚变空闲」的节点造成震荡;活跃请求数是唯一实时的信号。第二,除以权重实现加权 P2C:大机型自然承接更多请求,分数口径统一。第三,慢启动系数让新节点的有效权重从 10% 在 60 秒内线性爬升到 100%——没有它,新节点 activeRequests 为零、分数恒最低,两个候选只要有一个是新节点必被选中,扩容瞬间新节点被流量打爆(缓存冷、连接池空、JIT 未热)。finally 块中释放计数是正确性关键:请求异常(超时、连接失败)时若不释放,计数泄漏会让该节点「看起来越来越忙」最终零流量——这类 bug 的特征是某节点流量单调衰减,重启客户端才恢复。进阶扩展方向:负载信号换成 EWMA 延迟(对「慢节点」更敏感,适合请求耗时方差大的场景,但要处理信号滞后);加入重试时第二次选择排除首次尝试的节点(retry budget 内换节点重试);多优先级场景给候选集分层。把这个实现与 Envoy 的 LEAST_REQUEST 文档对照阅读,你会发现工业实现额外处理了并发计数原子性、panic 模式(健康节点低于阈值时忽略健康状态全量转发)等边角——算法五行,工程五百行,正是负载均衡的真实写照。
/* 示例 5:DNS 全局负载均衡配置(加权轮询 + 地理路由) */
/* 云 DNS / Route53 风格的 GSLB 配置(YAML 表达) */
dns_records:
  - name: www.example.com
    type: A
    routing_policy: latency-based-with-failover
    ttl: 60                      /* 短 TTL:切流生效速度的上限 */
    health_check_id: hc-global-edge
    records:
      - value: 203.0.113.10      /* 东京边缘 Anycast IP */
        region: ap-northeast-1
        weight: 40
        health_check:
          type: HTTPS
          endpoint: https://203.0.113.10/healthz
          interval: 10s
          failure_threshold: 3
      - value: 198.51.100.10     /* 法兰克福边缘 */
        region: eu-central-1
        weight: 35
        health_check: { type: HTTPS, endpoint: "https://198.51.100.10/healthz", interval: 10s, failure_threshold: 3 }
      - value: 192.0.2.10        /* 俄勒冈边缘 */
        region: us-west-2
        weight: 25
        health_check: { type: HTTPS, endpoint: "https://192.0.2.10/healthz", interval: 10s, failure_threshold: 3 }
    failover_order:               /* 区域故障时的溢出顺序 */
      ap-northeast-1: [us-west-2, eu-central-1]
      eu-central-1: [us-west-2, ap-northeast-1]
      us-west-2: [eu-central-1, ap-northeast-1]
/* 容量水位联动:区域负载超 70% 时自动下调 DNS 权重 */
capacity_aware_reweight:
  enabled: true
  metric: region_load_ratio
  high_watermark: 0.70           /* 超过则权重减半,流量溢出到次近区域 */
  low_watermark: 0.60            /* 滞回:降回 60% 以下才恢复权重 */
  min_weight: 5                  /* 保底权重,区域永不完全摘除(防全切震荡) */
这份配置展示了 DNS 层全局负载均衡的完整设计。TTL=60 是第一个关键决策:TTL 决定切流指令的理论生效时间,60 秒是「控制力」与「DNS 查询量」的平衡点——更短的 TTL 让权威 DNS 服务器查询压力倍增,且实际上部分运营商递归 DNS 会无视 TTL 缓存更久,所以 DNS 永远不能作为「秒级切流」的手段,秒级故障转移必须靠下层的 Anycast 加健康检查完成,DNS 负责分钟到小时级的区域调度。latency-based-with-failover 策略组合了两种路由逻辑:正常时按用户到各区域的网络延迟就近解析(东京用户解析到东京 IP),区域健康检查失败时按 failover_order 溢出到预设的次近区域——溢出顺序要事先基于地理和网络拓扑规划并演练,故障现场临时决定顺序是灾难配方。capacity_aware_reweight 是 GLB 的精髓「容量感知调度」:区域负载水位超过 70% 时自动下调其 DNS 权重,新解析的流量被导向其他区域;low_watermark 60% 的滞回设计防止权重在阈值附近反复震荡(升-降-升的抖动会让两个区域都不稳定);min_weight=5 保证过载区域不被完全摘除——完全摘除会让该区域的存量用户重解析时全部涌向其他区域,制造二次洪峰,保留小权重让流量平缓迁移。健康检查从多个地理分布的探测点发起(单点探测会把「探测点到区域的网络问题」误判为「区域故障」),failure_threshold=3 抗瞬时抖动。最后强调演练:这份配置的每个 failover 分支都应该在季度演练中真实触发过——DNS 切流的实际生效时间、边缘缓存的排空速度、目标区域的容量承接表现,只有演练数据能告诉你真相。
/* 示例 6:负载均衡质量监控(PromQL 指标集) */
/* 1. 后端负载分布均匀度:变异系数,理想 < 0.15 */
stddev(rate(http_requests_total{service="api",direction="backend"}[5m]))
/
avg(rate(http_requests_total{service="api",direction="backend"}[5m]))

/* 2. 最热与最冷后端负载比(topk/bottomk) */
topk(1, rate(http_requests_total{service="api",direction="backend"}[5m]))
/
bottomk(1, rate(http_requests_total{service="api",direction="backend"}[5m]))

/* 3. 健康后端数量:低于阈值 page 告警 */
count(envoy_cluster_membership_healthy{cluster="payment_service"}) < 3

/* 4. 异常点驱逐事件流:驱逐频发说明后端质量劣化 */
increase(envoy_cluster_outlier_detection_ejections_enforced_total[15m]) > 10

/* 5. LB 自身引入的延迟(P99,入口时间戳与后端时间戳之差) */
histogram_quantile(0.99,
  sum(rate(lb_internal_latency_seconds_bucket[5m])) by (le))

/* 6. 重试放大率:重试请求占比,超过 10% 告警 */
sum(rate(http_requests_total{service="api",is_retry="true"}[5m]))
/
sum(rate(http_requests_total{service="api"}[5m]))
这组 PromQL 把「负载均衡质量」从模糊感受变成六个可告警的指标,每个都对应一类真实故障模式。指标 1 的变异系数是均衡质量的金标准:stddev/avg 无量纲、可跨服务比较,持续大于 0.3 说明权重失配(新旧机型混部没调 weight)、算法失效(长连接倾斜)或热点键(一致性哈希被击穿)——它的价值在于「渐进退化的早期发现」,负载倾斜通常缓慢恶化,等用户投诉就晚了。指标 2 的 topk/bottomk 比值对「个别节点异常」更敏感:变异系数可能被大量正常节点稀释,而最热最冷比直接暴露极端。指标 3 和 4 监控健康检查系统本身:健康后端数骤降往往是大面积故障的最早信号(比用户侧错误率告警早数十秒),驱逐事件流频发则提示「半死不活」节点在反复进出——这种 flapping 模式典型对应 GC 卡顿或资源泄漏,需要人工介入而非依赖自动恢复。指标 5 是 LB 的自我 SLI:负载均衡器在关键路径上,它引入的附加延迟(正常应小于 1ms)突增说明 LB 自身过载或配置膨胀(路由规则过多)。指标 6 的重试放大率是安全阀:重试是可用性手段也是雪崩燃料,占比超 10% 意味着系统在用重试掩盖后端问题,且距离重试风暴只有一步之遥。实践建议:指标 1、2 做成每个负载均衡集群的标配仪表盘面板,指标 3、4、6 接入告警;每次扩容或算法调整后对比调整前后的变异系数——这是验证「均衡方案有效性」的直接证据,也是设计评审中负载均衡方案的量化验收标准。

实验笔记

本实验的目标是搭建一个可观测的负载均衡环境,亲手对比不同算法的行为差异,调优健康检查参数,并演练故障切换。实验分五个部分,预计 5 到 6 小时,需要 Docker 或虚拟机环境(4 个后端实例加 1 个负载均衡器加 1 个压测客户端)。第一部分:实验环境搭建(约 1 小时)。用 Docker Compose 启动:4 个后端容器(运行简单的 HTTP 服务,支持通过参数模拟延迟——如 /slow?ms=500 返回前睡眠 500ms,/fail 返回 500,用于后续故障注入);1 个 Nginx 或 Envoy 容器做负载均衡;1 个压测客户端(wrk 或 k6)。为每个后端配置差异化的处理能力(如 2 个正常、1 个所有请求延迟 200ms、1 个 CPU 限额减半),模拟真实的异构环境。给后端服务加上 /healthz 端点和请求计数指标(暴露为 Prometheus 格式)。检查清单:压测能跑通、每个后端的 QPS 可独立观测、故障注入参数生效。第二部分:算法对比实验(约 1.5 小时)。这是实验核心。依次配置四种算法并在相同负载下对比:配置 A 轮询——压测 30% 混合流量(70% 快请求加 30% 的 200ms 慢请求),记录各后端的 QPS 分布和 P99 延迟;配置 B 加权轮询——按后端能力设权重(正常 5、慢速 3、限 CPU 2),重复压测对比;配置 C 最少连接/P2C——同样负载重复压测;配置 D 一致性哈希——按 URL 路径哈希,验证相同路径总是命中同一后端(用后端日志确认),然后杀掉一个节点,统计流量重新分布的比例(理论值约 25%)。为每种算法产出对比表:各后端负载的变异系数、全局 P99、错误率。预期观察:轮询下慢后端积压(活跃连接数远高于其他节点)、P99 最差;最少连接自动把流量从慢后端引开、P99 改善;一致性哈希的负载与键分布强相关。用自己的数据验证「请求成本不均匀时轮询失效」这个论断。第三部分:健康检查调优(约 1 小时)。用故障注入测试健康检查的灵敏度和稳定性:场景 A「硬故障」——直接停掉一个后端进程,测量从停止到流量完全切走的时间(主动检查间隔 × 失败阈值 + 摘除生效时间),对比不同参数组合(interval 5s/threshold 2 vs interval 30s/threshold 5)的检测延迟;场景 B「软故障」——让一个后端的 /healthz 保持 200 但业务请求延迟升到 2s(模拟 GC 卡顿),验证主动检查完全测不出来、只有被动检测(outlier detection 或 max_fails)能发现——亲身体会「半死不活节点」的检测难题;场景 C「抖动」——让后端每 15 秒随机失败一次,验证摘除参数是否过敏(理想状态:不触发摘除或短暂摘除后快速恢复);场景 D「全体故障」——让所有后端返回 500,验证 max_ejection_percent 的兜底行为(应保留部分节点而非全体摘除)。记录每个场景的检测时间、误判情况和流量损失。第四部分:长连接与扩容实验(约 1 小时)。配置负载均衡器使用长连接(Nginx keepalive 或 Envoy 连接池),压测建立稳定流量后:实验 A「扩容倾斜」——新增 2 个后端实例,观察新节点流量爬升曲线;对比开启与关闭连接重建机制(keepalive_requests 或 max_connection_age)的差异,验证「存量连接锁死流量分布」现象;实验 B「慢启动」——如果环境支持(Envoy slow_start 或手动实现权重爬升),对比有无慢启动时新节点的延迟尖峰;实验 C「排空下线」——摘除一个节点时对比「立即断开」与「优雅排空」(停止新请求、等待存量完成)的错误率差异。产出:扩容操作的标准流程文档(何时观察、何时确认、异常时如何回退)。第五部分:故障切换演练与复盘(约 45 分钟)。综合演练:在持续压测中同时注入「一个后端软故障 + 一个后端硬故障」(4 节点只剩 2 个健康),验证系统行为:流量是否正确集中到健康节点、剩余节点是否过载(容量按 N+1 规划的话应能承接)、客户端错误率峰值和恢复时间。然后撰写实验报告:四种算法的量化对比表、健康检查参数的推荐值及依据、扩容流程、以及回答思考题——如果你的后端全部同构且请求成本均匀,还需要 P2C 吗?(预期答案:轮询即可,P2C 的价值在异构和成本不均场景;但多 LB 实例环境下 P2C 的抗羊群特性仍有价值。)决策树练习:生产环境扩容后新节点 CPU 瞬间打满、老节点负载反而下降,你的排查路径?(预期路径:确认是否慢启动缺失导致 P2C 集中砸新节点 → 检查新节点缓存命中率(冷缓存放大成本)→ 检查连接重建是否让流量瞬间迁移 → 临时下调新节点权重,逐步爬升。)

延伸阅读

  • The Site Reliability Workbook 第 9 章 "Load Balancing at the Frontend and Backend"——本章原始出处,以 Google 的前端(GFE/DNS/Maglev)和后端(Borg 内的任务分配)负载均衡为案例,讨论了均衡算法选择、连接管理、以及 Google 内部的负载均衡架构分层。原文对「前端均衡与后端均衡目标差异」的分析是理解全章的框架。
  • Maglev: A Fast and Reliable Software Network Load Balancer(Eisenbud 等人的 NSDI 2016 论文)——Google L4 负载均衡器 Maglev 的设计论文,详细描述了一致性哈希查找表、ECMP 集群、连接跟踪的实现,以及「有界负载一致性哈希」的算法细节。这是理解大规模 L4 负载均衡的必读材料,工程细节密度极高。
  • The Power of Two Choices in Load Balancing(Michael Mitzenmacher 的论文及其综述 "A Survey of Load Balancing Algorithms")——P2C 算法的数学基础:为什么两次选择能把最大负载偏差从 O(log n) 降到 O(log log n)。Mitzenmacher 的综述覆盖了几乎所有负载均衡算法的理论分析,适合想深入算法性质的读者。
  • Envoy 官方文档:Load Balancing Policies 与 Outlier Detection——Envoy 是云原生负载均衡的事实标准,其文档对 ROUND_ROBIN、LEAST_REQUEST、RING_HASH、MAGLEV 四种策略的适用场景、慢启动配置、异常点检测参数有工业级细节。配合本章示例 2、3 阅读,可直接落地。
  • gRPC 官方文档:Load Balancing(grpc.io 的 grpc-load-balancing 设计文档)——解释为什么 gRPC 的 HTTP/2 长连接模型让 L4 负载均衡失效、客户端负载均衡如何工作、以及 pick_first 与 round_robin 策略的实现。使用 gRPC 微服务的团队必读,能避开「gRPC 加 L4 LB 负载不均」这个最著名的坑。
  • SRE 黑皮书第 22 章 "Addressing Cascading Failures" 与第 21 章 "Handling Overload"——负载均衡的失败模式(重试风暴、惊群、服务过载)在这两章有系统讨论。特别是「请求级重试的放大效应」和「慢启动与优雅降级的配合」,与本章的连接管理和健康检查内容互为表里。
  • AWS 与 Google Cloud 的负载均衡最佳实践白皮书——AWS ELB 的《Best Practices for Elastic Load Balancing》和 GCP 的《Load Balancing architecture guide》分别描述了云上 LB 的容量规划、健康检查配置、跨可用区均衡的官方建议。云上用户对照自己的 LB 配置逐项检查,通常能发现多个改进点。
  • Cloudflare 博客的 Anycast 与 GLB 系列文章——Cloudflare 用第一视角讲述了 Anycast 路由、BGP 收敛、DNS 调度在全球负载均衡中的实战细节,包括多次真实故障的复盘(如 BGP 泄漏导致流量黑洞)。比教科书更生动,是理解「DNS 加 Anycast 加 L4/L7」分层调度如何在全球尺度运作的最佳材料。
9/12负载均衡
0%