Dubbo 是国内 Java 体系里用得最多的 RPC 框架。它的能力很全,但默认配置偏乐观(比如重试),直接用容易出事。
一、架构与调用链路
关键点:Dubbo 是直连调用,注册中心只做服务发现,不在调用链路上。所以注册中心短暂不可用不会影响已有调用 —— 这是它比早期 Spring Cloud(Eureka + Ribbon 也类似)之外很多方案更稳的地方。
二、基本配置
dubbo:
application:
name: order-service
registry:
address: nacos://10.0.0.10:8848
# 或用 zookeeper://10.0.0.10:2181
protocol:
name: dubbo
port: 20880
serialization: hessian2 # 也支持 protobuf / kryo
threads: 200 # 业务线程池大小
provider:
timeout: 3000 # ★ 服务端超时
retries: 0 # ★ 服务端默认重试次数,建议 0
filter: -exception # 按需
consumer:
timeout: 3000
retries: 0 # ★ 关键:默认 2 次重试,非幂等接口会出事
check: false # 启动时不检查提供者是否可用(避免启动顺序死锁)
loadbalance: leastactive
服务暴露与引用:
@DubboService(version = "1.0.0", group = "prod", timeout = 3000)
public class OrderServiceImpl implements OrderService { ... }
@DubboReference(version = "1.0.0", group = "prod", check = false)
private OrderService orderService;
version 和 group 是做灰度与多版本共存的关键:新老版本同时注册,消费者按 version 引用,就能平滑升级。
三、负载均衡策略
| 策略 | 说明 | 场景 |
|---|---|---|
random(默认) |
随机,按权重 | 通用,简单 |
roundrobin |
轮询 | 各节点性能接近 |
leastactive |
最少活跃调用 | 推荐:慢的节点自动少分请求 |
consistenthash |
一致性哈希 | 需要同一参数落到同一节点(如有状态场景) |
shortestresponse |
最短响应(2.7.7+) | 响应时间差异大时 |
我一般选 leastactive,它能自动规避慢节点,比 random 稳。
四、集群容错
| 策略 | 行为 | 场景 |
|---|---|---|
failover(默认) |
失败自动切换其它节点重试 | 幂等的读操作 |
failfast |
失败立即报错 | 非幂等的写操作(下单、扣款) |
failsafe |
失败忽略 | 日志、通知这类可丢的 |
failback |
失败记录,定时重发 | 消息通知 |
forking |
并行调用多个,取第一个成功 | 实时性要求高,浪费资源 |
broadcast |
广播所有节点 | 通知所有节点刷新缓存 |
最重要的一条:写接口必须配 failfast + retries=0。Dubbo 默认 failover + 2 次重试,一个下单接口超时后自动重试 2 次,就是 3 个订单。
@DubboReference(cluster = "failfast", retries = 0, timeout = 2000)
private PayService payService;
五、超时与重试的正确配置
Dubbo 的超时有几层,优先级:方法级 > 接口级 > 全局(consumer > provider)。
@DubboService(methods = {
@Method(name = "batchCreate", timeout = 10000), // 批量方法单独放宽
@Method(name = "queryOne", timeout = 1000)
})
配置原则:
- 消费端优先于服务端:消费端更清楚自己能等多久。
- 超时值要大于服务端 P99:否则正常请求也被判超时。
- 重试只在幂等接口上开,且次数 ≤ 2。
- 超时不是越短越好:设太短会在下游抖动时大量超时,反而放大故障。一般取 P99 的 1.5–2 倍。
六、服务治理常用能力
// 泛化调用(不依赖服务方 JAR,做网关/测试平台很有用)
GenericService genericService = (GenericService) applicationContext.getBean("genericService");
Object result = genericService.$invoke("createOrder",
new String[]{"com.xxx.OrderDTO"}, new Object[]{paramMap});
// 本地存根(参数校验放消费端,减少网络往返)
@DubboReference(stub = "com.xxx.OrderServiceStub")
// 回声测试(检查服务是否可用)
// telnet 到 dubbo 端口
telnet 10.0.0.10 20880
> ls # 列出服务
> invoke com.xxx.OrderService.queryOne(1)
灰度发布:用 version + 路由规则,或者在 Dubbo Admin 里配置标签路由(给部分机器打标,指定流量过去)。
七、序列化选择
| 方式 | 性能 | 跨语言 | 建议 |
|---|---|---|---|
hessian2(默认) |
中 | 有限 | 够用 |
kryo / fst |
高 | 否 | 纯 Java 内部服务可选 |
protobuf |
高 | 是 | 跨语言/长期演进首选 |
fastjson2 |
中 | 是 | 需要可读性好时 |
注意:序列化方式服务端和消费端必须一致,否则反序列化失败。生产上统一在 protocol.serialization 里指定,不要各配各的。
八、生产实践
案例:一次超时引发的连锁故障
背景:某 B2B 电商公司的分布式微服务(Dubbo + Zookeeper),订单服务调用库存服务。
现象:大促开始后订单服务大面积超时,最后整个下单链路不可用,但库存服务本身 CPU 正常。
排查:
- 看 Dubbo 线程池监控:
ThreadPool exhausted,业务线程池(默认 200)全部占满。 jstack看线程在等什么:都在等库存服务的响应。- 库存服务的日志:有慢 SQL,RT 从 20ms 涨到 2s。
- 再看消费端配置:
retries=2(默认)—— 库存慢 → 订单超时 → 自动重试 2 次 → 请求量翻 3 倍 → 库存更慢 → 恶性循环。
根因:重试放大了故障。下游已经不堪重负,上游还在给它 3 倍流量。
处理:
- 把写接口的
retries改成 0,集群容错改failfast。 - 给 Dubbo 线程池加监控和告警(活跃线程数 > 80% 告警)。
- 修复库存慢 SQL(加索引 + 缓存热点 SKU)。
- 引入熔断(Sentinel):库存服务 RT 超过阈值时快速失败,不再重试。
- 线程池参数调优:核心 200 → 400,队列从无界改成 1000(防止内存堆积)。
结论:RPC 框架的重试是双刃剑。读接口重试能提升成功率,写接口重试会造成重复和放大。区分读写、区分幂等,是配置 RPC 的第一原则。
案例:注册中心地址写错导致服务找不到
现象:测试环境服务注册成功,消费端报 No provider available。
排查:ls 命令看不到服务 → 检查 @DubboService 的包路径是否在 @EnableDubbo(scanBasePackages=...) 扫描范围内 → 发现 Provider 的 group 是 test,Consumer 写的是 prod。
教训:group / version 不一致是「明明注册了却找不到」的第一嫌疑。排查顺序:注册中心里有没有 → group/version 是否一致 → 接口全限定名是否一致 → 序列化是否一致。
九、排查命令清单
# Dubbo 的 telnet 运维(连 Provider 的 dubbo 端口)
telnet 127.0.0.1 20880
ls # 服务列表
ls -l # 详细(含地址)
ps # 端口与连接
invoke <接口>.<方法>(参数) # 直接调用,验证逻辑
count <接口> # 调用统计
# 日志里开 access log(记录每次调用耗时)
dubbo.protocol.accesslog=true
一起聊聊