概述
消费者引用了一个服务,注册中心里挂着三台 Provider,这次调用到底该打哪一台?Dubbo 把这个决策内置成了可配置的策略,loadbalance 一行属性就能换。但它生效有个前提,工程里那个 loadbalance = "Random" 也藏着容易误读的细节,这篇把它讲透并跑一遍验证。
纲要
- 前提:负载均衡只在「同一服务、同一版本、有多个实例」时才有效果
- 内置策略:
Random/RoundRobin/LeastActive/ConsistentHash/ShortestResponse - 「随机」不是均分:
Random与RoundRobin都带权重语义,权重决定了概率与配额 - 五种策略的对比表:选择依据、优点、缺点、适用场景
- 三种配置方式:消费者侧
@DubboReference、提供者侧@DubboService、全局 yaml - 配置的层级与优先级,以及为什么「怎么选实例」应该由消费者决定
- 权重怎么配:
weight属性、yaml、dubbo-admin 动态调整 - Dubbo 的权重和注册中心(Nacos)的权重是两套机制------这条要分清楚
- 实战:用
dubbo-demo的两个 Provider 实例验证「策略切换前后调用分布的变化」 version不同 ≠ 负载均衡,这是最容易混淆的一处- 对照:Dubbo 负载均衡与 SpringCloud Ribbon / LoadBalancer 的关系
- 实战坑清单与下一篇衔接
前提:没有多实例,配什么策略都一样
先把这条摆在最前面,因为它是「配完看不到效果」的唯一原因。
负载均衡的输入是一个提供者列表 ,输出是列表里被选中的某一个。列表长度为 1 的时候,无论选谁都是它,策略写成 Random、RoundRobin 还是 LeastActive,调用链路完全一致。
#mermaid-svg-u58SxWXMM1XnnAIo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-u58SxWXMM1XnnAIo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-u58SxWXMM1XnnAIo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-u58SxWXMM1XnnAIo .error-icon{fill:#552222;}#mermaid-svg-u58SxWXMM1XnnAIo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-u58SxWXMM1XnnAIo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-u58SxWXMM1XnnAIo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-u58SxWXMM1XnnAIo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-u58SxWXMM1XnnAIo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-u58SxWXMM1XnnAIo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-u58SxWXMM1XnnAIo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-u58SxWXMM1XnnAIo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-u58SxWXMM1XnnAIo .marker.cross{stroke:#333333;}#mermaid-svg-u58SxWXMM1XnnAIo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-u58SxWXMM1XnnAIo p{margin:0;}#mermaid-svg-u58SxWXMM1XnnAIo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-u58SxWXMM1XnnAIo .cluster-label text{fill:#333;}#mermaid-svg-u58SxWXMM1XnnAIo .cluster-label span{color:#333;}#mermaid-svg-u58SxWXMM1XnnAIo .cluster-label span p{background-color:transparent;}#mermaid-svg-u58SxWXMM1XnnAIo .label text,#mermaid-svg-u58SxWXMM1XnnAIo span{fill:#333;color:#333;}#mermaid-svg-u58SxWXMM1XnnAIo .node rect,#mermaid-svg-u58SxWXMM1XnnAIo .node circle,#mermaid-svg-u58SxWXMM1XnnAIo .node ellipse,#mermaid-svg-u58SxWXMM1XnnAIo .node polygon,#mermaid-svg-u58SxWXMM1XnnAIo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-u58SxWXMM1XnnAIo .rough-node .label text,#mermaid-svg-u58SxWXMM1XnnAIo .node .label text,#mermaid-svg-u58SxWXMM1XnnAIo .image-shape .label,#mermaid-svg-u58SxWXMM1XnnAIo .icon-shape .label{text-anchor:middle;}#mermaid-svg-u58SxWXMM1XnnAIo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-u58SxWXMM1XnnAIo .rough-node .label,#mermaid-svg-u58SxWXMM1XnnAIo .node .label,#mermaid-svg-u58SxWXMM1XnnAIo .image-shape .label,#mermaid-svg-u58SxWXMM1XnnAIo .icon-shape .label{text-align:center;}#mermaid-svg-u58SxWXMM1XnnAIo .node.clickable{cursor:pointer;}#mermaid-svg-u58SxWXMM1XnnAIo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-u58SxWXMM1XnnAIo .arrowheadPath{fill:#333333;}#mermaid-svg-u58SxWXMM1XnnAIo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-u58SxWXMM1XnnAIo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-u58SxWXMM1XnnAIo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-u58SxWXMM1XnnAIo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-u58SxWXMM1XnnAIo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-u58SxWXMM1XnnAIo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-u58SxWXMM1XnnAIo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-u58SxWXMM1XnnAIo .cluster text{fill:#333;}#mermaid-svg-u58SxWXMM1XnnAIo .cluster span{color:#333;}#mermaid-svg-u58SxWXMM1XnnAIo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-u58SxWXMM1XnnAIo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-u58SxWXMM1XnnAIo rect.text{fill:none;stroke-width:0;}#mermaid-svg-u58SxWXMM1XnnAIo .icon-shape,#mermaid-svg-u58SxWXMM1XnnAIo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-u58SxWXMM1XnnAIo .icon-shape p,#mermaid-svg-u58SxWXMM1XnnAIo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-u58SxWXMM1XnnAIo .icon-shape .label rect,#mermaid-svg-u58SxWXMM1XnnAIo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-u58SxWXMM1XnnAIo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-u58SxWXMM1XnnAIo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-u58SxWXMM1XnnAIo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 服务列表
选一个
选一个
选一个
Consumer
@DubboReference
注册中心
Nacos :8848
负载均衡策略
Provider A
:20881
Provider B
:20882
Provider C
:20883
所以验证负载均衡,最少要满足三个条件:
text
1. 同一个服务接口
2. 同一个 version(同一 group)
3. 至少两个 Provider 实例同时在线
第二条是最容易漏的。同一个接口的 1.0.0 和 2.0.0 属于两个不同的服务,消费者指定了 version 就只会在该版本内部挑实例,跨版本不会发生选择行为------那是「版本路由」,不是负载均衡。这一点下文单独展开。
五种内置策略
讲义在「Dubbo高级特性」一节列出的内置策略是四种:Random、RoundRobin、LeastActive、ConsistentHash。本工程的依赖是 dubbo-spring-boot-starter 2.7.8,该版本从 2.7.7 起另外提供了 ShortestResponse,一并放进来看会更完整。表格前四行是讲义口径,ShortestResponse 是版本补充。
| 策略 | 选择依据 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
Random(默认) |
按权重随机,权重高者概率大 | 实现简单,长期分布收敛于权重比 | 单次调用可能连续命中同一台 | 通用默认;实例性能有差异时用权重表达 |
RoundRobin |
按权重轮询,严格按配额依次下发 | 请求分布均匀、可预期 | 不看实例当前负载,慢实例会持续被打 | 各实例性能接近、耗时稳定的场景 |
LeastActive |
选当前活跃调用数最少的实例,相同活跃数时随机 | 能感知实例快慢,慢实例因活跃数堆积被少选 | 需要统计活跃数,极端情况下可能短时倾斜 | 各实例耗时差异大、有明显快慢机器 |
ConsistentHash |
一致性哈希,相同参数绑定到同一实例 | 天然会话粘性,可命中实例本地缓存 | 实例上下线会引起哈希环变动、请求重新分布 | 需要粘性、有本地缓存的场景 |
ShortestResponse |
比较各实例的平均响应时间,选最短的 | 直接以耗时为目标函数 | 依赖滑动窗口统计,突发流量下可能滞后 | 响应时间是核心指标的在线服务 |
Random:默认策略,但它不是均分
名字叫「随机」,实际是按权重随机。Dubbo 把所有 Provider 的权重求和,再按权重给每个实例切出对应的数字区间,然后以权重和为基数随机一个值,落到哪个区间就打哪台。
#mermaid-svg-ghDG6fb3K187PWAT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ghDG6fb3K187PWAT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ghDG6fb3K187PWAT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ghDG6fb3K187PWAT .error-icon{fill:#552222;}#mermaid-svg-ghDG6fb3K187PWAT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ghDG6fb3K187PWAT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ghDG6fb3K187PWAT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ghDG6fb3K187PWAT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ghDG6fb3K187PWAT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ghDG6fb3K187PWAT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ghDG6fb3K187PWAT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ghDG6fb3K187PWAT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ghDG6fb3K187PWAT .marker.cross{stroke:#333333;}#mermaid-svg-ghDG6fb3K187PWAT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ghDG6fb3K187PWAT p{margin:0;}#mermaid-svg-ghDG6fb3K187PWAT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ghDG6fb3K187PWAT .cluster-label text{fill:#333;}#mermaid-svg-ghDG6fb3K187PWAT .cluster-label span{color:#333;}#mermaid-svg-ghDG6fb3K187PWAT .cluster-label span p{background-color:transparent;}#mermaid-svg-ghDG6fb3K187PWAT .label text,#mermaid-svg-ghDG6fb3K187PWAT span{fill:#333;color:#333;}#mermaid-svg-ghDG6fb3K187PWAT .node rect,#mermaid-svg-ghDG6fb3K187PWAT .node circle,#mermaid-svg-ghDG6fb3K187PWAT .node ellipse,#mermaid-svg-ghDG6fb3K187PWAT .node polygon,#mermaid-svg-ghDG6fb3K187PWAT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ghDG6fb3K187PWAT .rough-node .label text,#mermaid-svg-ghDG6fb3K187PWAT .node .label text,#mermaid-svg-ghDG6fb3K187PWAT .image-shape .label,#mermaid-svg-ghDG6fb3K187PWAT .icon-shape .label{text-anchor:middle;}#mermaid-svg-ghDG6fb3K187PWAT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ghDG6fb3K187PWAT .rough-node .label,#mermaid-svg-ghDG6fb3K187PWAT .node .label,#mermaid-svg-ghDG6fb3K187PWAT .image-shape .label,#mermaid-svg-ghDG6fb3K187PWAT .icon-shape .label{text-align:center;}#mermaid-svg-ghDG6fb3K187PWAT .node.clickable{cursor:pointer;}#mermaid-svg-ghDG6fb3K187PWAT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ghDG6fb3K187PWAT .arrowheadPath{fill:#333333;}#mermaid-svg-ghDG6fb3K187PWAT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ghDG6fb3K187PWAT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ghDG6fb3K187PWAT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ghDG6fb3K187PWAT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ghDG6fb3K187PWAT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ghDG6fb3K187PWAT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ghDG6fb3K187PWAT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ghDG6fb3K187PWAT .cluster text{fill:#333;}#mermaid-svg-ghDG6fb3K187PWAT .cluster span{color:#333;}#mermaid-svg-ghDG6fb3K187PWAT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ghDG6fb3K187PWAT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ghDG6fb3K187PWAT rect.text{fill:none;stroke-width:0;}#mermaid-svg-ghDG6fb3K187PWAT .icon-shape,#mermaid-svg-ghDG6fb3K187PWAT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ghDG6fb3K187PWAT .icon-shape p,#mermaid-svg-ghDG6fb3K187PWAT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ghDG6fb3K187PWAT .icon-shape .label rect,#mermaid-svg-ghDG6fb3K187PWAT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ghDG6fb3K187PWAT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ghDG6fb3K187PWAT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ghDG6fb3K187PWAT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} r ∈ [0, 2)
r ∈ [2, 5)
r ∈ [5, 10)
随机数 r ∈ [0, 10)
r 落在哪个区间
实例A
weight = 2
实例B
weight = 3
实例C
weight = 5
权重 2 / 3 / 5 的结果不是「每台各占三分之一」,而是大约 20% / 30% / 50%。权重越高,被随机到的概率越大。这也是为什么新上线的低配机器可以通过调低权重先接少量流量,而不是一刀切下线。
默认权重是 100,不显式配置时各实例等权,此时才退化成我们直觉里的「均匀随机」。
RoundRobin:按权重轮询,注意慢实例积压
轮询是「依次循环调用」,加权重之后变成按配额循环:权重 2 / 3 / 5 时,前 10 次调用会分配成 A 两次、B 三次、C 五次,之后循环。
它的问题在于只看配额,不看实例当下忙不忙。如果实例 C 因为 GC 或者慢 SQL 每次耗时 500ms,而 A 只要 20ms,轮询仍然会给 C 下发和权重成正比的请求,C 的请求队列越积越长,最终拖垮超时。碰到这种机器快慢明显不均的情况,轮询不是好选择。
LeastActive:用活跃数感知快慢
LeastActive 选的是「当前正在处理中的调用数」最少的实例。同一个时间窗口里,实例 C 有 3 个在途请求、实例 A 只有 1 个,那么新请求优先给 A。
它的巧妙之处在于活跃数是一个能间接反映快慢的指标:处理得慢的实例,请求堆在它身上来不及释放,活跃数自然居高不下,于是被少选。等它的活跃数降下来,又会重新参与分配------这是一个自动收敛的负反馈。
ConsistentHash:相同参数打到同一实例
一致性哈希把「参数」映射到哈希环上的位置,同一个参数值每次都落在同一台实例上。典型用途是本地缓存:实例 A 缓存了用户 1 的数据,只要用户 1 的请求始终进 A,缓存就一直有效;如果换成随机或轮询,请求散落到所有实例,每台都要各自加载一遍,缓存等于白做。
代价是实例上下线会引起重分布。哈希环上增删一个节点,落在受影响区间的那些参数会改投别的实例,缓存随之失效。所以它适合实例数量稳定的场景,不适合频繁扩缩容。
ShortestResponse:以平均响应时间为准
ShortestResponse 统计各实例在一段时间窗口内的平均响应时间,选最短的那台。它比 LeastActive 更直接地把「快慢」作为目标,但依赖统计窗口,流量突变或实例刚启动(样本不足)时判断会滞后。
策略怎么配:三种位置,层级不同
代码里能改的只有一行属性,位置不同含义不同。
消费者侧(本工程用的就是这种) 。user-consumer 的 UserController 里,引用声明上直接带策略:
java
package cn.mf.user.controller;
import cn.mf.user.domain.User;
import cn.mf.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
// 引用远程服务:loadbalance 指定消费者选实例时使用的策略
@DubboReference(version = "2.0.0", retries = 0, loadbalance = "Random")
private UserService userService;
@GetMapping("/usernames/{id}")
public String queryUsername(@PathVariable("id") Long id) {
return userService.queryUsername(id);
}
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
return userService.queryById(id);
}
}
提供者侧。在暴露服务时声明,作为「本服务建议使用的策略」注册到 URL 里:
java
package cn.mf.user.service.impl;
import cn.mf.user.domain.User;
import cn.mf.user.mapper.UserMapper;
import cn.mf.user.service.UserService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
// version = 2.0.0,weight = 200 表示该实例接双倍流量
@DubboService(version = "2.0.0", weight = 200)
public class UserServiceImpl2 implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public String queryUsername(Long id) {
return userMapper.findById(id).getUsername();
}
@Override
public User queryById(Long id) {
User user = userMapper.findById(id);
user.setUsername(user.getUsername() + "v2.0");
return user;
}
}
全局级。写在 yaml 里,对该应用下所有服务生效:
yaml
dubbo:
registry:
address: nacos://127.0.0.1:8848
consumer:
loadbalance: RoundRobin # 给本应用的所有引用兜底
check: false
retries: 0
provider:
weight: 100 # 提供者侧默认权重
Dubbo 的配置优先级是「更具体者优先」:方法级 > 接口/服务级 > 全局级 ;消费端与提供端都配了时,消费端优先(怎么挑实例是调用方的决策,供给方只提供建议值)。各版本对细节的排序处理可能有微调,实际项目里以官方配置文档为准;方向是明确的------越靠近具体调用点的配置越说了算。
也正因为这个方向,loadbalance 通常写在消费者侧。提供者侧写它只是「建议」,一旦某个消费者显式指定了自己的策略,提供者的建议就被覆盖。
权重:和 Nacos 的权重不是一回事
Random 和 RoundRobin 都吃权重,所以权重得单独说清楚。
配置权重有三个入口。跟随服务实现的注解最直观,注解值本身默认是 100:
java
package cn.mf.user.service.impl;
import cn.mf.user.domain.User;
import cn.mf.user.mapper.UserMapper;
import cn.mf.user.service.UserService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
// weight = 200:该实例的权重是默认值的两倍,随机策略下大约接走两倍流量
@DubboService(version = "2.0.0", weight = 200)
public class UserServiceImpl2 implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public String queryUsername(Long id) {
return userMapper.findById(id).getUsername();
}
@Override
public User queryById(Long id) {
User user = userMapper.findById(id);
user.setUsername(user.getUsername() + "v2.0");
return user;
}
}
提供者侧也可以给应用级默认值:
yaml
dubbo:
provider:
weight: 100 # 本应用所有服务的默认权重,单个服务可用注解覆盖
第三个入口是 dubbo-admin 控制台,在线拖动权重,不重启即生效,适合临时压测和灰度调流。
真实用途有两个。日常是让配置更好的机器多接流量 :8 核的机器给 200,4 核的给 100,随机分配下前者拿到的请求量就是后者的两倍。灰度时则是把流量往新版本牵引:同版本的多个实例里,新扩容的那批先给低权重试水,观察监控无异常再逐步调高。
这里有个高频混淆点要澄清:
Dubbo 的
weight和注册中心的权重是两套独立的机制。
Dubbo 的 weight 是服务级参数,由提供者在注册时写进服务 URL(形如 dubbo://ip:port/...&weight=200),只被 Dubbo 自己的负载均衡器读取,粒度到「服务/方法」。
Nacos 的实例权重是注册中心层面的字段,粒度到「实例」,被 Nacos 的消费者读取。在 SpringCloud 那一侧,NacosRule 用它做权重筛选,SpringCloud LoadBalancer 也读它。
这意味着,如果项目里同时用 Dubbo 和 Feign,你在 dubbo-admin 里调的权重对 Feign 调用毫无作用;反过来在 Nacos 控制台拖动实例权重,Dubbo 的 Random 策略也不会理你。两套都要配,也都要分别验证。Dubbo 服务的 weight 是随服务 URL 一起注册的元数据参数,而 Nacos 里那个实例权重是实例自身的字段,名字相同、载体不同------看到两个「权重」入口时先分清是哪一层,再决定改哪个。
实战:两个 Provider 实例,观察调用分布
工程结构如下,需要重点关注的只有三个模块:
tree
dubbo-demo
├── pom.xml # 父工程,SpringBoot 2.3.9 + Dubbo 2.7.8
├── user-api # 接口 + 领域对象,Provider 和 Consumer 共同依赖
│ └── src/main/java/cn/mf/user
│ ├── service/UserService.java
│ └── domain/User.java
├── user-provider # 提供者,server.port=18080,dubbo.protocol.port=20881
│ └── src/main/java/cn/mf/user
│ ├── UserProviderApplication.java
│ ├── mapper/UserMapper.java
│ └── service/impl
│ ├── UserServiceImpl.java # @DubboService(version = "1.0.0")
│ └── UserServiceImpl2.java # @DubboService(version = "2.0.0")
└── user-consumer # 消费者,server.port=18081
└── src/main/java/cn/mf/user
├── UserConsumerApplication.java
└── controller/UserController.java
UserServiceImpl 的 queryById 会在用户名后拼 v1.0,UserServiceImpl2 拼 v2.0------这个标记就是观察「请求打到了哪个实例」的探针,非常便于验证。
java
package cn.mf.user.service.impl;
import cn.mf.user.domain.User;
import cn.mf.user.mapper.UserMapper;
import cn.mf.user.service.UserService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
// 1.0.0 版本:返回值里拼上 v1.0
@DubboService(version = "1.0.0")
class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public String queryUsername(Long id) {
return userMapper.findById(id).getUsername();
}
@Override
public User queryById(Long id) {
User user = userMapper.findById(id);
user.setUsername(user.getUsername() + "v1.0");
return user;
}
}
java
package cn.mf.user.service.impl;
import cn.mf.user.domain.User;
import cn.mf.user.mapper.UserMapper;
import cn.mf.user.service.UserService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
// 2.0.0 版本:返回值里拼上 v2.0
@DubboService(version = "2.0.0")
class UserServiceImpl2 implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public String queryUsername(Long id) {
return userMapper.findById(id).getUsername();
}
@Override
public User queryById(Long id) {
User user = userMapper.findById(id);
user.setUsername(user.getUsername() + "v2.0");
return user;
}
}
准备一张演示表:
sql
CREATE TABLE tb_user
(
id BIGINT PRIMARY KEY,
username VARCHAR(50),
address VARCHAR(100)
);
INSERT INTO tb_user (id, username, address) VALUES (1, '项目', '北京');
先把 version 与负载均衡的区别讲清
工程里 Consumer 写的是 @DubboReference(version = "2.0.0", loadbalance = "Random")。这句话有两层约束同时生效:
#mermaid-svg-bHB0qkyWD4UwA6xW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bHB0qkyWD4UwA6xW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bHB0qkyWD4UwA6xW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bHB0qkyWD4UwA6xW .error-icon{fill:#552222;}#mermaid-svg-bHB0qkyWD4UwA6xW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bHB0qkyWD4UwA6xW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bHB0qkyWD4UwA6xW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bHB0qkyWD4UwA6xW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bHB0qkyWD4UwA6xW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bHB0qkyWD4UwA6xW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bHB0qkyWD4UwA6xW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bHB0qkyWD4UwA6xW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bHB0qkyWD4UwA6xW .marker.cross{stroke:#333333;}#mermaid-svg-bHB0qkyWD4UwA6xW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bHB0qkyWD4UwA6xW p{margin:0;}#mermaid-svg-bHB0qkyWD4UwA6xW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bHB0qkyWD4UwA6xW .cluster-label text{fill:#333;}#mermaid-svg-bHB0qkyWD4UwA6xW .cluster-label span{color:#333;}#mermaid-svg-bHB0qkyWD4UwA6xW .cluster-label span p{background-color:transparent;}#mermaid-svg-bHB0qkyWD4UwA6xW .label text,#mermaid-svg-bHB0qkyWD4UwA6xW span{fill:#333;color:#333;}#mermaid-svg-bHB0qkyWD4UwA6xW .node rect,#mermaid-svg-bHB0qkyWD4UwA6xW .node circle,#mermaid-svg-bHB0qkyWD4UwA6xW .node ellipse,#mermaid-svg-bHB0qkyWD4UwA6xW .node polygon,#mermaid-svg-bHB0qkyWD4UwA6xW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bHB0qkyWD4UwA6xW .rough-node .label text,#mermaid-svg-bHB0qkyWD4UwA6xW .node .label text,#mermaid-svg-bHB0qkyWD4UwA6xW .image-shape .label,#mermaid-svg-bHB0qkyWD4UwA6xW .icon-shape .label{text-anchor:middle;}#mermaid-svg-bHB0qkyWD4UwA6xW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bHB0qkyWD4UwA6xW .rough-node .label,#mermaid-svg-bHB0qkyWD4UwA6xW .node .label,#mermaid-svg-bHB0qkyWD4UwA6xW .image-shape .label,#mermaid-svg-bHB0qkyWD4UwA6xW .icon-shape .label{text-align:center;}#mermaid-svg-bHB0qkyWD4UwA6xW .node.clickable{cursor:pointer;}#mermaid-svg-bHB0qkyWD4UwA6xW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bHB0qkyWD4UwA6xW .arrowheadPath{fill:#333333;}#mermaid-svg-bHB0qkyWD4UwA6xW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bHB0qkyWD4UwA6xW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bHB0qkyWD4UwA6xW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bHB0qkyWD4UwA6xW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bHB0qkyWD4UwA6xW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bHB0qkyWD4UwA6xW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bHB0qkyWD4UwA6xW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bHB0qkyWD4UwA6xW .cluster text{fill:#333;}#mermaid-svg-bHB0qkyWD4UwA6xW .cluster span{color:#333;}#mermaid-svg-bHB0qkyWD4UwA6xW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bHB0qkyWD4UwA6xW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bHB0qkyWD4UwA6xW rect.text{fill:none;stroke-width:0;}#mermaid-svg-bHB0qkyWD4UwA6xW .icon-shape,#mermaid-svg-bHB0qkyWD4UwA6xW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bHB0qkyWD4UwA6xW .icon-shape p,#mermaid-svg-bHB0qkyWD4UwA6xW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bHB0qkyWD4UwA6xW .icon-shape .label rect,#mermaid-svg-bHB0qkyWD4UwA6xW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bHB0qkyWD4UwA6xW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bHB0qkyWD4UwA6xW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bHB0qkyWD4UwA6xW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① version 过滤
Consumer 固定 2.0.0
② loadbalance 选择
全部 Provider
1.0.0 与 2.0.0 各有实例
候选实例集
只剩 2.0.0 的实例
最终命中的那一台
version 决定「哪些实例有资格」,负载均衡决定「有资格的里面挑哪一个」。 顺序不能颠倒:版本不匹配的实例压根没进入候选列表,轮不到负载均衡去选它。
工程里 1.0.0 和 2.0.0 各只有一个实现类、跑在同一个 Provider 进程里。也就是说,Consumer 固定 2.0.0 之后,候选列表里只有一个实例,loadbalance = "Random" 此刻不会产生任何可见效果。这不是配置写错了,而是负载均衡的输入不够------只有一台机器,随机也好轮询也好,结果都一样。
同版本启动两个实例的正确做法
要看到效果,得让同一个 version 的两个实例同时在线。
做法是在 UserServiceImpl2 的返回值里带上自己的端口,用启动参数区分实例。先把标记改成实例级(dubbo.protocol.port 同时会被 Dubbo 读取并绑定端口):
java
package cn.mf.user.service.impl;
import cn.mf.user.domain.User;
import cn.mf.user.mapper.UserMapper;
import cn.mf.user.service.UserService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
// 演示用:返回值带上本进程的 Dubbo 端口,便于肉眼分辨请求落到了哪个实例
@DubboService(version = "2.0.0")
class UserServiceImpl2 implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public String queryUsername(Long id) {
return userMapper.findById(id).getUsername();
}
@Override
public User queryById(Long id) {
User user = userMapper.findById(id);
String port = System.getProperty("dubbo.protocol.port", "20881");
user.setUsername(user.getUsername() + "-port" + port);
return user;
}
}
然后打两个包,用不同的端口各起一个。这里统一用 -D 传参,Dubbo 与 Spring Boot 都能从系统属性里读到:
bash
# 打包(跳过测试)
mvn -q -pl user-provider -am clean package -DskipTests
# 实例一:server 18080 / dubbo 协议 20881(与 yml 默认一致,直接起即可)
java -jar user-provider/target/user-provider-1.0-SNAPSHOT.jar
# 实例二:server 18082 / dubbo 协议 20882
java -Dserver.port=18082 -Ddubbo.protocol.port=20882 \
-jar user-provider/target/user-provider-1.0-SNAPSHOT.jar
两个进程都声明 version = "2.0.0",注册中心里 UserService:2.0.0 就有了两个提供者。启动 Consumer:
bash
java -jar user-consumer/target/user-consumer-1.0-SNAPSHOT.jar
# 默认端口 18081
连续调用,观察分布
Consumer 提供了 GET /user/{id},返回体里的 username 带端口标记,循环调用即可看到分布。
bash
# 连续调用 12 次,观察 username 里的 -port 后缀
for i in $(seq 1 12); do
curl -s "http://127.0.0.1:18081/user/1"
echo
done
预期观察:
loadbalance = "Random"时,12 次调用里两个-port后缀都会出现,且顺序是乱的,不会严格交替。次数比大致接近权重比(默认等权时约各占一半,但样本小会波动)。- 把 Consumer 改成
loadbalance = "RoundRobin"重启后,同样的循环会看到严格交替(A、B、A、B......)------这是轮询「按顺序逐个下发」的直接体现,也是区分两种策略最快的方法。
对应的注解改动只有一处:
java
// 验证轮询:改完必须重启 Consumer 才生效
@DubboReference(version = "2.0.0", retries = 0, loadbalance = "RoundRobin")
private UserService userService;
如果不想用 curl,也可以在 user-consumer 的测试目录下写一段把调用和统计放在一起的测试(user-consumer 需要先引入 spring-boot-starter-test):
java
package cn.mf.user;
import cn.mf.user.domain.User;
import cn.mf.user.service.UserService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@SpringBootTest
class LoadBalanceTest {
@DubboReference(version = "2.0.0", retries = 0, loadbalance = "Random")
private UserService userService;
@Test
void observeDistribution() {
Map<String, Integer> counter = new ConcurrentHashMap<>();
for (int i = 0; i < 100; i++) {
User user = userService.queryById(1L);
// username 形如 项目-port20881,取端口作为实例标识
String instance = user.getUsername().substring(user.getUsername().lastIndexOf("-port"));
counter.merge(instance, 1, Integer::sum);
}
counter.forEach((k, v) -> System.out.println(k + " => " + v));
}
}
100 次之后打印出的计数就是各实例分到的请求数。把 loadbalance 换成 RoundRobin,计数会变成几乎完全相等;换成 LeastActive,如果其中一个实例被人工注入延迟,它的计数会明显低于另一个。
和 SpringCloud Ribbon / LoadBalancer 的关系
两者都属于客户端负载均衡------决策发生在调用方进程内,而不是中间加一个独立的负载均衡器。区别在作用的层次:
| 维度 | Dubbo 负载均衡 | SpringCloud Ribbon / LoadBalancer |
|---|---|---|
| 作用层次 | RPC 调用层(Dubbo 协议) | HTTP 客户端层(RestTemplate / Feign) |
| 策略配置位置 | @DubboReference(loadbalance=...)、dubbo.consumer.loadbalance |
ribbon.NFLoadBalancerRuleClassName、LoadBalancerClient 自定义 |
| 内置策略 | Random / RoundRobin / LeastActive / ConsistentHash / ShortestResponse | RoundRobin / Random / 重试等,另有 NacosRule 做权重筛选 |
| 权重来源 | Dubbo 服务 URL 上的 weight |
注册中心实例权重(Nacos weight) |
真实工程里如果同时用 Dubbo 和 Feign,就会同时存在两套负载均衡配置,各管各的调用链路,需要分别维护、分别验证。改动其中一套不会影响另一套,这一条比「哪个更好用」更值得记住。
实战坑清单
| 现象 | 根因 | 处理方式 |
|---|---|---|
| 配了策略但调用总是同一台 | 候选实例只有一个,负载均衡没有可选项 | 同版本起两个实例再验证 |
| 以为换了 version 就实现了负载均衡 | version 是路由维度,不是负载均衡 |
明确「先版本过滤、后挑实例」的顺序 |
| 权重只在一侧配,看不到效果 | 消费端策略与提供端权重未同时生效 | 权重配在提供者,策略配在消费者 |
ConsistentHash 后扩缩容,缓存大面积失效 |
哈希环节点变动导致参数重新映射 | 粘性场景避免频繁上下线,或改用哈希槽方案 |
慢实例在 RoundRobin 下持续被打 |
轮询不考虑实例实时负载 | 换 LeastActive 或 ShortestResponse,或调低其权重 |
| 调了 dubbo-admin 的权重,Feign 调用没变化 | 混淆了 Dubbo 权重与注册中心权重 | 两套权重分别调整、分别验证 |
改完 loadbalance 没生效 |
注解在启动时解析,运行中不重载 | 重启 Consumer |
| 循环调用「看起来」不均 | 随机策略单次不可预测,样本太小 | 放大调用次数(如 100 次)再看分布 |
API 速览
| 位置 | 属性 | 取值示例 | 作用 |
|---|---|---|---|
@DubboReference |
loadbalance |
"Random" |
消费者侧指定选实例策略(常用位置) |
@DubboReference |
version |
"2.0.0" |
先做版本过滤,决定候选实例集合 |
@DubboReference |
retries |
0 |
关掉重试,避免验证时重复调用干扰分布观察 |
@DubboService |
loadbalance |
"RoundRobin" |
提供者侧给出的策略建议值,可被消费者覆盖 |
@DubboService |
weight |
200 |
实例权重,影响 Random / RoundRobin 的分配比例 |
@DubboService |
version |
"2.0.0" |
声明服务版本,同版本多实例才是负载均衡的候选 |
| yml | dubbo.consumer.loadbalance |
RoundRobin |
消费者应用级默认策略 |
| yml | dubbo.provider.weight |
100 |
提供者应用级默认权重 |
| yml | dubbo.protocol.port |
20881 |
Dubbo 协议端口,多实例演示时每个进程一个 |
| yml | dubbo.registry.address |
nacos://127.0.0.1:8848 |
注册中心地址 |
官方文档
- Dubbo 负载均衡
- Dubbo 配置参考(consumer / provider 属性)
- Dubbo 源码仓库(
dubbo-cluster模块下的loadbalance包) - @DubboService / @DubboReference 注解参考
总结
Dubbo 的负载均衡不是「配了就生效」的功能,它是一个纯粹的选择函数,只在候选实例不止一个时才有输出。理解它抓住三条线就够了:
- 候选集合由 version 决定,选择结果由 loadbalance 决定。 版本不匹配的实例进不了候选,也就谈不上被均衡。
Random是带权重的默认值,权重是表达「实例能力强弱」和「灰度流量比例」的手段。 它和 Nacos 实例权重是并行的两套,别跨层去找效果。- 策略没有绝对优劣,只有匹配与否。 机器齐整用
RoundRobin,快慢不均用LeastActive,要本地缓存粘性用ConsistentHash,其余场景交给默认的Random就行。
验证方法也简单:同版本起两个实例,让返回值带上实例标识,循环调用看分布,换策略再看一次分布变了没有。比盯着配置文件猜要可靠得多。
下一篇按讲义顺序进入「SpringCloud 整合 Dubbo」,讲怎么在享受 SpringCloud 生态的同时用 Dubbo 的 RPC 性能把 Ribbon / Feign 替换掉。