Dubbo 的负载均衡这块,基本是每个搞微服务的 Java 工程师都绕不开的话题。不管是面试被问到“Dubbo 默认负载均衡策略是什么”,还是线上 409 高延迟排查时发现流量全打在一个 Provider 上,最终都会回到同一个问题——RPC 场景下的负载均衡,到底和 Nginx 那种 HTTP 负载均衡差在哪,Dubbo 内置的几种策略各自又适合什么场景。这篇文章我就把自己的理解和实际排查经验摊开来讲,从源码逻辑到配置落地,把随机、轮询、最少活跃调用、一致性哈希这几种策略一次说清楚,顺便把权重预热、虚拟节点这些容易忽略的细节也补上,让刚接触 Dubbo 的同学能直接照着用,让写过几年的老手也能对“一致性哈希为什么需要 160 个虚拟节点”这类细节有个更踏实的答案。
1. 负载均衡在 RPC 架构里的位置,先别急着对比算法
1.1 消费端负载均衡和服务端负载均衡是两码事
很多人刚接触 Dubbo 时会惯性思维,把负载均衡和 Nginx、F5 这类东西划等号,其实这是两类完全不同的机制。Nginx 是典型的服务端负载均衡,请求先到达 Nginx,由它根据 upstream 配置把请求转发给后端的某台 Web 服务器,客户端感知不到背后到底有几台机器。而 Dubbo 的负载均衡发生在消费端,服务消费者本地已经维护了一个可用 Provider 列表(Invoker 列表),每次发起 RPC 调用前,先从列表里按照某种策略选出一个 Invoker,再发起远程调用。
这两者的核心差异决定了设计思路的不同。服务端负载均衡代理了所有流量,所以它必须关注连接数、吞吐量、健康检查、会话保持等一大堆问题,还要考虑自身不能成为单点。而消费端负载均衡是分布式无中心的,每个消费端都独立做选择,即使一个消费端挂了也不影响整体,代价是没有全局视角,每个消费端拿到的 Provider 列表可能因为注册中心推送延迟而不同,选择结果天然带有局部性。
Dubbo 的负载均衡接口定义也很简洁,核心逻辑就是一句话:从一组 Invoker 里挑一个出来。接口叫 LoadBalance,2.7.x 版本以后核心方法签名是public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException,传入可用的调用者列表、调用方 URL 和本次调用的信息,返回选中的那个。这个接口是 @SPI 注解的,意味着可以自定义扩展,默认策略通过dubbo.loadbalance配置项指定。
1.2 为什么 Dubbo 的默认策略不是很多人以为的轮询
关于默认负载均衡,网上答案很多,但真正靠谱的说法是:Dubbo 2.6.x 及之前版本默认是 RandomLoadBalance(加权随机),2.7.x 也延续了这个默认值,到了 3.x 协议默认依然是 random。很多人以为是轮询,其实是把 Nginx 的默认策略惯性带进来了。
Dubbo 把随机作为默认策略,我认为主要是三个考虑:
一是随机策略的代码逻辑最简单,没有状态,不需要记录上一次选到哪个节点,也不需要在并发时维护计数器的一致性。二是加权随机在实际生产环境中足够用,服务提供方通常会配置不同权重来应对机器规格差异,随机策略配合权重可以在大体上让流量按比例分布。三是万一某个节点出问题,Dubbo 的集群容错机制(比如 Failover)会重试其他节点,随机策略天然降低了连续重试都命中同一异常节点的概率。
轮询策略看起来公平,但它有一个隐含假设——所有 Provider 处理能力完全一样,每次请求耗时也差不多。这在机器配置统一、接口逻辑稳定的场景下没毛病,可一旦某台机器 GC 变慢或者网络抖动,轮询依然会把新请求送过去,导致这台机器越来越慢,最后拖垮整个调用链路。随机策略虽然也会把请求打到慢节点,但从概率上看不会像轮询那样“稳定地”每次都打到它,配合超时重试机制更容易自愈。
1.3 负载均衡在调用链路中的触发时机
Dubbo 的调用链路大致是:消费端通过代理对象发起调用,经过 Cluster 层做容错(比如 FailoverCluster 会在这里做重试),然后到达 LoadBalance 做选择,选出的 Invoker 通过 Directory 获取真实地址,最后走 Netty 等通信层发送请求。
这是理解很多问题的关键。比如你配置了retries="2",实际上在一次业务调用中,LoadBalance 会被执行最多三次(第一次调用加两次重试),每次重试都可能选中不同的 Provider。也就是说,负载均衡不仅决定了第一个请求发给谁,还决定了失败后的重试流量怎么分布。
我在实际排障时遇到过一种情况:一个消费端接口超时率很高,排查日志发现报错的全是同一台 Provider,但负载均衡配置的是随机策略。这是因为消费端重试机制在起作用——第一次请求超时后,重试选中了其他节点,但因为业务方法本身耗时就不稳定,两三次重试后又回到了最初的慢节点。这种情况下单纯调整负载均衡策略是没用的,真正要解决的是把超时时间、重试次数和慢节点的隔离机制配合起来。
2. 四大内置负载均衡策略的源码逻辑拆解
2.1 RandomLoadBalance:加权随机的实现细节
先看默认的随机策略。RandomLoadBalance 是 AbstractLoadBalance 的子类,核心逻辑是在doSelect方法里。它会遍历所有 Invoker,通过getWeight方法拿到每个调用者的权重,然后累加总权重,生成一个[0, totalWeight)之间的随机数,看落在哪个区间就选哪个 Invoker。
逻辑本身不复杂,但有几个容易忽略的细节值得单独说。
第一个是权重为零或全部相等的处理。如果所有 Provider 的权重都相同,Dubbo 做了优化,直接用ThreadLocalRandom.current().nextInt(length)生成随机下标,省去区间判断的循环。如果某个 Provider 权重是 0,说明它不接收流量(可以用来做优雅下线),遍历时会跳过。
第二个是权重更新和预热机制。getWeight方法并不只是返回配置里的静态权重,它会结合 Provider 的启动时长做增量计算,具体逻辑放在 2.3 节单独展开,这里先说结论:新启动的 Provider 在预热期内权重会从很小的值逐步增长到实际配置值,避免冷启动时 JIT 未完成就接收大量流量。
第三个是随机数生成器选择。JDK 自带的Math.random()每次调用都要用 synchronized 保证原子性,并发高时会成为瓶颈。Dubbo 用了ThreadLocalRandom,每个线程维护自己的随机种子,减少了竞争,这也是为什么在万级 QPS 下随机策略的开销依然可以忽略不计的原因。
从源码层面看,随机策略几乎没有什么维护成本,也不用关心上一次的调用状态,适合大多数无状态服务。它的缺点也很直接:无法感知 Provider 的真实健康状态,一个响应时间已经飙到 5 秒的节点,依然有概率被选中。它只能保证“概率上平均”,不能保证“实际上平均”。
2.2 RoundRobinLoadBalance:平滑加权轮询的实现与坑点
再来看轮询。Dubbo 的 RoundRobinLoadBalance 并不是简单的 A->B->C 轮流来,而是带权重的平滑轮询,这个设计跟 Nginx 的 smooth weighted round-robin 思路是一脉相承的。
实现上,每个 Invoker 对应一个 AtomicInteger 记录当前轮询的权重值,每次调用时把所有 Invoker 的当前权重都加上各自的配置权重,然后选出当前权重最大的那个,最后把选中的 Invoker 当前权重减去总权重。这个算法保证了在一个轮询周期内,每个节点被选中的次数比例接近权重比例,而且不会出现某个大权重节点被连续选中的情况,分布非常平滑。
举个例子:A 权重 80,B 权重 20。第一轮,A 当前权重从 0 变为 80,B 变为 20,选 A,A 减 100 变成 -20;第二轮,A 从 -20 变 60,B 从 20 变 40,选 A,A 减 100 变 -40;第三轮,A 从 -40 变 40,B 从 40 变 60,选 B,B 减 100 变 -40。整个周期下来 A 被选中 4 次,B 被选中 1 次,比例恰好是 80:20,而且 B 不会永远排在最后。
轮询策略的优势是绝对均匀,适合请求量波动大但每个请求耗时相对稳定的场景。最典型的例子是定时任务批量调用接口,如果一批任务数量是固定的,轮询能让每台 Provider 处理的任务数尽量一致,方便提前预估资源使用。
但轮询有两个很明显的坑。第一,它需要维护轮询状态,Dubbo 的实现用了一个ConcurrentMap<String, AtomicInteger>来按方法维度记录权重快照,接口升级或重启时状态会重新初始化,瞬间可能造成短暂的流量分布不均。第二,轮询本身没有任何性能感知能力,在 Provider 能力差异大或有个别慢节点时,慢节点会稳定接收新流量,加速它的资源耗尽。
我在生产环境里遇到过轮询导致的问题:某个接口有三台 Provider,其中一台机器因为磁盘故障导致 IO 等待很高,响应时间从 20ms 飙升到 2s。因为用的是轮询,每三个请求必有一个打到这台故障机器,调用方整体成功率直线下降。后来在监控里看到这台机器 CPU 不高但 IO 繁忙,排查到根因后临时把它权重改成 0,让轮询跳过它,流量才恢复平稳。所以如果你确定用轮询,一定要配套做好 Provider 的健康检查和权重动态调整。
2.3 LeastActiveLoadBalance:最少活跃数策略的活性计数机制
最少活跃数策略,核心思想是:谁当前处理的请求少,就把新请求发给谁。Dubbo 对“活跃数”的定义是正在处理中的调用数量,也就是请求发出去之后还没收到响应的数量。
这个数量并不是 LoadBalance 自己统计的,而是由 ActiveLimitFilter 在 Filter 链路里维护的。当消费端发起调用时,进入 Filter 链,ActiveLimitFilter 会对当前 Invoker 的活跃数执行before逻辑加一,等调用结束后再减一。换句话说,这个数值是一个消费端视角的本地计数,跟 Provider 端实际的并发处理能力没有直接关系。
LeastActiveLoadBalance 的doSelect实现会遍历 Invoker 列表,找到活跃数最小的那一批,如果最小的只有一个,直接选中;如果有多个,则在这几个里面再按权重随机选一个。这样做实际上是“最少活跃优先 + 权重随机兜底”的组合,既保证了流量能避开当前正在堆积的节点,又能在多个空闲节点之间保持权重均衡。
我个人认为,这个策略在慢接口场景下非常实用。比如一个接口内部调用了第三方服务,耗时波动很大,有的请求 100ms 就返回,有的要等 3 秒。使用轮询或随机策略时,那台处理了慢请求的 Provider 会被继续打入新请求,线程池很快被占满。而最少活跃数策略会在请求未返回期间提高该节点的活跃数,新请求就会被路由到其他更空闲的节点,相当于做了一个消费端层面的自适应流量转移。
不过它有滞后性。活跃数的统计基于历史调用,只有在 Provider 已经积压了请求后,消费端才能感知到并调整路由,而且这种感知是消费端本地的,不同消费端的感知速度不一样。如果所有消费端同时向同一台机器发起大量请求,活跃数上升的速度可能跟不上流量增长的速度,还是会出现瞬时倾斜。所以在要求非常严格的场景下,还是要依赖 Provider 端的线程池告警和限流配合。
2.4 ConsistentHashLoadBalance:一致性哈希的参数路由与虚拟节点设计
一致性哈希在分布式缓存(Redis、Memcached)场景里很常见,Dubbo 也内建了这个负载均衡策略,核心思路是:根据调用参数计算 hash 值,映射到一个 0 到 2^32-1 的哈希环上,然后顺时针查找,找到的第一个虚拟节点对应的 Provider 就是目标节点。
第一次看到 Dubbo 的一致性哈希实现时,我最大的困惑是:为什么默认副本数是 160,而不是常见的 100 或者 200?答案跟 TreeMap 的存储结构密切相关。ConsistentHashLoadBalance 内部会为每个方法构造一个ConsistentHashSelector,它用TreeMap<Long, Invoker>存储哈希环,虚拟节点的 key 是通过md5(节点地址 + 序号)计算出来的。160 个副本节点意味着每个 Provider 在环上均匀分布 160 个位置,这样在 Provider 数量较少时(比如两个节点),哈希环上的节点分布依然足够均匀,减少数据倾斜。如果只有两个 Provider 却只有 10 个虚拟节点,很可能环上某个弧段过长,导致大量请求集中到某一台。
一致性哈希最大的价值在于,相同参数的请求一定会路由到同一个 Provider。这正好适应一些带本地缓存的服务。比如用户维度的数据缓存在 Provider 的本地内存里,如果同一用户的请求总是打到不同机器,每台机器都要去数据库查一遍,缓存完全失效。用一致性哈希做参数路由,同一个 userId 的请求会稳定地打到同一台机器,缓存命中率大幅提升,数据库压力也会明显下降。
但这个策略也有两个关键坑点需要了解。
第一个是 hash 参数的确定方式。Dubbo 默认取调用方法的第一个参数做 hash 计算,如果第一个参数是基本类型或 String,行为符合直觉;但如果第一个参数是一个复杂对象,hashCode()方法会参与计算,而你重写了 hashCode 或者对象内部字段顺序变化,会导致同一逻辑用户的请求被路由到不同节点,缓存策略失效。我在实际项目中遇到过对象equals和hashCode被 Lombok 自动生成而变动,结果一致性哈希完全失灵的问题。解决方式是配置 hash 参数下标,显式指定参与 hash 的参数位置,比如methods=sayHello时配置arguments="0,1",让核心标识字段稳定参与计算。
第二个是节点上下线时的流量迁移。一致性哈希相比取模的好处是节点变化时只影响部分流量,但“部分”有多大取决于虚拟节点分布。如果 Provider 数量少且虚拟节点不均匀,某台机器下线后,它对应的流量只会顺时针迁移到下一个虚拟节点,如果下一个虚拟节点恰好集中在一台 Provider 上,这台 Provider 就会瞬间接收到大量流量,表现就是负载飙高甚至 OOM。所以节点数少时建议调大虚拟节点参数virtual.nodes,控制在 320 或更高,让流量迁移更平滑。
3. 权重与预热机制,这些细节决定了线上表现
3.1 权重在负载均衡中的作用和配置方式
Dubbo 的权重配置按 Provider 维度设置,在 service 暴露时通过weight参数指定,默认值是 100。比如 XML 配置里可以这样写:
<dubbo:service interface="com.example.UserService" ref="userService" weight="200" />或者在注解配置里,通过@Service(weight = 200)指定。
权重的意义在于,它告诉消费端“这台 Provider 应该接收多大的流量比例”。三台机器权重分别是 100、200、300,那么理想状态下流量比例是 1:2:3。这个比例在随机、轮询、最少活跃数策略中都会生效(一致性哈希不直接使用权重,它只关心参数 hash)。
但我见过很多团队配了权重却完全没生效的情况。原因往往是接口级配置了方法级覆盖。Dubbo 的配置覆盖规则是细粒度优先,方法级配置会覆盖接口级配置。如果接口级配置了weight=200,但某个方法上又写了weight=100,那么走到这个方法时,权重就变成了 100。排查这类问题时可以先查配置中心的最终配置快照,再确认是否有多层覆盖。
3.2 预热权重的计算逻辑,为什么新启动的节点不能立刻接收全量流量
Dubbo 的 AbstractLoadBalance 里有一个getWeight(Invoker<?> invoker, Invocation invocation)方法,它做了三件事:先读取配置的 weight,然后判断当前调用是否包含预热上下文,最后结合 Provider 启动时长计算实际生效权重。
具体计算逻辑是:如果 Provider 启动时间小于预热时间(默认 10 分钟),实际权重会按照启动时长 / 预热时间 * 配置权重的比例计算。比如配置权重 100,启动 1 分钟后,实际生效权重大概是 10;启动到 5 分钟时,变成 50;满 10 分钟后才完全生效。由于负载均衡的权重计算发生在每次调用时,所以这个值是动态的,不需要人工干预。
这个机制的原理很简单:新启动的 JVM 进程需要经历类加载、JIT 编译热点代码、连接池初始化、缓存预热等过程,前几分钟性能通常不如稳定运行期。如果新节点一启动就接收全量流量,很可能会因为自身性能不足导致超时,然后消费端触发重试把流量又打到其他节点,整体成功率反而下降。预热机制让新节点先接收少量流量,逐步提升到正常水平,属于一种自我保护策略。
我自己的经验是:如果服务启动过程特别重(比如要加载几十万条缓存),10 分钟预热时间可能不够。这时候可以调大warmup参数,或者在服务完全初始化完成前不注册到注册中心(通过延迟注册),两者结合效果更好。Dubbo 3.x 里还支持在启动阶段配合优雅上下文的方案,让流量缓慢导入,线上线下表现都更符合预期。
3.3 动态权重调整,实现无感知上下线和容量扩缩
权重还可以在运行时动态调整。最简单的方式是通过 Dubbo Admin 控制台,对某个 Provider 的 weight 做修改,修改后会实时推送到注册中心,消费端感知到配置变更后更新本地权重信息。这在临时摘除故障节点、灰度发布、容量评估场景下非常实用。
举个典型的例子,一台 Provider 内存持续增长但还没到崩溃阈值,你可以先把它的权重从 100 调到 50,让新流量减半,观察一段时间再决定是继续下调还是重启。相比直接下线服务,权重调整既保留了部分流量的健康探测能力,又避免了流量瞬间全部转移引发的另一台 Provider 压力突增。
权重操作时有一个注意点:权重变更推送到所有消费端不是同步完成的,可能出现短暂窗口内部分消费端还在按旧权重路由。如果变更幅度很大(比如从 100 调到 0),流量不会瞬间完全停止,需要等待注册中心推送完成。在严格的发布流程中,建议配合服务限流和监控阈值,而不是单纯依赖权重来做流量切零。
4. 负载均衡策略的配置与选型,结合注册中心看实际落地
4.1 XML、注解、API 三种配置方式的优先级
Dubbo 的配置层级很多,负载均衡的配置同样遵循“消费者方法级 > 消费者接口级 > 服务提供者方法级 > 服务提供者接口级 > 全局默认”的优先级顺序。实际配置常见有三种方式。
XML 方式,在消费者端配置最直观:
<dubbo:reference id="userService" interface="com.example.UserService" loadbalance="roundrobin"> <dubbo:method name="getUserById" loadbalance="consistenthash"/> </dubbo:reference>注解方式,适合 Spring Boot 项目。在 @DubboReference 注解中指定:
@DubboReference(loadbalance = "consistenthash", methods = { @Method(name = "getUserById", loadbalance = "consistenthash") }) private UserService userService;API 方式则是在构建 ReferenceConfig 时设置:
ReferenceConfig<UserService> reference = new ReferenceConfig<>(); reference.setLoadbalance("consistenthash");我建议统一在消费者端配置,而且尽量通过配置中心下发,别把负载均衡配置写死在代码里。原因是负载均衡策略往往需要根据线上实际情况动态调整,今天用随机,明天可能因为缓存问题要切到一致性哈希,如果写死代码就要重新发版。通过 Nacos 等配置中心可以做到运行时调整,配合监控数据看效果,迭代效率高很多。
4.2 全局默认策略和接口级策略的联动
Dubbo 支持在全局层面设置默认负载均衡策略,通过dubbo.consumer.loadbalance配置项指定:
dubbo.consumer.loadbalance=leastactive这个全局配置对所有消费者接口生效。但某些接口业务特征不同,需要单独定制。比如大部分无状态接口用 random 就好,但一个本地缓存命中率很关键的接口必须用 consistenthash,这时候接口级配置覆盖全局默认。
这里有个容易踩的坑:你只想改某个接口的策略,却在全局配置里改了,结果所有接口的负载均衡行为都变了,如果某些接口恰好依赖轮询的绝对均匀性,线上流量分布可能瞬间错乱。所以新增策略时最好先通过接口级配置灰度观察,再决定是否提升为全局默认。
4.3 结合 Nacos 注册中心看消费端地址列表更新对负载均衡的影响
负载均衡生效的前提是消费端有一份正确的 Provider 地址列表。Dubbo 2.7 以后支持 Nacos 作为注册中心,服务提供者启动时向 Nacos 注册实例,消费者通过订阅获取地址列表,并在变更时实时收到推送。
地址列表的变动会直接影响负载均衡效果。比如某台 Provider 挂了,Nacos 会推送下线事件,消费端从 Invoker 列表里移除该节点后,负载均衡才不会选中它。但这个推送有延迟,在延迟窗口内依然可能调用到已经下线的节点,触发连接异常。Dubbo 的容错机制会捕获这个异常并重试,此时负载均衡会在剩余的 Invoker 里重新选择,所以短暂的地址不一致通常不构成大问题。
但有一种情况值得注意:如果服务提供者优雅停机没有做好,进程直接被杀,活跃连接被 RST,消费端会在调用时才发现异常。此时如果重试次数配置不当,一次业务调用会产生大量异常日志和多次网络连接,负载均衡的压力也会上升。生产环境建议通过 preStop 钩子主动向注册中心反注册,并等待几秒让消费端刷新地址列表后再真正停机。
4.4 场景化策略选型参考,不只看算法本身
选负载均衡策略,本质是你在回答一个问题:“我的流量特征需要怎样的路由倾向?”我整理了一张选型参考表,按常见场景给出倾向性建议:
| 场景特征 | 推荐策略 | 核心原因 |
|---|---|---|
| 无状态接口,Provider 配置一致 | random | 实现简单,无状态维护,默认值足够可靠 |
| 请求耗时长且波动大 | leastactive | 活跃数感知堆积,避免请求持续打到慢节点 |
| 有状态服务,依赖本地缓存 | consistenthash | 相同参数路由到同一节点,提升缓存命中率 |
| 批量任务,请求数量固定 | roundrobin | 均匀分发任务,便于预估每台机器处理量 |
| Provider 能力差异大,需要加权 | random/leastactive 配合 weight | 权重让高配机器承担更多流量 |
| 对流量倾斜极度敏感,需要平滑 | roundrobin | 平滑加权轮询的分布波动最小 |
这张表只是起点,真正选型时要结合你接口的超时设置、重试次数、Provider 数量、机器规格差异来综合判断。我见过有人为了缓存命中率强行用一致性哈希,但方法参数里有个随机生成的 traceId 导致 hash 每次都不同,结果一致性哈希完全失效,流量分布还不如随机均匀。这种情况要先解决参数设计的问题,而不是纠结策略本身。
5. 常见问题与排查技巧实录
5.1 问题一:配置了 weight=0,但流量没有完全切走
关于权重为 0 的实例,Dubbo 的处理是:在 RandomLoadBalance 中权重为 0 的节点不会参与区间计算,意味着它们不会被选中。但有一个前提:Invoker 列表里必须还有权重大于 0 的可用节点。如果所有节点权重都是 0,负载均衡会退化,为了保证可用性仍然会选中其中一个。
我遇到的实际场景是:某台机器内存告警,运维把三台 Provider 中的一台 weight 改为 0 期望它不再接收流量,但配置中心推送有延迟,且另一台机器恰好也处于过载自动降权状态,导致权重为 0 的节点依然收到了请求。后来我们约定:临时摘流优先用注册中心的禁用操作,而不是权重置零,禁用能直接从消费端地址列表移除节点,更干净。
5.2 问题二:一致性哈希生效后,流量严重倾斜到单台机器
这个问题的根因基本都在虚拟节点数上。服务 Provider 只有 2 台,默认 160 个虚拟节点,单机节点下线后,原本分布在这台机器上的区间全部转移到顺时针相邻的虚拟节点,如果恰好几段区间都落在同一台机器上,这台机器就会短时接收远超预期的流量。
排查时先看 Provider 数量,如果服务实例数少于 3,建议把virtual.nodes调大到 320 或 640。调大虚拟节点数能显著减小单个节点故障时的流量迁移集中度,代价是内存占用增加和 hash 环构建时间变长,但相对流量倾斜来说,这点开销不值一提。
还有一种隐蔽情况:不同方法共用同一个 hash 环吗?答案是否定的。ConsistentHashSelector 是按方法维度构造的,每个方法有自己的 TreeMap 环,所以不同方法之间互不影响。如果你发现只有某个方法流量倾斜,其他方法正常,那就不是虚拟节点的问题,而是这个方法本身的参数 hash 分布有偏,比如参数值集中在某几个值上。
5.3 问题三:随机策略下,部分 Provider 的 QPS 差异仍然很大
随机策略从概率上保证流量比例,但样本量不足时偏差会很显眼。比如每秒只有 10 个请求,两台的机器即使权重相同,实际 QPS 也可能 7:3 分布,这是正常的统计波动。如果 QPS 明明很高,长期看分布依然不均匀,那就要考虑几个因素:配置权重是否正确、是否有方法级覆盖、消费端是否有本地缓存导致列表刷新不及时、Provider 是否配置了多个协议导致 Invoker 列表膨胀。
我遇到过一个真实案例:同一服务通过 dubbo 协议和 rest 协议同时暴露,消费端引用时没有指定协议,由于 Invoker 列表里 dubbo 和 rest 各占一份,导致负载均衡时 50% 的流量走 rest 协议,而 rest 协议的性能明显弱于 dubbo。最终表现就是整体 RT 偏高且波动大,排查了很久才发现是协议混用的问题,跟单纯选策略没关系。
5.4 问题四:重试与负载均衡叠加后,异常流量被放大
前面提到,Failover 容错模式下,一次业务调用失败后会按retries次数重新调用,每次重新调用都会重新走负载均衡。这在随机和轮询策略下问题不大,但遇到一致性哈希时就有风险了:同一个参数再次 hash,依然选中同一台 Provider,如果这台机器真的挂了,重试相当于继续打同一台故障机器,直到超时耗尽。
解决方式有两个方向。一个是将一致性命 hash 和 Failover 结合使用时,对retries配置持保守态度,不要设置太大,因为重试无法帮你换节点。另一个是配置cluster=failfast,快速失败而不是重试,让上层业务决定是否重新发起完整调用。在一致性哈希用于本地缓存场景时,我倾向于 failfast,因为重试带来的收益很低,反而会放大对故障节点的压力。
5.5 问题五:自定义负载均衡扩展的正确姿势
Dubbo 的 @SPI 扩展机制允许你实现自己的 LoadBalance,实际业务中确实有一些场景内置策略覆盖不了,比如按机房优先、按 Provider 的实时健康分路由。
扩展一个自定义 LoadBalance 并不复杂,三步走:
实现 LoadBalance 接口(或继承 AbstractLoadBalance),在doSelect里写你的选择逻辑。在META-INF/dubbo目录下创建接口全限定名对应的文件,比如org.apache.dubbo.rpc.cluster.LoadBalance,文件内容写myStrategy=com.example.MyLoadBalance。消费端配置loadbalance="myStrategy"即可生效。
一个最常见的自定义策略例子是机房优先策略:消费端根据自身所在机房,优先选择同机房的 Provider,只有同机房不可用时才跨机房。实现思路是遍历 Invoker,读取 URL 中的zone参数,跟本地配置的zone对比,优先过滤出同机房节点,再在这些节点里用随机或最少活跃策略选择。这里要注意,自定义策略要做兜底,不能因为同机房节点全部不可用就返回 null,否则调用会直接失败。
我建议自定义策略前先想清楚一个原则:负载均衡只负责“怎么从可用节点里选”,不负责“判断节点是否可用”。健康检查、熔断这些应该交给注册中心、集群容错和 Filter 链去处理,不要在负载均衡里做太重的逻辑,否则每次调用前的选择开销会变成新的瓶颈。
6. 从 Dubbo 3.x 看负载均衡的未来:更智能的调度是趋势
Dubbo 3.x 引入了新的 Triple 协议和更多服务治理能力,负载均衡方向也有新的内容。比如 ShortestResponseLoadBalance(最短响应时间策略)在 2.7.x 后期版本就已经加入,它统计每个 Provider 的滑动窗口平均响应时间,优先选择响应最快的节点。这种策略适合 Provider 能力差异大且请求耗时敏感的接口,某种程度上比最少活跃数更“聪明”,因为它直接拿结果说话,而不是通过并发数间接推测。
同时,Dubbo 3.x 在负载均衡中引入了更多自适应机制,比如基于服务治理数据的动态权重调整,结合应用级服务发现让地址列表更可靠。我个人觉得,未来负载均衡会越来越趋近于“数据驱动”:不只是看权重和静态配置,而是综合响应时间、错误率、连接池状态等指标做动态路由。从运维角度看这是好事,但从排查角度看,策略越智能,行为越难以用简单规则预测,所以对可观测性的要求也会更高。
回到实践,我的建议是:大多数团队不必一开始就追求最复杂的策略,先摸清接口的流量特征和 Provider 的表现,再逐步引入更复杂的路由逻辑。负载均衡只是整个微服务治理体系中的一个环节,它解决的是“流量怎么分”的问题,“节点是否健康”“服务是否可用”这些问题需要由注册中心、集群容错、熔断限流共同回答,别指望某一个组件能包打天下。
最后分享一个我的个人习惯:每次调整负载均衡配置,都会在监控面板上同时观察四个指标——各 Provider 的 QPS 分布、平均响应时间、错误率、线程池活跃度。只调一个维度往往不能解决问题,多指标联动才能判断调整是否真的有效。Dubbo 的负载均衡配置看起来只是一个字符串,但它的背后是整个调用链路的稳定性设计,值得多花一点心思去理解。