Dubbo 的服务调用过程是什么样的?
结论先行:一次调用在消费端要穿过“代理 → 集群 → 目录 → 路由 → 负载均衡 → 过滤器 → 协议 → 交换 → 传输 → 序列化”十道关卡,在提供端按相反顺序还原。真正决定成败的是超时、重试与线程池三处边界。
一、消费端全链路
调用 userService.getById(1L)
│
├─ Proxy 生成 RpcInvocation(方法名、参数类型、实参、attachment)
├─ Cluster FailoverClusterInvoker:循环重试
├─ Directory 拿到该接口的 invoker 列表(本地缓存,注册中心变更时刷新)
├─ Router 按条件/标签规则过滤
├─ LoadBalance 选出一个 invoker(默认 random 加权)
├─ Filter 消费端过滤器链:超时、埋点、限流、泛化
├─ Protocol 组装 Request
├─ Exchange 分配 requestId,建 DefaultFuture
├─ Transport Netty 写出
└─ Serialize Hessian2 编码 body
二、每一层的关键动作
| 层 | 关键动作 | 出问题时的现象 |
|---|---|---|
| Proxy | 生成 RpcInvocation | 参数类型不匹配、泛化调用失败 |
| Cluster | 选择容错策略,组织重试 | 重试放大流量、非幂等重复执行 |
| Directory | 维护 invoker 列表与本地缓存 | 地址为空、旧地址未摘除 |
| Router | 过滤候选集 | 灰度流量打错、跨机房调用 |
| LoadBalance | 加权选择 | 单机热点、长尾延迟 |
| Filter | 埋点、鉴权、限流 | 链路断点、限流误杀 |
| Protocol/Exchange | 编码、requestId 配对 | 响应错配、连接断开 |
| Transport | 连接复用与心跳 | 连接数暴涨、假死 |
三、提供端反向流程
Netty 读到字节
→ 解码(长度字段拆包)
→ 反序列化得到 Invocation
→ 查 Exporter(按 interface + version + group 定位)
→ 过滤器链(Provider 侧)
→ 反射调用实现类
→ 结果序列化写回,附 requestId
请求 ID 是消费端与提供端唯一的对应关系,一条连接上并发多个请求时靠它把响应唤醒到正确的 Future。
四、可调的关键参数
dubbo:
consumer:
timeout: 1000 # 单次调用超时,毫秒
retries: 2 # 失败重试次数,仅幂等方法开启
check: false # 启动时不检查提供者是否存在
actives: 100 # 每服务每方法最大并发
provider:
threads: 200 # 业务线程池大小
executes: 200 # 每方法最大并发执行数
accepts: 0 # 允许的最大连接数,0 表示不限制
// 单方法覆盖全局配置
@DubboReference(timeout = 500, retries = 0, cluster = "failfast")
private PayService payService;
常见追问
- 追问:超时时间从哪一刻开始算?从发起调用算起,包含排队、路由、网络、服务端处理全过程;消费端超时后不保证服务端停止执行,所以写操作必须靠幂等兜底。
- 追问:重试为什么容易引发雪崩?一次失败变成三次请求,下游已过载时流量被放大三倍,所以非幂等接口要把
retries设为 0。 - 追问:
actives和executes有什么区别?前者限制消费端对某服务的并发请求数,后者限制提供端某方法的并发执行数,一个在入口一个在出口。
小结
- 消费端十层串联,提供端反向还原,
requestId是并发下的配对依据。 - 路由决定“能不能去”,负载均衡决定“去哪个”,重试决定“失败再去哪”。
- 超时是端到端的,消费端放弃不代表服务端停止。
- 非幂等接口必须关重试,否则故障会被放大。