ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

面试官:你能说说 Ribbon 的负载均衡策略及原理吗?

面试官:你能说说 Ribbon 的负载均衡策略及原理吗? 开篇Ribbon 到底在面试中考察什么很多候选人在聊微服务的时候第一反应是 Spring Cloud 全家桶也会把 Ribbon 挂在嘴边但当面试官追问「那你说说 Ribbon 的负载均衡策略有哪些」「RoundRobinRule 在源码里是怎么实现的」「为什么默认策略是 ZoneAvoidanceRule」时往往只能回答出「有轮询、随机」这几句再往深挖就卡壳了。Ribbon 这道题在大厂面试里属于典型的「看似基础、实则分层」的题目表面上考察的是你背没背过几种负载均衡策略实际上是通过这些策略背后的实现逻辑考察你对客户端负载均衡、服务发现、定时更新、重试机制以及 Spring Cloud 整合原理的掌握程度。这篇文章会从零开始先讲清楚负载均衡的本质再拆解 Ribbon 的架构模型然后逐一深入讲解它内置的七大负载均衡策略包括各自的算法思想、适用场景和核心源码片段最后延伸到重试机制、饥饿加载、Feign 整合、自定义策略实战以及面试中高频追问的解题思路。目标是把「Ribbon 负载均衡」这一条线从头到尾讲透。1. 为什么需要负载均衡在单体应用时代一个 Java 进程就能承载全部业务逻辑用户请求进来后由 Web 容器直接处理不存在「把请求分给谁」的问题。随着业务量增长单机性能出现瓶颈最直接的办法就是水平扩展把同一个服务部署成多个实例例如把订单服务部署到 192.168.1.10、192.168.1.11、192.168.1.12 三台机器上。一旦服务有多个实例客户端调用的时候就必须解决一个核心问题这次请求应该发到哪一台机器这个「选择一台机器来承接请求」的过程就是负载均衡。它要解决的不是简单的随机挑一台而是要在多台后端实例之间合理地分配流量尽量做到每台机器负载均衡同时兼顾实例的可用性和网络开销。2. 服务端负载均衡与客户端负载均衡从负载均衡器所处的位置来看负载均衡通常分为两类服务端负载均衡和客户端负载均衡。理解这两者的区别是理解 Ribbon 定位的前提。2.1 服务端负载均衡服务端负载均衡的典型代表是 Nginx、F5、HAProxy。它的特点是在服务调用方和服务提供方之间增设一个独立的代理层所有请求先打到负载均衡器上由负载均衡器根据配置的算法把流量转发给后端的某个实例。text客户端 ── Nginx负载均衡器 ── 服务实例 A ├── 服务实例 B └── 服务实例 C这种方式的优点是集中管理负载均衡逻辑对客户端透明客户端不需要关心后端有多少实例缺点也很明显负载均衡器本身会成为系统的单点和性能瓶颈同时会在请求链路上额外增加一跳网络开销。此外服务端负载均衡通常工作在 TCP 或 HTTP 层很难感知到服务语义层面的调用细节。2.2 客户端负载均衡客户端负载均衡的代表就是 Ribbon 和 Spring Cloud LoadBalancer。它的核心思想是把「选择哪个实例」的逻辑下沉到服务调用方自己手里服务调用方会维护一份服务地址列表在发起请求之前由本地的负载均衡器根据算法从地址列表中选择一个目标实例然后直接向该实例发起调用中间没有独立代理层。text服务调用方内置 Ribbon │ │ 本地维护 [实例 A, 实例 B, 实例 C] │ 根据策略选出一个实例 │ ├── 服务实例 A ├── 服务实例 B └── 服务实例 C客户端负载均衡的优势在于调用方直接连接服务实例减少了一层网络转发延迟更低负载均衡逻辑和应用绑定在一起可以感知到更多业务上下文同时没有集中式单点问题。它的代价是每个服务调用方都需要内嵌一套负载均衡组件并且需要自己维护服务实例列表的获取和刷新。2.3 Ribbon 在其中的位置Ribbon 是 Netflix 开源的客户端负载均衡器它运行在服务消费者的进程内部。在 Spring Cloud 微服务体系中Ribbon 通常和 Eureka、OpenFeign 配合使用Eureka 负责服务注册与发现Ribbon 从 Eureka 获取服务实例列表再在自己的进程内部完成负载均衡选择。这就是典型的「注册中心 客户端负载均衡」的配合模式。3. Ribbon 是什么以及它的演进3.1 Ribbon 是什么Ribbon 是 Netflix 推出的进程内负载均衡组件提供了一系列完善的客户端负载均衡能力包括轮询、随机、响应时间加权、重试、区域感知等策略同时支持可插拔的负载均衡规则和可自定义的服务实例来源。在 Spring Cloud 体系中Ribbon 曾经长期作为默认的负载均衡解决方案。Spring Cloud Alibaba 中的 Ribbon 整合、Feign 的负载均衡调用底层都离不开 Ribbon 的支撑。需要明确的是Ribbon 一方面指 Netflix 开源的原始组件另一方面也指 Spring Cloud 中整合后的 spring-cloud-starter-netflix-ribbon 模块两者在接口上基本一致但实现细节上因为维护策略不同而有差异。3.2 Ribbon 的演进与替代方案Ribbon 在 2018 年前后进入维护模式Netflix 不再继续大规模投入新功能的开发。Spring Cloud 官方在后续版本中推出了 Spring Cloud LoadBalancer 作为新的标准负载均衡组件并逐步替代 Ribbon 的默认地位。不过由于历史存量项目众多Ribbon 仍然是面试和日常维护中绕不开的话题。很多原理和概念例如客户端负载均衡、服务列表维护、IRule 规则接口在后来的 Spring Cloud LoadBalancer 中也得到了延续。理解 Ribbon 的价值在于它是客户端负载均衡设计思想的一个典型实现。即使你未来使用 Spring Cloud LoadBalancer 或 Dubbo 的负载均衡很多设计模式也是一脉相承的。4. Ribbon 的核心架构与关键接口在讲策略之前必须先了解 Ribbon 的几个核心接口。很多候选人对策略名称倒背如流却说不清楚这些接口之间的协作关系导致在源码追问环节露怯。4.1 ILoadBalancer负载均衡器入口ILoadBalancer 是 Ribbon 对外暴露的核心接口负责把「服务实例列表」和「负载均衡策略」结合起来最终返回一个可用的服务实例。核心方法包括addServers向负载均衡器中添加服务实例。chooseServer根据负载均衡策略选择一个服务实例。markServerDown标记某个实例为不可用。getReachableServers获取当前所有可达的实例。getAllServers获取所有实例包括不可达实例。javapublic interface ILoadBalancer { void addServers(ListServer newServers); Server chooseServer(Object key); void markServerDown(Server server); ListServer getReachableServers(); ListServer getAllServers(); }BaseLoadBalancer 是 ILoadBalancer 的基础实现内部维护一个服务列表和一个 IRule 规则对象。chooseServer 方法的核心逻辑就是把选择动作委托给 IRule 来完成。4.2 IRule负载均衡策略接口IRule 是「负载均衡策略」的抽象它定义了从一个实例列表中选择一个实例的规则。这是本文的重头戏Ribbon 内置的所有策略本质上都是 IRule 的不同实现。javapublic interface IRule { Server choose(Object key); void setLoadBalancer(ILoadBalancer lb); ILoadBalancer getLoadBalancer(); }IRule 的 choose 方法接收一个 key返回一个 Server。大多数策略在选择时都需要访问 ILoadBalancer 来获取当前的实例列表因此接口里保留了 setLoadBalancer 和 getLoadBalancer 的方法用来把策略与负载均衡器关联起来。4.3 ServerList服务实例来源ServerList 负责提供某个服务的所有实例。它是服务实例的「数据源」。在静态配置模式下ServerList 可以来自配置文件写死的一组地址在动态服务发现模式下比如和 Eureka 结合时ServerList 的实现会从注册中心拉取实例列表。javapublic interface ServerListT extends Server { ListT getInitialListOfServers(); ListT getUpdatedListOfServers(); }getInitialListOfServers 用于首次获取服务列表getUpdatedListOfServers 用于后续定时刷新。DomainExtractingServerList 等实现会把 Eureka 实例包装成 Ribbon 的 Server 对象。4.4 IPing实例存活检测IPing 用来检测一个服务实例是否存活。Ribbon 需要定期剔除已经宕机的实例否则会把流量发到不可用的节点上。常见的实现包括 PingUrl、NIWSDiscoveryPing其中 NIWSDiscoveryPing 通常和 Eureka 配合借助 Eureka 的健康状态判断实例可用性。javapublic interface IPing { boolean isAlive(Server server); }这个机制和策略中的可用性过滤策略关系密切一个实例是否被过滤往往取决于 IPing 的判断结果。4.5 ServerListUpdater服务列表更新器ServerListUpdater 负责定时触发服务列表的更新。它不关心列表从哪来、也不关心如何选择只负责「定时刷新」这件事。常见实现有 PollingServerListUpdater通过一个定时任务周期性地重新拉取服务列表并把变化同步到 ILoadBalancer 中。这四个接口加一个负载均衡器构成了 Ribbon 最核心的组件协作模型ServerList 提供数据ServerListUpdater 定时刷新数据IPing 判断实例存活IRule 根据算法选择实例ILoadBalancer 把这一切串起来供业务调用。5. Ribbon 七大负载均衡策略总览Ribbon 在 com.netflix.loadbalancer 包下内置了多套 IRule 实现其中最常见的七种分别是策略类核心思想典型特点RoundRobinRule轮询依次循环选择简单公平RandomRule随机随机挑选实现简单RetryRule带重试的轮询在限定时间内选择失败会重试WeightedResponseTimeRule响应时间加权响应越快的实例权重越高BestAvailableRule最空闲优先过滤断路器打开和并发高的实例AvailabilityFilteringRule可用性过滤过滤故障实例再去轮询ZoneAvoidanceRule区域回避默认策略过滤不可用区域和实例下面逐个剖析它们的算法思想、源码实现和适用场景。只有把这七种策略的实现细节讲清楚面试中才不会被追问「它底层是怎么写的」而卡住。6. RoundRobinRule轮询策略6.1 算法思想轮询是最经典、最常见的负载均衡算法。RoundRobinRule 会把服务实例排成一个环每来一个请求就依次选择下一个实例选到最后一个之后重新回到第一个循环往复。例如有三个实例 A、B、C请求就会按照 A、B、C、A、B、C 的顺序分配。它的优势是足够简单、绝对公平每个实例被选中的次数基本一致缺陷是无法感知实例的真实负载。假如某台实例性能较差轮询依然会给它分配同样的流量从而可能拖垮慢节点并让快节点空闲。因此轮询适合实例性能相近、请求处理成本相对均匀的场景。6.2 源码实现RoundRobinRule 维护了一个 AtomicInteger 类型的计数器每次选择时将计数器加一并取模从而定位到对应的 Server。javapublic class RoundRobinRule extends AbstractLoadBalancerRule { private AtomicInteger nextServerCyclicCounter; private static final boolean AVAILABLE_ONLY_SERVERS true; private static final boolean ALL_SERVERS false; public Server choose(ILoadBalancer lb, Object key) { if (lb null) { return null; } Server server null; int count 0; while (server null count 10) { ListServer reachableServers lb.getReachableServers(); ListServer allServers lb.getAllServers(); int upCount reachableServers.size(); int serverCount allServers.size(); if (upCount 0 || serverCount 0) { return null; } int nextServerIndex incrementAndGetModulo(serverCount); server allServers.get(nextServerIndex); if (server null) { Thread.yield(); continue; } if (server.isAlive() server.isReadyToServe()) { return server; } server null; } if (count 10) { log.warn(No available alive servers after 10 tries from load balancer: lb); } return server; } private int incrementAndGetModulo(int modulo) { for (;;) { int current nextServerCyclicCounter.get(); int next (current 1) % modulo; if (nextServerCyclicCounter.compareAndSet(current, next)) { return next; } } } }这段代码有几个值得注意的细节。第一它使用 AtomicInteger 配合 CAS 自旋来实现计数器的线程安全递增在高并发下避免了 synchronized 的开销。第二它虽然从 allServers 中按位置取实例但取出来后还会校验 server.isAlive() 和 server.isReadyToServe()不满足条件时重新进入循环最多尝试 10 次防止选到一个已经不可用的节点。第三取模计算放在一个无限循环中直到 CAS 成功才返回保证了并发场景下计数器不会跳号。6.3 面试可能追问的点如果面试官继续追问常见的角度有为什么计数器要加一个 volatile 保证可见性为什么不用简单的 i这其实是在考察你对原子类、CAS 和可见性的理解。AtomicInteger 内部用 volatile int 存储 valueCAS 操作依赖 Unsafe 的 CPU 原子指令保证并发下递增的安全。7. RandomRule随机策略7.1 算法思想RandomRule 的做法非常直接在有效的服务实例列表中随机挑选一个。因为随机数在足够大的样本量下近似均匀分布所以从长期看请求也会被相对均匀地分配到各个实例。随机策略的优点是实现简单、没有状态不需要维护计数器缺点是单次选择波动较大短期可能出现流量倾斜。当然和轮询一样它同样无法感知实例的真实压力和响应快慢。7.2 源码实现RandomRule 会选择可达的实例列表然后用 ThreadLocalRandom 生成一个随机下标。javapublic class RandomRule extends AbstractLoadBalancerRule { public Server choose(ILoadBalancer lb, Object key) { if (lb null) { return null; } Server server null; while (server null) { if (Thread.interrupted()) { return null; } ListServer upList lb.getReachableServers(); ListServer allList lb.getAllServers(); int serverCount allList.size(); if (serverCount 0) { return null; } int index chooseRandomInt(serverCount); server upList.get(index); if (server null) { Thread.yield(); continue; } if (server.isAlive()) { return server; } server null; Thread.yield(); } return server; } protected int chooseRandomInt(int serverCount) { return ThreadLocalRandom.current().nextInt(serverCount); } Override public Server choose(Object key) { return choose(getLoadBalancer(), key); } }这段代码有几个容易踩坑的点。第一它使用 ThreadLocalRandom 生成随机数比直接使用共享的 java.util.Random 更适合高并发场景因为每个线程都有自己的随机数生成实例减少了竞争。第二chooseRandomInt 方法接收 serverCount来自 allList 的大小但最终却用这个下标去 upList 里取元素当可达实例列表比全量列表小的时候存在下标越界风险这个细节也是源码追问时的高频点。第三choose 方法进入 while 循环后先判断线程是否被打断如果被打断则返回 null保证线程在关闭过程中能够及时退出。7.3 面试可能追问的点随机策略的追问通常不会太难但有三个方向值得准备。第一Math.random() 和 ThreadLocalRandom 的区别前者底层依赖一个全局同步的伪随机数生成器高并发下存在竞争后者是 JDK 8 引入的线程隔离随机数实现性能更好。第二随机策略和轮询策略相比为什么仍然无法保证完全均匀因为随机具有短期波动性只有在样本量足够大时才趋近均匀。第三如果服务列表频繁变化随机策略会有什么问题由于每次选择都是独立的它不会像轮询那样保证相邻请求分散到不同实例极端情况下可能出现短暂的流量集中。8. RetryRule重试策略8.1 算法思想RetryRule 并不是一种独立的负载均衡算法而是在既定策略之上包装了一层重试能力。它内部默认使用 RoundRobinRule 作为真正的选择策略当某次选择没有返回可用实例时它不会马上放弃而是在一段时间窗口内反复重试直到选到一个可用实例或者超过最大重试时间。这里需要先澄清一个概念RetryRule 重试的是「选择服务实例」这个动作而不是重发 HTTP 请求。也就是说它解决的是「服务列表里暂时选不出可用实例」的问题而不是「请求已经发出去了但失败了」的问题。真正的 HTTP 请求重试需要依赖 Spring Retry 机制的配合这一点在后面的重试机制章节还会展开。8.2 源码实现RetryRule 的核心逻辑非常简单记录一个截止时间然后循环调用内部策略的 choose 方法。只要返回的 Server 为 null 且没有超过截止时间就让出 CPU 并继续尝试。默认重试窗口为 500 毫秒。javapublic class RetryRule extends AbstractLoadBalancerRule { IRule subRule new RoundRobinRule(); long maxRetryMillis 500; public RetryRule() { } public RetryRule(IRule subRule) { this.subRule (subRule ! null) ? subRule : new RoundRobinRule(); } public void setMaxRetryMillis(long maxRetryMillis) { this.maxRetryMillis maxRetryMillis; } public Server choose(ILoadBalancer lb, Object key) { long requestTime System.currentTimeMillis(); long deadline requestTime maxRetryMillis; Server answer null; answer subRule.choose(key); while ((answer null) (System.currentTimeMillis() deadline)) { Thread.yield(); answer subRule.choose(key); } return answer; } Override public void setLoadBalancer(ILoadBalancer lb) { super.setLoadBalancer(lb); subRule.setLoadBalancer(lb); } }可以看到RetryRule 的 choose 方法在第一次选择失败后进入自旋重试它没有控制重试次数只用了超时时间作为边界。这样可以短时间内兼容服务列表尚未初始化完成、或者暂时没有可达实例的情况。8.3 适用场景与注意事项RetryRule 适合服务实例列表可能出现短暂空窗期的场景例如应用刚启动、注册中心列表尚未同步完成的时候。但由于它的重试非常轻量只针对选择动作本身不能替代真正的请求失败重试。另外如果服务实例大面积宕机RetryRule 在 500 毫秒内反复空转后仍然会返回 null所以它只是缓解不是根治。9. WeightedResponseTimeRule响应时间加权策略9.1 算法思想WeightedResponseTimeRule 是一种动态的、根据实际运行情况调整权重的策略。它的核心思路是统计每个实例的近期平均响应时间响应越快的实例被赋予越高的权重被选中的概率也越大。这样在实例性能差异明显时客户端会主动把更多流量分配给更快的节点从而提升整体吞吐。它的动态性体现在两个维度。第一权重不是固定配置的而是根据每个实例每次请求返回的耗时动态计算。第二权重会随时间周期性地重新计算例如每 30 秒更新一次保证权重能跟得上服务性能的变化。9.2 源码实现思路WeightedResponseTimeRule 内部维护了一个定时任务周期性地从 LoadBalancerStats 读取每个实例的响应时间统计然后根据公式计算权重。核心公式可以概括为先求出所有实例的平均响应时间之和再以「最慢实例的响应时间减去某实例的响应时间」作为该实例的相对权重贡献最后把每个实例的权重累加形成累积权重数组。javadouble totalResponseTime 0; for (Server server : servers) { totalResponseTime stats.getResponseTimeAvg(server); } double weight 0; for (Server server : servers) { double avg stats.getResponseTimeAvg(server); weight (totalResponseTime / avg); }选择时它会在 0 到总权重之间生成一个随机数然后从权重区间里定位到对应的实例。因为累积权重是按照实例顺序排列的可以用二分查找或顺序遍历实现。9.3 适用场景与注意事项WeightedResponseTimeRule 适合实例性能存在持续差异、且客户端希望在运行时动态调整的场景例如集群里混有新旧规格的机器、或者某些实例所在网络环境较差的情况。但要注意它的权重是基于历史响应时间计算的对刚启动的实例可能不够公平因为新实例缺少统计数据同时它的实现相对复杂调试和排障成本也更高。因此它更适合中大型、流量足够大的系统。10. BestAvailableRule最空闲优先策略10.1 算法思想BestAvailableRule 的目标很直接从所有可用实例中选出当前并发请求数最小的那一个。也就是说它不看响应时间只看「谁当前手上的活最少」。这有点像超市里排最短队理论上可以让每个实例的压力更均衡。在实现中它还会过滤掉「断路器已经打开」的实例避免把请求交给已经判定为故障的节点。因此它结合了「负载感知」和「故障隔离」两种能力。10.2 源码实现思路javapublic class BestAvailableRule extends ClientConfigEnabledRoundRobinRule { private LoadBalancerStats loadBalancerStats; Override public Server choose(Object key) { if (loadBalancerStats null) { return super.choose(key); } ListServer serverList getLoadBalancer().getAllServers(); int minimalConcurrentConnections Integer.MAX_VALUE; long currentTime System.currentTimeMillis(); Server chosen null; for (Server server : serverList) { ServerStats serverStats loadBalancerStats.getSingleServerStat(server); if (!serverStats.isCircuitBreakerTripped(currentTime)) { int concurrentConnections serverStats.getActiveRequestsCount(currentTime); if (concurrentConnections minimalConcurrentConnections) { minimalConcurrentConnections concurrentConnections; chosen server; } } } if (chosen null) { return super.choose(key); } return chosen; } }这段代码有几个关键点。第一它遍历所有实例逐个检查断路器状态如果某个实例的断路器已经打开直接跳过。第二它比较每个实例的活跃请求数取最小的那个作为结果。第三如果没有一个实例通过筛选它会退化为父类的 RoundRobinRule 兜底。10.3 适用场景与注意事项BestAvailableRule 适合对当前负载敏感的短请求场景例如查询类接口。但对于长请求或响应时间差异较大的场景单纯看并发数可能不够准确因为一个处理慢请求的实例可能并发数不高但已经接近瓶颈。此外它的选择过程需要遍历全部实例实例数量较多时性能开销不可忽略。11. AvailabilityFilteringRule可用性过滤策略11.1 算法思想AvailabilityFilteringRule 的核心是「先过滤、后轮询」。它不直接对所有实例做负载均衡而是先做两轮过滤把明显不可用的实例排除掉再在剩余实例中做轮询选择。过滤条件有两个第一过滤掉断路器已经打开的实例第二过滤掉并发请求数超过阈值的实例。断路器状态和活跃请求数都来自 LoadBalancerStats。11.2 源码实现思路javapublic class AvailabilityFilteringRule extends PredicateBasedRule { private int availableFiltered 0; Override public Server choose(Object key) { int count 0; Server server roundRobinRule.choose(key); while (count 10) { if (predicate.apply(new PredicateKey(server))) { return server; } server roundRobinRule.choose(key); } return super.choose(key); } }代码里的 predicate 封装了两个过滤条件一个是判断断路器是否打开另一个是判断活跃请求数是否超过配置阈值。它最多尝试 10 次如果 10 次都没选到通过过滤的实例就退化为父类的 choose 逻辑也就是对全部实例做可用性过滤后选择。11.3 适用场景与注意事项AvailabilityFilteringRule 适合实例数量较多、需要动态屏蔽故障节点的场景。因为它每次只尝试固定次数所以对性能影响可控。它的默认过滤条件相对保守如果并发请求阈值设置得不合适可能过滤掉一些实际上还能承接流量的实例。实际项目中可以根据业务特征调整阈值。12. ZoneAvoidanceRule区域回避策略12.1 算法思想ZoneAvoidanceRule 是 Ribbon 的默认策略也是七大策略里最复杂的一个。它的核心思想是在微服务集群按区域Zone划分时优先选择与调用方同区域的实例同时过滤掉整体表现不佳的区域和故障实例。所谓「区域」通常对应云环境的可用区或机房。同区域调用延迟低、带宽成本低跨区域调用延迟高、可能产生额外流量费用。因此 ZoneAvoidanceRule 会优先在本地区域选择实例只有本地区域不可用时才考虑其他区域。12.2 源码实现思路ZoneAvoidanceRule 继承自 PredicateBasedRule内部组合了两个过滤器。第一个过滤器负责过滤掉故障区域它会统计每个区域的实例数量、平均响应时间、断路器打开比例等指标如果某个区域的错误率过高或实例数量过少就把这个区域整体排除。第二个过滤器负责过滤掉故障实例例如断路器打开的实例。在通过过滤的候选实例集合上它使用轮询算法进行最终选择。因此可以把 ZoneAvoidanceRule 理解为「区域过滤 实例过滤 轮询」的三段式策略。12.3 适用场景与注意事项ZoneAvoidanceRule 适合多机房、多可用区部署的生产环境也是 Spring Cloud 默认采用的策略。它的好处是兼顾了性能、可用性和成本但也带来了两个需要注意的点。第一区域划分依赖配置和注册中心元数据如果配置不对可能出现区域识别错误导致流量走向异常。第二它的过滤逻辑相对复杂线上排查问题需要结合 LoadBalancerStats 的指标一起看。13. Ribbon 的重试机制13.1 RetryRule 与请求重试的区别很多同学把 RetryRule 和「请求失败重试」混为一谈。前者重试的是「选择服务实例」这个动作后者重试的是「发起 HTTP 请求」。Ribbon 本身不直接负责请求重试请求重试是由 Ribbon 与 Spring Retry 的整合、或 Feign 的重试机制来完成的。13.2 Spring Cloud 中的重试配置在 Spring Cloud 中可以通过以下配置开启对同一服务的请求重试yamlspring: cloud: loadbalancer: retry: enabled: true ribbon: MaxAutoRetries: 1 MaxAutoRetriesNextServer: 1 OkToRetryOnAllOperations: false其中 MaxAutoRetries 表示同一实例内的最大重试次数MaxAutoRetriesNextServer 表示换实例重试的最大次数OkToRetryOnAllOperations 表示是否对写操作也开启重试。默认只对 GET 请求开启重试因为 POST、PUT、DELETE 等写操作天然不幂等重复执行可能造成数据问题。13.3 重试的代价与注意事项开启重试可以显著提高系统的容错能力但也会带来新的问题。第一重试会放大流量尤其在下游已经过载时重试会让情况进一步恶化。第二重试对幂等性有要求写接口一旦重复执行可能产生重复扣款、重复下单等问题。第三重试会增加请求延迟超时时间设置不当可能导致请求链路上雪崩。因此生产环境中应当遵循几条原则只对幂等接口开启重试、设置合理的重试次数上限、配合熔断器使用、监控重试指标。14. Feign 与 Ribbon 的整合原理14.1 Feign 与 Ribbon 的协作方式Feign 是一个声明式 HTTP 客户端它本身不包含负载均衡能力。Feign 和 Ribbon 整合之后当使用 FeignClient 声明一个服务调用接口时Feign 会把服务名的解析委托给 Ribbon由 Ribbon 完成实例选择再通过 HTTP 客户端发起请求。整个调用链可以概括为Feign 接口的方法调用 - 方法签名和参数被解析 - 通过 LoadBalancerFeignClient 请求 Ribbon - Ribbon 根据服务名找到对应的 ILoadBalancer - IRule 选出目标实例 - 使用 HTTP 客户端向该实例发起请求。14.2 饥饿加载Ribbon 在首次访问某个服务时才会初始化对应的 ApplicationContext这个过程需要拉取服务列表、初始化 ILoadBalancer 等耗时可能达到几百毫秒甚至更久。对于延迟敏感的服务第一次请求会明显变慢这就是「首次调用慢」问题的根源。Spring Cloud 提供了饥饿加载配置让应用启动时就把指定服务的 Ribbon 上下文初始化好yamlribbon: eager-load: enabled: true clients: user-service,order-service开启饥饿加载后服务列表会在启动阶段就完成初始化首请求延迟问题会明显改善但也会增加启动时间需要在两者之间做权衡。15. 自定义负载均衡策略实战15.1 自定义 IRule假设有一个需求某个服务希望把请求优先发给响应最快的实例同时过滤掉最近 1 分钟内出错率超过 30% 的节点。可以通过实现 IRule 接口来完成。javapublic class SmartRule extends AbstractLoadBalancerRule { Override public Server choose(Object key) { ILoadBalancer lb getLoadBalancer(); ListServer servers lb.getReachableServers(); if (servers.isEmpty()) { return null; } Server best null; long bestScore Long.MAX_VALUE; for (Server server : servers) { ServerStats stats LoadBalancerStats.from(getLoadBalancer()) .getSingleServerStat(server); if (stats.getFailureCount() 0 stats.getSuccessCount() / (double) stats.getFailureCount() 3) { continue; } long score (long) stats.getResponseTimeAvg(); if (score bestScore) { bestScore score; best server; } } return best ! null ? best : servers.get(0); } }15.2 注册自定义策略在配置类中为指定服务注册自定义 IRulejavaConfiguration public class RibbonConfig { Bean public IRule ribbonRule() { return new SmartRule(); } } RibbonClient(name user-service, configuration RibbonConfig.class) public class UserServiceClient { }需要注意的是RibbonConfig 不能位于主应用的 ComponentScan 扫描范围内否则它会成为全局默认策略影响所有服务。这也是 Ribbon 自定义配置中最容易踩的坑之一。16. 面试高频追问及答题思路16.1 为什么默认策略是 ZoneAvoidanceRule可以从三个角度回答。第一微服务集群通常按区域划分同区域调用延迟低、成本低ZoneAvoidanceRule 天然支持这一点。第二它同时具备区域过滤和实例过滤能力能自动屏蔽故障区域和故障实例稳定性更好。第三它是 Ribbon 官方在多区域场景下长期验证的默认选择。16.2 如何排查负载均衡策略没有生效常见原因有三个。第一自定义 IRule 放在了主应用扫描范围内变成了全局策略第二RibbonClient 配置里的 name 与实际调用的服务名不一致第三负载均衡策略对象没有被正确注入IRule 仍然是默认的 ZoneAvoidanceRule。排查时可以先打日志打印当前生效的 IRule 类型再逐步核对配置。16.3 Ribbon 和 Spring Cloud LoadBalancer 有什么区别从定位上两者都是客户端负载均衡器。从实现上Ribbon 是 Netflix 维护的组件配置方式和接口偏繁琐已进入维护模式Spring Cloud LoadBalancer 是 Spring Cloud 官方推出的替代品接口更简洁支持响应式编程模型。从迁移上Ribbon 的很多概念ILoadBalancer、IRule在 LoadBalancer 中都有对应的抽象理解 Ribbon 有助于快速上手 LoadBalancer。16.4 如何应对服务实例更新导致的瞬时抖动可以从三点入手。第一开启饥饿加载避免首请求慢。第二调整 PollingServerListUpdater 的刷新间隔让服务列表更新更及时。第三结合熔断器和重试策略即使偶发选到刚下线的实例也能通过重试快速恢复。17. 总结Ribbon 看似只是「负载均衡策略」几个字实际上串联了客户端负载均衡、服务发现、实例存活检测、列表更新、重试、熔断、区域感知等一整套微服务核心能力。理解 Ribbon本质上就是理解「服务消费者如何选择服务提供者」这件事背后的设计取舍。回顾一下要点客户端负载均衡 vs 服务端负载均衡Ribbon 属于客户端负载均衡运行在服务消费者进程内从注册中心拉取实例列表后自行选择目标实例。五大核心接口ILoadBalancer、IRule、ServerList、IPing、ServerListUpdater 各司其职构成 Ribbon 的骨架。七大策略各有侧重RoundRobinRule 简单公平、RandomRule 无状态、RetryRule 提供选择重试、WeightedResponseTimeRule 按响应时间加权、BestAvailableRule 看并发数、AvailabilityFilteringRule 过滤故障实例、ZoneAvoidanceRule 兼顾区域和实例。重试要区分层级RetryRule 重试选择动作Spring Retry 重试请求本身两者不能混淆。Feign 与 Ribbon 的整合Feign 负责声明式调用Ribbon 负责实例选择两者配合形成完整的客户端负载均衡链路。自定义策略要注意作用域自定义 IRule 不能放在主应用扫描范围内否则会变成全局策略。
返回列表