想象你走进一座生意火爆的美食城:门口的兰州拉面、黄焖鸡和麻辣香锅三个档口一字排开。最开始只有拉面一个档口,中午客人一拥而入,师傅手都抖了,队伍还是越排越长——这就是典型的“单机瓶颈”。后来老板学聪明了:多开几个档口,让客流分着走。可是怎么分?老板想到了两种办法:第一种,给每个顾客手机里装一个“档口小程序”,大家自己看排队情况、自己决定去哪家——这就是客户端负载均衡;第二种,在门口高薪聘请一位“领位员”,所有顾客先来报到,由领位员统一查看全局、安排座位——这就是服务端代理负载均衡。在互联网世界里,前者叫 Ribbon,住在每个调用方的程序里;后者叫 Envoy,以 Sidecar 身份蹲在服务旁边。这篇文章就借这座美食城的故事,把负载均衡的来龙去脉、健康检查、权重路由、重试容错,以及两大主角的选型对比,一锅端给你。
学透负载均衡,你就能在流量放大时从容扩容、在服务抖动时快速定位故障,这是架构师处理高并发与高可用问题的核心内功之一。
可视化演示
展示Ribbon进程内负载均衡与Envoy Sidecar代理的架构差异、四种均衡策略的请求分发模式及健康检查与重试机制,揭示客户端与服务端负载均衡的核心区别。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Ribbon vs Envoy架构对比 Ribbon:客户端进程内LB,直接选择服务实例发请求 Envoy:Sidecar代理模式,请求经由Envoy转发到目标 核心差异:进程内vs进程外,侵入性vs透明代理 Ribbon: Client→LB(in-process)→Server | Envoy: Client→Envoy→Envoy→Server 架构模式决定运维边界与扩展性
负载均衡基础
想象一座刚开业的美食城只有一家兰州拉面档口。中午客人涌进来,档口只有三个灶头,师傅手再快也赶不上点单的速度——队伍越排越长,客人越等越气,最后骂骂咧咧地走了。老板很快明白一个道理:再能干的师傅,单口灶台的产能也有天花板。于是他把档口从一家开到三家,可新的问题来了:客人该去哪家?
把“档口”换成“服务器”,把“排队点单”换成“网络请求”,这就是负载均衡要解决的核心问题:单机存在瓶颈,与其让一台机器硬扛,不如让一个集群共同分摊流量,让每台机器都轻轻松松。按分工层次,负载均衡分为四层(传输层)和七层(应用层):四层看源目 IP 和端口,转发快,典型如 LVS;七层看 HTTP 的 URL 和 Header,能精细分流,典型如 Nginx。按决策位置,又分成客户端负载均衡(Ribbon)和服务端代理负载均衡(Envoy/Nginx),这也是本文的两大主角。
集群带来的不只是吞吐翻倍,还有可用性的数学提升——只要不是所有机器同时宕机,服务就能继续:
回到美食城:老板开了三家分店,顾客知道只要还有一家开门,就不至于白跑一趟。负载均衡就是“让所有档口都在工作,且任何一家倒下都不至于塌方”的组织方法。接下来我们看两种主流组织方式:让顾客自己挑(Ribbon),还是请领位员统一安排(Envoy)。
- 单机可用率 $A_0$一台服务器在一年中能正常服务的时间占比,比如 99%,对应每年约 3.65 天的故障时间。
- 单机不可用率 $1 - A_0$这台服务器“某时刻恰好处于宕机状态”的概率,也就是顾客来了却发现档口关门的概率。
- 整组同时宕机 $(1 - A_0)^n$假设各机器独立运行,n 台同时不可用的概率是单机不可用率的 n 次方——就像三个档口同时关门一样稀有。
- 取补集得到集群可用性整体可用 = 1 减去“全部宕机”的概率,即至少有一台存活,系统就还能响应。
Ribbon原理
美食城老板给所有顾客配了一个小程序:打开就能看到各家档口当前的排队人数和出餐速度。你端详一番,决定今天去人最少的黄焖鸡。每个顾客的选择自己拍板,不经过任何人协调——Ribbon 就是这样一个“小程序”。
Ribbon 是 Spring Cloud 体系中经典的客户端负载均衡器,它以 jar 包的形式“住”在调用方进程里,为每一次远程调用挑选目标实例。它不是靠猜,而是定时从注册中心(Eureka、Nacos 等)拉取服务实例列表,缓存到本地内存。当你的订单服务要调用用户服务时,Ribbon 就从缓存列表里按策略挑一个地址。最经典的策略是轮询(Round Robin),像转盘一样挨个来:
除了轮询,Ribbon 还支持随机、响应时间加权等策略,并可以自定义。它的最大特点是“去中心化”:每个调用方自己决策,链路最短、零额外跳数。但就像每个顾客手机里的信息可能不同步,各客户端的缓存列表也可能不一致,决策分散意味着没有全局视角——这是它的优点,也是隐患。
- 当前选中下标 $S_{current}$记录上一条请求被派给了列表里的第几个实例。
- 下标加一 $S_{current} + 1$把指针拨向下一个实例,这是轮询“挨个来”的灵魂。
- 对实例总数取模 $\bmod n$当指针走到列表末尾(等于 n)时,取模让它回到 0,形成环形队列。
- 实例总数 $n$当前健康的服务实例个数,这个数字来自注册中心拉取的最新列表。
Envoy代理
顾客自己挑档口总出幺蛾子:有人信息滞后,有人爱扎堆。于是老板高薪聘请了一位领位员站在门口,顾客进门先报到,领位员看一眼全局,统一安排座位——这就是 Envoy 的思路。
Envoy 是云原生时代最流行的服务端代理,它不住在客户端进程里,而是以 Sidecar 模式蹲在业务容器旁边(同一个 Pod 里的另一个容器),替业务进程收发流量。业务代码完全不用改,Java、Go、Python 一视同仁。Envoy 把架构清晰地切成两层:控制面(如 Istio 的 pilot)负责下发配置,数据面(Envoy 自己)负责转发流量。这套配置下发基于 xDS 协议:CDS 发现集群列表、EDS 发现每个集群的端点、RDS 发现路由规则。
配好之后,请求从客户端进来,先被 iptables 透明劫持到 Sidecar,再按路由规则分发给目标服务。Envoy 按权重把流量分给各个端点,分流概率的公式如下:
领位员的代价是“所有顾客都从门口过”:多一跳就多一份延迟,门口要是堵了,整座美食城都歇菜。所以选用 Envoy 必须配套健康检查、超时和熔断,把这位管家保护得明明白白。
- 第 i 个节点的权重 $w_i$相当于档口的“受欢迎程度”,可以静态配置,也可以动态算出来。
- 所有权重之和 $\sum_{j=1}^{n} w_j$把 n 个档口的受欢迎程度加总,作为比例尺的“分母”。
- 分流概率 $P_i$第 i 个节点分到的流量占比,权重越大、概率越高。
- 归一化特性所有节点概率加起来正好等于 1,保证每一份流量都有去处。
健康检查机制
某个档口水管爆了、大厨请假,但老板没来得及关灯。如果领位员还一个劲地把客人往那儿领,客人们只能对着黑灯瞎火的档口干瞪眼——更糟的是,客人还以为这家店在营业,白白浪费了时间。
健康检查就是让系统自动识别“黑灯档口”。它有两套手法:主动探测,定时向每个档口发起 HTTP 探活请求或 TCP 连接,看它是否回应;被动检查,观察线上真实请求的失败率,失败次数多了就把它拉进黑名单。档口偶尔也会抖动——网络闪断、GC 停顿都可能导致一两秒无响应,不能一抖就摘。所以主流设计都采用“连续失败阈值”:连续失败 k 次才判定宕机。这个设计的数学依据是:
Ribbon 和 Envoy 都做健康检查,但姿势不同:Ribbon 靠客户端在自己的调用结果里被动统计失败,再配合注册中心的心跳剔除;Envoy 由控制面下发主动探测配置,探测失败后按阈值摘除,并把结果广播给整个网格。就像小程序只能凭顾客反馈猜测“暂停营业”,而领位员可以亲自去敲档口的门——各有一套,但目标一致。
- 单次探测失败概率 $p$一次探活请求超时或返回错误码的概率,包含网络抖动、GC 停顿等瞬态因素。
- 连续失败次数 $k$判定宕机前允许连续失败的次数,k 越大越保守、k 越小越激进。
- 连续 k 次失败概率 $p^k$若每次失败互相独立,连续 k 次失败的概率呈指数级下降,因此“连续失败”能过滤大部分瞬时抖动。
- 摘除与恢复一旦触发阈值,节点被摘除;恢复后还要“慢启动”,先放少量流量试探,确认稳定再逐步加压。
权重路由策略
三个档口口味不一样:麻辣香锅天天排队,新开的兰州拉面没什么人。如果平均分客,香锅门口挤爆、拉面门口冷清,客人体验都很差。老板一拍大腿:按比例分人!香锅 50%、拉面 30%、黄焖鸡 20%。
权重路由就是“按比例分人”。权重可以在配置文件里静态指定,比如灰度发布时把新版本权重设为 5%,让一小撮用户先尝鲜;也可以动态计算,让性能好的节点多接客。最常见的动态权重以平均响应时间为依据:响应越快的节点权重越高,公式如下:
权重大的节点也不能被连续轰炸,否则它再快也会被压垮。生产上常用平滑加权轮询(Smooth WRR):每轮先给所有节点累加权重,选中当前累计权重最大的节点,再让它减去总权重。就像拉面店叫号,排队多的号也不能连续叫两次。权重路由在微服务里最重要的应用是灰度发布:5% 流量先访问新版本,观察 P99 和错误率没问题,再逐步把权重调到 100%。
- 本节点平均响应时间 $R_i$滑动窗口内第 i 个实例处理请求的平均耗时,是动态权重的“原材料”。
- 全局最短响应时间 $R_{min}$所有节点里响应最快的那个,作为基准线。
- 响应时间倒数为权重 $W_i$响应越短、$W_i$ 越大,体现“能者多劳”。
- 归一化后使用把算出的权重代入分流概率公式,按比例分发流量。
重试与容错
顾客被领位员派到黄焖鸡,结果里面坐满了,他退回来重新排队。退一次两次还好;如果几十个人同时在门口反复横跳、换来换去,走廊就会被堵得水泄不通——比某个档口排队更可怕。
重试是容错最常用的手段,但必须有限度。设计重试策略时有三要素:最大重试次数、退避时间(指数退避加随机抖动)、全局超时上限。即使单次失败概率很高,只要重试次数有限,整体仍然有机会成功;但重试再多也不会让失败率归零,重试 m 次后仍然失败的概率是:
重试还有一颗隐藏地雷——幂等性。GET 查询重试十遍都没问题,但扣款、下单这类写操作,重试一次就可能产生两笔订单。所以只有幂等接口才允许自动重试,否则要为请求引入全局幂等键。再往上就是熔断降级:断路器有关闭、打开、半开三个状态,正常时放行,失败率超标就快速失败,半开时放少量流量试探恢复。Ribbon 的重试依赖应用代码(如 Spring Retry),没有全局视图,容易层层叠加成重试风暴;Envoy 可以在代理层统一配置 retry_policy,把次数、退避、超时集中管起来。记住:重试是止痛药,不是营养品——开药前先确认不会中毒(幂等)。
- 单次失败概率 $p$一次调用因超时、连接拒绝等原因失败的概率。
- 总尝试次数 $m + 1$初始 1 次加上最多重试 m 次,比如重试 2 次意味着最多试 3 次。
- 全部失败的概率 $p^{m+1}$每次尝试都失败,才最终放弃;这是指数退避之外的第二个“指数”。
- 实践的取舍m 越大成功率越高,但耗时和上游压力也越大,必须设上限来防止重试风暴。
选型对比实践
开连锁分店时,老板面临抉择:该给每个顾客配“档口小程序”(Ribbon 模式),还是每家店门口都站一个领位员(Envoy 模式)?这个问题的答案,藏在流量模型和团队能力里。
先说性能:Ribbon 是进程内直连,请求从客户端直接到服务端,零额外跳数,延迟最低;Envoy 多一跳代理,每次请求都要经过 Sidecar 转发,延迟必然多出几十微秒到几毫秒。再说治理:Ribbon 的决策分散在各个客户端,没有全局视图,出问题要靠日志拼图;Envoy 由控制面统一管理,流量、健康、路由全部集中可观测,配合 Dashboard 一目了然。接入成本上,Ribbon 只是 Spring Cloud 的一个依赖;Envoy 需要注入 Sidecar、部署控制面、处理 iptables 劫持,运维复杂度明显更高。
选型口诀:Spring Cloud 体系内、延迟敏感的同步调用,优先 Ribbon;多语言混合架构、南北向入口、需要统一灰度与可观测性的云原生平台,选 Envoy。一个容易忽略的细节是:代理本身也会挂,端到端可用性必须串联计算:
最后分享一次生产复盘:某电商大促期间,订单服务的多个下游同时变慢,客户端层层重试叠加,把订单服务打成了雪崩。事后整改三板斧——限制重试次数、全局超时熔断、非幂等请求一律不重试,并对代理层做了双活部署。美食城的生意要做大,健康检查、权重路由、有限重试这三件套一个都不能少;无论你信“小助手”还是“领位员”,这套基本功都是活下去的底线。
---
- 代理可用性 $A_{proxy}$领位员自己不出错的概率,包括它所在节点的存活率和转发成功率。
- 服务可用性 $A_{service}$后端业务实例整体可用的概率,健康检查、权重路由都是为了抬升它。
- 串联相乘 $A_{proxy} \times A_{service}$请求必须依次经过代理和服务,任何一环失败都算失败,所以概率相乘。
- 结论每增加一个必经过的环节,可用性都会打折,这提醒我们在引入代理时要同步做高可用冗余。
AI 应用场景
电商大促的弹性扩容与流量分发
阶段:流量预测驱动的容量规划与执行(AIOps 中“预测—决策—执行”环路的执行端)
解决的问题:大促前手工修改 Nginx 上游列表既慢又易错,扩容实例难以被流量感知。借助注册中心 + Ribbon,新扩容的实例注册后立即被客户端发现,请求自动摊到新节点上,无需人工干预。
具体案例:618 大促,订单服务由 20 个实例快速扩展到 80 个。Ribbon 轮询策略把峰值流量均匀分给所有健康实例,配合注册中心告警,扩容后 30 秒内流量分布完全均衡。
推荐系统新版本的灰度发布
阶段:在线学习模型迭代上线(模型版本管理的 A/B 实验环节)
解决的问题:推荐模型 v2 上线,直接全量切换风险极大。用 Envoy 路由权重把 5% 流量切到新版本,实时对比 P99 延迟与 CTR 指标,确认稳定后再平滑提升权重到 100%,实现低风险发布。
具体案例:某内容平台推荐服务用 Envoy 配置了 v1/v2 两个集群,v2 权重从 5% 起步,每 30 分钟观察一次业务指标,4 小时后全量切换,全程零故障。
混沌工程故障演练与自愈恢复
阶段:异常检测告警触发后的自动容灾恢复
解决的问题:传统故障靠人肉发现,反应慢。通过健康检查 + 熔断的组合,系统能在故障发生数十秒内自动摘除异常节点、切换流量,业务无感。
具体案例:某支付团队在演练中人为 kill 掉一个核心服务实例,Envoy 主动健康检查连续 3 次失败后立即摘除节点;同时 Ribbon 客户端被动统计到失败率超标触发熔断,请求自动转向健康实例,演练期间业务成功率保持 99.99%。
Python 演示
# 负载均衡模拟器: 健康检查 + 平滑加权轮询 + 有限重试# 模拟美食城三个档口的客流分配过程import randomimport time class Stall: """档口: 一个后端服务实例""" def __init__(self, name, weight): self.name = name self.weight = weight self.alive = True self.current = 0 class FoodCourt: """美食城: 统一负责健康检查与客流分发""" def __init__(self, stalls): self.stalls = stalls self.total = sum(s.weight for s in stalls) def health_check(self, down_rate=0.15): """模拟档口随机挂出"暂停营业"的牌子""" for s in self.stalls: if random.random() < down_rate: s.alive = False def pick(self): """平滑加权轮询: 每轮先累加权重, 再选出最大者发客""" best = None for s in self.stalls: if not s.alive: continue s.current += s.weight if best is None or s.current > best.current: best = s if best is None: return None best.current -= self.total return best def serve(self, max_retry=2): """带有限重试的领位流程, 防止重试风暴""" for n in range(max_retry + 1): stall = self.pick() if stall is None: print(" -- 所有档口都暂停营业, 客人离开") return False if random.random() < 0.2: print(" -- %s排队太久, 顾客回来重试(%d)" % (stall.name, n + 1)) continue print(" %s接待了这位顾客" % stall.name) return True print(" -- 重试次数用尽, 降级到备用方案") return False def main(): random.seed(42) court = FoodCourt([ Stall("兰州拉面", 3), Stall("黄焖鸡米饭", 2), Stall("麻辣香锅", 1), ]) for i in range(10): print("第%d批顾客进场" % (i + 1)) court.health_check() court.serve() time.sleep(0.2) if __name__ == "__main__": main()
s.current += s.weight每轮先给健康档口的“账本”累加权重,相当于记录它的实时票数。best.current -= self.total被选中的档口要减去总权重,防止权重大的档口被连续轰炸,实现“平滑”。s.alive = False健康检查把宕机档口标记为不可用,后续轮询直接跳过它。if stall is None:所有档口都不可用时返回 None,调用方必须准备降级方案,而不是死等。for n in range(max_retry + 1)最多重试 max_retry 次,配合后面的“重试次数用尽”提示,避免无限重试引发重试风暴。random.seed(42)固定随机种子,让演示结果可复现,方便你对比不同参数的效果。
常见误区
误区负载均衡不就是 Nginx 或云 LB 吗?在系统入口放一台负载均衡器就完事了,Ribbon 这种东西没必要学。
正解负载均衡按决策位置分为客户端型和服务端型。Nginx 是服务端集中式,站在大门口统一接客;Ribbon 是客户端进程内式,住在每个调用方手机里。前者管南北向入口流量,后者管微服务之间的东西向调用,两者解决的是不同场景的问题,不能互相替代。
为何常见:大多数入门教程和面试题都从 Nginx 讲起,而南北向流量最直观,容易让人以为负载均衡只有“入口网关”一种形态。
误区只要把服务实例从 3 个加到 30 个,负载均衡会自动让系统变得坚不可摧。
正解负载均衡只负责“分活”,不负责“修路”。如果健康检查失效,请求会源源不断发给已宕机的节点,错误率反而上升;各客户端缓存的服务列表不同步时,流量还会被“楔”到个别节点上,形成雪崩。增节点必须配合注册中心、健康检查和优雅上下线,否则只是把风险摊到更多机器上。
为何常见:集群扩容的收益直观可见,但“分活不均”和“坏节点未摘除”这两个隐患不到故障时刻不会暴露。
误区调用失败就自动重试,多试几次总能成功,反正不会更糟。
正解重试不是免费的保险。上游已经超时拥堵时,重试只会雪上加霜,形成重试风暴;对扣款、下单等非幂等接口重试,还会产生重复订单等严重副作用。正确做法是限制最大重试次数、使用指数退避加抖动、只对幂等请求重试,并设置全局超时预算。
为何常见:本地调试时重试确实经常有效,让人忽略了分布式环境下的排队效应和副作用累积。
误区Envoy 是云原生标配,功能全面、治理集中,Ribbon 是过时技术,应该全面换成 Envoy。
正解Envoy 多一跳代理,延迟和运维复杂度都在 Ribbon 之上;Ribbon 进程内直连、零额外跳数,在 Spring Cloud 体系内依然高效。选型取决于流量模型:内部高频低延迟调用适合进程内,跨语言、入口流量、需要统一观测才适合代理。技术没有新旧,只有合不合适。
为何常见:技术潮流让从业者容易“喜新厌旧”,而忽略了延迟预算、运维成本和团队掌握度这些真问题。
面试题
Q1. Ribbon 在微服务调用中扮演的角色是?
- A.服务端集中式负载均衡器
- B.客户端进程内负载均衡器← 正确答案
- C.网关层的流量分发组件
- D.域名解析层的分发器
答案:B。Ribbon 以 jar 包形式运行在调用方进程内,从注册中心拉取列表并自行决策,属于客户端负载均衡。A 是 Nginx/Envoy 一类的角色,C 是网关,D 是 DNS 负载均衡,都不符合。
Q2. Envoy 最常见的部署模式是?
- A.与应用同机部署的 Sidecar 代理← 正确答案
- B.独立于应用的集中式代理
- C.嵌入业务代码的 SDK 类库
- D.部署在 DNS 层的域名代理
答案:A。Envoy 以 Sidecar 模式挂在业务容器旁边,与应用同 Pod 部署,替业务收发流量。B 是 Nginx 式的集中代理,C 是 Ribbon 式的 SDK 形态,D 是 DNS 层的设施,都不对。
Q3. 轮询策略选择下一个实例的方法是?
- A.当前下标加一后对总数取模← 正确答案
- B.生成随机数对实例数取模
- C.选择响应时间最短的实例
- D.选择权重最大的那个实例
答案:A。轮询的公式是 next = (current + 1) mod n,像转盘一样挨个分发。B 是随机策略,C 是响应时间加权,D 是权重优先,均与轮询无关。
Q4. 健康检查设置“连续失败 3 次才摘除”的主要目的是?
- A.让故障节点尽快被摘除
- B.过滤掉瞬时的网络抖动← 正确答案
- C.减少健康检查的探测流量
- D.让客户端缓存尽快过期
答案:B。网络闪断、GC 停顿等瞬态抖动很常见,若单次失败立即摘除,节点会频繁上下线。连续失败阈值的数学依据是 p 的 k 次方,指数级过滤抖动。A 是单次失败就摘除的效果,C、D 与阈值设定无直接关系。
Q5. 动态权重通常依据哪个指标计算?
- A.实例 CPU 使用率的高低
- B.平均响应时间的倒数← 正确答案
- C.实例内存占用的大小
- D.服务最近一次的启动时间
答案:B。主流动态权重用响应时间倒数为权重,公式 W_i = R_min / R_i,响应越快权重越大。CPU 和内存是资源类指标,通常做限流而不是路由权重;启动时间与权重无关。
Q6. 下列哪类请求最适合自动重试?
- A.用户账户的扣费操作
- B.幂等查询类的读取请求← 正确答案
- C.订单创建类的写入请求
- D.短信验证码的发送请求
答案:B。幂等请求重试任意次结果不变,安全可靠。扣费、下单、发短信都是非幂等写操作,重试可能重复扣款、重复下单,必须引入幂等键或干脆禁止自动重试。
Q7. 四层负载均衡主要依据什么信息转发?
- A.HTTP 请求的 URL 路径
- B.源目 IP 与端口四元组← 正确答案
- C.应用层的会话 Session
- D.消息队列中的消息主题
答案:B。四层工作在传输层,只看 IP 和端口,速度快;七层工作在应用层,才解析 URL、Header、Session 等业务信息。D 与负载均衡层次无关。
Q8. 负载均衡的常见分发策略包括哪些?
- A.轮询与加权轮询策略← 正确答案
- B.加权随机采样策略← 正确答案
- C.一致性哈希分担策略← 正确答案
- D.全量广播复制策略
答案:ABC。轮询、随机、哈希都是经典的负载均衡策略,各有适用场景:轮询均匀、加权体现能力差异、一致性哈希保证会话亲和。全量广播是消息队列的扇出模型,不是负载均衡策略。
Q9. Envoy 相比 Ribbon 的优势有哪些?
- A.支持多语言无侵入接入← 正确答案
- B.流量治理集中便于观测← 正确答案
- C.每一跳的延迟必然更低
- D.路由配置支持动态热更新← 正确答案
答案:ABD。Envoy 以 Sidecar 形式接入,业务代码零侵入,多语言通吃;控制面集中治理,流量观测性强;xDS 协议支持配置动态下发。但多一跳代理必然增加延迟,C 恰恰是 Ribbon 的优势,故不选。
Q10. 关于平滑加权轮询算法,下列说法正确的有?
- A.每轮先给各节点累加自身权重← 正确答案
- B.被选中的节点要减去本轮总权重← 正确答案
- C.大权重节点永远不会被连续选中
- D.小权重节点永远不会被选中
答案:AB。平滑加权轮询每轮先令每个节点 current += weight,再选 current 最大的节点,选中后令该节点 current -= total,因此 A、B 正确。C 错误:大权重节点仍可能连续两次被选中(如权重 5:1:1 时前两轮都是 A),只是不会长期垄断;D 错误:小权重节点每隔若干轮也会被选中,保证长期比例公平。