Dubbo
Java 高级 · 阿里中间件
Dubbo 是高性能 Java RPC 框架。会用 @DubboService/@DubboReference 只是入门,真正的功力在于读懂:一次 RPC 调用背后 Proxy/Cluster/LoadBalance/Filter/Invoker 如何协作?Dubbo 的 SPI 为什么比 JDK SPI 强?Netty 传输与心跳怎么保活? 本文拆解调用链、自适应扩展、集群容错与优雅下线。
一、架构角色与一次调用链路
- Provider:服务提供者,暴露接口。
- Consumer:服务消费者,引用接口。
- Registry:注册中心(Nacos/ZK),Provider 注册、Consumer 订阅。
- Monitor:监控中心(可选),统计调用次数/耗时。
- Container:服务运行容器。
一次调用的完整协作(Consumer 侧):
二、SPI:自适应扩展机制
Dubbo 的微内核架构建立在自研 SPI(ExtensionLoader)之上,比 JDK 标准 SPI 强在三处:
- 按需实例化 + IoC:JDK SPI 一次性实例化所有实现;Dubbo SPI 只加载,用到才
getExtension(name)实例化,并支持 setter 注入依赖。 @Adaptive自适应扩展:运行时根据 URL 参数动态选实现。编译期生成一个适配器类,方法里从 URL 取 key,再getExtension实际实现。这是 Dubbo "URL 驱动一切"的核心。Wrapper自动包装(AOP):带构造器参数是该接口的Wrapper会被自动层层包裹,形成装饰链(如ProtocolFilterWrapper把 Filter 链织入)。
// 接口声明扩展点
@SPI("dubbo") // 默认实现名
public interface Protocol {
@Adaptive // 自适应: 从 URL 的 protocol 参数决定用哪个实现
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
}
// META-INF/dubbo/org.apache.dubbo.rpc.Protocol 内容:
// dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
// grpc=org.apache.dubbo.rpc.protocol.grpc.GrpcProtocolTIP
为什么 Dubbo 用 URL 贯穿全链路?因为 RPC 的所有参数(协议、序列化、超时、版本、分组、负载均衡策略)都被编码进一个 URL 对象,传给每一层。这样 Cluster、LoadBalance、Protocol 都能从同一个 URL 里读出自己该用什么实现——这就是 @Adaptive 能"运行时动态选实现"的前提。理解 URL 即理解 Dubbo 的扩展哲学。
三、Invoker:调用链的通用抽象
Dubbo 把"可调用的实体"统一抽象为 Invoker:它持有"invoker 是谁(URL)、能调什么(interface)、怎么调(invoke)"。无论是本地 Invoker、远程 Invoker、还是 Cluster 聚合后的 Invoker,对上层都是同一个接口。
- Consumer 侧:
@DubboReference通过ProxyFactory.getProxy用 Javassist/CGLIB 动态代理生成Proxy,把接口方法调用转成Invoker.invoke(invocation)。多个 Provider 被Cluster聚合成一个Cluster Invoker。 - Filter 链:
ConsumerContextFilter/ActiveLimitFilter等在 Invoker 外层包裹,做 traceId 透传、激活数限制等。 - Provider 侧:收到的请求经
Protocol解码 → 找到Invoker→ 经Filter链 → 反射调用真实实现类。
四、集群容错与负载均衡
Cluster 把多个 Provider Invoker 聚合成一个,负责容错:
| 容错策略 | 行为 | 场景 |
|---|---|---|
Failover(默认) | 失败重试其他节点(retries 次) | 读操作 |
Failfast | 失败立即抛 | 非幂等写 |
Failsafe | 失败忽略 | 日志等不重要的 |
Forking | 并行调多个,一个成功即返回 | 低延迟敏感 |
Broadcast | 广播所有节点 | 通知类 |
负载均衡:Random(加权随机,默认)、RoundRobin(加权轮询,带平滑)、LeastActive(最少活跃调用,谁闲给谁)、ConsistentHash(一致性哈希,相同 key 落同节点,保会话粘性与缓存命中)。
@DubboService(version = "1.0.0", timeout = 3000)
public class UserServiceImpl implements UserService { ... }
@DubboReference(version = "1.0.0",
loadbalance = "consistenthash", // 同 key 打到同一 Provider
cluster = "failover", retries = 2,
timeout = 3000)
private UserService userService;DANGER
- 非幂等接口用了 Failover 重试:扣款、下单等写操作,重试会执行多次(重复扣款)。非幂等接口必须
retries=0或加幂等键(如请求携带idempotentKey,落幂等表去重)。 - 超时设置不合理:Consumer 超时过短 + Failover 会放大故障(一个慢节点触发多次重试,雪崩);Provider 处理慢应先优化而非一味加 timeout。
- 接口版本不兼容就上线:
version不变导致旧 Consumer 调到新 Provider 字段缺失。重大变更必须升version+ 灰度。 - 大对象跨网络:DTO 含巨型集合撑爆网络与序列化。应裁剪字段、分页。
五、Netty 传输与心跳
Dubbo 默认用 Netty 做传输(4.x NIO 主从模型)。DubboProtocol 在 Provider 侧起 Netty Server 监听端口,Consumer 侧用 NettyClient 连接。关键机制:
- 编解码:请求/响应用
ExchangeCodec编码成 Dubbo 私有协议(魔数 + 头 + body),body 用 Hessian2/Kryo 序列化。请求头带requestId关联异步响应(Netty IO 线程解码后,按requestId唤醒等待的 Future)。 - 心跳:Consumer/Provider 间定时发心跳包(默认
heartbeat=60s),对方在heartbeatTimeout内无响应则断连并触发重连/剔除,避免半开连接。 - 异步化:调用默认返回
CompletableFuture,NettyClient发完不阻塞,等响应回来按requestId完成 Future。
六、注册中心交互与优雅下线
- 注册/订阅:Provider 启动 → 向 Registry 注册(含接口、IP、端口、权重、版本);Consumer 启动 → 订阅,Registry 把可用列表推给 Consumer,
NotifyListener更新本地Directory(Invoker 目录)。 - 优雅下线(graceful shutdown):服务收到
SIGTERM时,Dubbo 先注销注册中心上的实例(让 Consumer 不再路由新请求),等待已在进行中的调用处理完(默认wait=10000ms),再关闭协议端口。这避免了"直接 kill 导致正在处理的请求失败"。
<dubbo:service interface="com.demo.UserService" ref="userService"
version="1.0.0" timeout="3000" retries="2"/>
<dubbo:reference id="userService" interface="com.demo.UserService"
version="1.0.0" loadbalance="leastactive" cluster="failover"/>📚 官方文档与参考资料
- Dubbo 官方文档:https://dubbo.apache.org/zh-cn/overview/
- Dubbo GitHub:https://github.com/apache/dubbo
- Dubbo SPI 扩展机制:https://dubbo.apache.org/zh-cn/overview/tasks/extensibility/
✏️ 练习与自测
- 画出 Consumer 到 Provider 一次 RPC 中 Proxy/Cluster/LoadBalance/Filter/Invoker 的协作关系,标注 Netty 在哪一层。
- 设计"非幂等扣款接口"如何避免 Failover 重试造成重复扣款(幂等键 + retries=0 + 幂等表)。
- 用
@Adaptive思想说明 Dubbo SPI 相比 JDK SPI 的扩展优势(按需实例化 / URL 自适应 / Wrapper AOP)。 - 阐述 Dubbo 优雅下线的步骤,为什么不能直接
kill -9?
本节小结
Dubbo 把"服务注册发现 + 动态代理(Javassist)+ Cluster 容错 + 自适应 SPI + Netty 传输"封装为成熟体系。SPI(URL 驱动 + @Adaptive 运行时选实现)是微内核的根,Invoker 是统一调用抽象,Cluster+LoadBalance 做容错与选路,Netty 做高效传输与心跳,优雅下线避免请求丢失。用好它的关键:版本治理、合理超时、幂等保护、可观测。