Dubbo 集群容错
📖 前置阅读:本文假设读者已掌握 SpringBoot Dubbo 的基本 RPC 操作(
@DubboService/@DubboReference)。如果还不熟悉,建议先阅读 SpringBoot Dubbo 全操作指南。
一、⚡ 问题切入:RPC 调用失败了怎么办?
上一篇文章的 @DubboReference 默认配置了三件事:
- Provider 有多个实例时——随机选一个(负载均衡)
- 调用失败时——自动重试 2 次(集群容错)
- 调用超过 1 秒——抛超时异常
默认策略覆盖了 80% 的场景,但剩下的 20% 需要精确控制——这就是 Dubbo 服务治理的核心:集群容错(怎么处理失败)、负载均衡(怎么选择节点)、调用模式(同步还是异步)。
二、集群容错 —— 失败了怎么办
2.1 六种集群容错策略
Dubbo 的 Cluster 接口定义了容错行为。Consumer 侧配置:
| 策略 | 配置值 | 行为 | 适用场景 |
|---|---|---|---|
| Failover(默认) | failover | 失败后自动切换其他 Provider 重试,默认重试 2 次(共 3 次调用) | 幂等的读操作 |
| Failfast | failfast | 失败后立即报错——不重试 | 非幂等写操作(创建订单、扣库存) |
| Failsafe | failsafe | 失败后吞掉异常——返回 null / 忽略错误 | 非关键操作(日志、通知) |
| Failback | failback | 失败后记录到后台线程——定时重试 | 最终一致性场景(数据同步) |
| Forking | forking | 同时调用所有 Provider——取第一个成功返回的 | 高可用低延迟,但浪费资源 |
| Broadcast | broadcast | 逐个调用所有 Provider——任何一个失败就报错 | 通知所有实例(缓存刷新、配置更新) |
// 消费端配置集群容错策略
@DubboReference(
cluster = "failfast", // 写操作——不重试
retries = 0 // 即使 Failover 模式,也可以设 retries=0 关掉重试
)
private OrderService orderService;
2.2 Failover 原理与幂等陷阱
Failover 是默认策略——它的逻辑是:
Consumer 调用 Provider-A → 超时/异常
↓
Consumer 自动换 Provider-B → 成功 → 返回结果
↓
Provider-B 处理了这次请求
但 Provider-A 可能也处理了——只是返回超时了!
这就是 Dubbo 的重复调用陷阱:
// 需求:创建订单——只能成功一次
@DubboReference(
cluster = "failover", // 默认——危险!
retries = 2 // 默认——更危险!
)
private OrderService orderService;
// 调用 createOrder(10001, "iPhone", 6999)
// 场景:Provider-A 处理成功但 Consumer 超时
// → Consumer 认为失败 → 重试到 Provider-B
// → Provider-B 又创建了一次 → 同一个订单创建了两条!
正确配置:
// 非幂等写操作——必须关掉重试
@DubboReference(
cluster = "failfast", // 失败立即报错——不重试
retries = 0
)
private OrderService orderService;
flowchart TD
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px;,color:#fecaca;
CALL([Consumer 发起 RPC]) --> Q{操作是否幂等?}
Q -- "读取/查询" --> FAILOVER[Failover\n失败重试 2 次\n换 Provider 重试]
Q -- "创建/扣减" --> FAILFAST[Failfast\n失败立即报错\nretries=0]
Q -- "日志/通知" --> FAILSAFE[Failsafe\n失败吞异常\n返回 null 或忽略]
Q -- "最终一致性" --> FAILBACK[Failback\n失败记日志\n后台定时补发]
Q -- "全实例通知" --> BROADCAST[Broadcast\n逐个调所有实例\n全成功才返回]
class CALL startEnd;
class Q condition;
class FAILOVER,FAILFAST,FAILSAFE,FAILBACK,BROADCAST highlight;
2.3 Failback 的适用场景
// 数据同步——必须最终一致但不要求实时
// 场景:订单创建后异步同步到数据仓库
@DubboReference(
cluster = "failback",
retries = 5, // 后台重试 5 次
timeout = 1000 // 每次调用 1s 超时
)
private DataWarehouseSyncService syncService;
Failback 的行为:
调用失败 → 记录到后台线程的失败列表 → 立即返回(不阻塞主线程)
后台线程 → 每隔一段时间重试 → 5 次后仍然失败 → 丢弃
三、负载均衡 —— 多 Provider 怎么选
3.1 七种负载均衡策略
| 策略 | 配置值 | 算法 | 适用场景 |
|---|---|---|---|
| Random(默认) | random | 随机 + 权重——响应快的 Provider 权重高 | 通用场景 |
| RoundRobin | roundrobin | 轮询 + 权重——按顺序分配 | 各 Provider 性能相同 |
| LeastActive | leastactive | 选当前活跃调用数最少的 Provider | Provider 处理耗时差异大 |
| ShortestResponse | shortestresponse | 选响应时间最短 + 活跃数最少的 Provider | 综合考虑延迟和负载 |
| ConsistentHash | consistenthash | 一致性哈希——同一个参数值永远到同一个 Provider | 需要请求粘性的场景 |
| P2C(3.2+) | p2c | Power of Two Choice——随机选两个,挑负载低的 | 大规模集群的近似最优 |
| Adaptive(3.2+) | adaptive | 自适应——根据实时性能数据动态选 Provider | 自动优化场景 |
# yml 中全局配置
dubbo:
consumer:
loadbalance: leastactive # 全局默认
// 注解中局部覆盖
@DubboReference(loadbalance = "consistenthash")
private OrderService orderService;
3.2 权重的含义
每个策略都支持权重——权重越高,分配到的流量越多。权重可以在 Provider 端配置:
# Provider 端——这台机器配置高,权重设大
dubbo:
provider:
weight: 200 # 默认 100——这台机器分 2 倍流量
三个 Provider 实例,权重分别为 100, 200, 100:
Random 策略(按权重随机):
Provider-1 (权重 100) → 25% 请求
Provider-2 (权重 200) → 50% 请求
Provider-3 (权重 100) → 25% 请求
3.3 一致性哈希 —— 需要"请求粘性"时
// 场景:订单服务缓存了部分数据——同一个 orderId 永远路由到同一个 Provider
@DubboReference(
loadbalance = "consistenthash",
parameters = {"hash.arguments", "long[]"} // 按 orderId 参数哈希
)
private OrderService orderService;
一致性哈希在新增 Provider 时的行为:
正常状态(三个 Provider 节点):
orderId=10001 → hash → Provider-1
orderId=10002 → hash → Provider-2
orderId=10003 → hash → Provider-3
新增 Provider-4:
orderId=10001 → hash → Provider-1 (不变)
orderId=10002 → hash → Provider-2 (不变)
orderId=10003 → hash → Provider-4 (变了!)
只有 1/3 的请求换了 Provider——不是全量重新分配
3.4 P2C(Power of Two Choice)
Dubbo 3.2+ 的新策略——比 Random 更智能,比 LeastActive 开销更低:
P2C 算法(每次请求):
1. 从所有 Provider 中随机选 2 个
2. 比较这 2 个的活跃调用数
3. 选择活跃调用数少的那个
为什么高效:不需要维护全局的"最少活跃"状态——只要随机 2 个比一下
@DubboReference(loadbalance = "p2c")
private OrderService orderService; // Dubbo 3.2+
四、服务分组与版本隔离
4.1 服务分组 —— 同一接口不同业务场景
// Provider——南方机房
@DubboService(group = "south")
public class OrderServiceImpl implements OrderService { ... }
// Provider——北方机房
@DubboService(group = "north")
public class OrderServiceImpl implements OrderService { ... }
// Consumer——南方用户
@DubboReference(group = "south")
private OrderService orderService;
分组可以用于:
- 同机房调用:
group = "cn-south"/group = "cn-north" - 流量隔离:
group = "normal"/group = "vip" - 灰度发布:
group = "stable"/group = "canary"
4.2 服务版本 —— 灰度发布和向后兼容
// Provider V1——老版本(稳定)
@DubboService(version = "1.0.0")
public class OrderServiceV1Impl implements OrderService {
// 老方法:只返回订单基本信息
public Order getOrderById(Long orderId) { ... }
}
// Provider V2——新版本(灰度)
@DubboService(version = "2.0.0")
public class OrderServiceV2Impl implements OrderService {
// 新方法:返回订单 + 关联物流信息
public Order getOrderById(Long orderId) {
Order order = ...;
order.setLogistics(logisticsService.getByOrderId(orderId)); // 新功能
return order;
}
}
// Consumer——灰度用户(逐渐从 V1 切到 V2)
@DubboReference(version = "2.0.0") // 切到 V2
private OrderService orderService;
flowchart LR
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold;
classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;
CONS1([Consumer\n90% 流量\nversion=1.0.0]) -->|"call"| V1["Provider V1\n稳定版本"]
CONS2([Consumer\n10% 灰度\nversion=2.0.0]) -->|"call"| V2["Provider V2\n新版本"]
V2 -.->|"观察无问题后\nConsumer 全量切 V2\nProvider V1 下线"| V1
class CONS1,CONS2 startEnd;
class V1,V2 data;
五、异步调用 —— 不阻塞当前线程的 RPC
5.1 三种异步调用方式
| 方式 | 配置 | 适用场景 |
|---|---|---|
| CompletableFuture 直接返回 | 接口方法返回 CompletableFuture<T> | 需要组合多个 RPC 调用 |
| async=true | @DubboReference(async=true) + RpcContext.getCompletableFuture() | 老代码改造——接口不改 |
| 回调 | @DubboReference(async=true) + RpcContext.getCompletableFuture().whenComplete() | 需要通知的异步处理 |
5.2 方式一:CompletableFuture(推荐)
// 接口定义——返回 CompletableFuture
public interface OrderService {
CompletableFuture<Order> getOrderById(Long orderId);
}
// Provider 实现——和同步方法一样写
@DubboService
public class OrderServiceImpl implements OrderService {
@Override
public CompletableFuture<Order> getOrderById(Long orderId) {
// Dubbo 自动把返回值包装成 CompletableFuture
return CompletableFuture.completedFuture(
orderDB.get(orderId));
}
}
// Consumer 调用——组合多个 RPC 调用
@RestController
public class OrderController {
@DubboReference
private OrderService orderService; // 异步接口
@DubboReference
private LogisticsService logisticsService; // 另一个异步接口
@GetMapping("/order-detail/{orderId}")
public CompletableFuture<OrderDetail> getOrderDetail(@PathVariable Long orderId) {
// 同时发起两个 RPC 调用——不阻塞
CompletableFuture<Order> orderFuture = orderService.getOrderById(orderId);
CompletableFuture<Logistics> logisticsFuture = logisticsService.getByOrderId(orderId);
// 组合两个结果——等两个都返回后才处理
return orderFuture.thenCombine(logisticsFuture, (order, logistics) -> {
OrderDetail detail = new OrderDetail();
detail.setOrder(order);
detail.setLogistics(logistics);
return detail;
});
}
}
这个写法的威力——两个 RPC 调用同时发出,总耗时 = max(查询订单耗时, 查询物流耗时),而不是传统同步写法中的顺序等待(耗时 = 订单耗时 + 物流耗时)。
5.3 方式二:async=true 不改接口
// 接口定义——仍然是同步签名
public interface OrderService {
Order getOrderById(Long orderId); // 注意:返回类型是 Order,不是 CompletableFuture
}
// Consumer——不改接口,通过 async=true 异步调用
@DubboReference(async = true)
private OrderService orderService;
public void processOrder() {
// 发起调用——此时不阻塞
Order order = orderService.getOrderById(10001L);
// 注意:此时 order 为 null!因为 async=true,调用立即返回
// 获取真正的结果——阻塞等待
CompletableFuture<Order> future =
RpcContext.getServiceContext().getCompletableFuture();
Order realOrder = future.join(); // 阻塞直到拿到结果
}
⚠️ 新手提示:
async=true模式中,方法的返回值不是真实的执行结果——调用后立即返回 null 或空对象。必须通过RpcContext.getCompletableFuture()拿 Future 再获取结果。这个设计很反直觉——除非是改造遗留代码,否则直接用接口返回CompletableFuture更安全。
六、参数校验 —— 在 Provider 端拦截非法请求
6.1 内置验证过滤器
Dubbo 支持 JSR 303 Bean Validation——在 Provider 端自动校验参数:
// API 模块——在接口上声明约束
public interface OrderService {
Order getOrderById(
@NotNull(message = "orderId 不能为空")
@Min(value = 1, message = "orderId 必须大于 0")
Long orderId
);
Order createOrder(
@NotNull(message = "userId 不能为空") Long userId,
@NotBlank(message = "商品名不能为空") String productName,
@NotNull(message = "金额不能为空")
@DecimalMin(value = "0.01", message = "金额必须大于 0")
BigDecimal amount
);
}
// Provider 实现——不用改任何代码
@DubboService(validation = "true") // ← 开启参数校验
public class OrderServiceImpl implements OrderService {
// 实现不变——Dubbo 在方法执行前自动校验参数
}
// 子模块需要加依赖
// <dependency>
// <groupId>jakarta.validation</groupId>
// <artifactId>jakarta.validation-api</artifactId>
// </dependency>
// <dependency>
// <groupId>org.hibernate.validator</groupId>
// <artifactId>hibernate-validator</artifactId>
// </dependency>
当 Consumer 传入非法参数时,Provider 端的调用链路变为:
Consumer 发请求 → Dubbo Provider 收到 → 校验参数 → 不通过 → 抛 ValidationException
→ Consumer 收到异常(Failfast / Failover)
七、本地存根(Stub)—— Consumer 端预处理
// 场景:Consumer 在 RPC 调用前做一些本地预处理
// 如——记录日志、校验参数、降级兜底
// 本地存根——实现了和远程服务相同的接口
public class OrderServiceStub implements OrderService {
private final OrderService orderService; // Dubbo 自动注入远程代理
public OrderServiceStub(OrderService orderService) {
this.orderService = orderService; // 构造器注入——Dubbo 的要求
}
@Override
public Order getOrderById(Long orderId) {
// RPC 调用前的本地预处理
if (orderId == null || orderId <= 0) {
throw new IllegalArgumentException("orderId 不合法: " + orderId);
}
System.out.printf("[Stub] 即将远程调用: getOrderById(%d)%n", orderId);
try {
return orderService.getOrderById(orderId); // 真正的 RPC 调用
} catch (Exception e) {
// 降级兜底
System.err.println("[Stub] RPC 调用失败,返回降级数据");
return new Order(orderId, "UNKNOWN", BigDecimal.ZERO);
}
}
}
// Consumer 端——指定本地存根
@DubboReference(stub = "org.example.consumer.OrderServiceStub")
private OrderService orderService;
🎯 总结
集群容错六种策略,核心两个:
failover(默认,幂等读用)+failfast(非幂等写用——关重试)。其他四种策略只在特定场景使用——failsafe(日志通知)、failback(最终一致性)、broadcast(全实例通知)、forking(高可用)。负载均衡先 random 后 advanced:默认
random权重随机覆盖 80% 场景。3.2+ 的p2c(随机两个挑负载低的)是对 LeastActive 的性能优化版本。consistenthash用于需要请求粘性的场景。异步调用首选 CompletableFuture 接口:方法直接返回
CompletableFuture<T>——调用方可以用thenCombine、thenCompose组合多个 RPC 调用,总耗时 = max(各调用耗时)。不要用async=true模式——容易误读返回的 null 值。版本号做灰度,分组做隔离:
version用于向后兼容的升级(V1 → V2 灰度),group用于同接口不同业务场景(同机房调用、流量隔离)。
📖 下一步阅读:服务治理的核心能力搞定了。但"注册中心"到底是怎么工作的?Nacos 和 Zookeeper 选哪个?怎么用 Nacos 做配置中心?继续阅读 注册中心:Nacos 与 Zookeeper。