Dubbo完整调用链:从服务暴露到Invoker返回
一、学完本页要真正会什么
Dubbo让远程调用写起来像Java接口,但内部不是一次普通方法调用。Provider要把实现对象转换成Invoker并通过Protocol导出;Consumer要把地址列表转换成远程Invoker,再由Directory、Router、Cluster和LoadBalance选择;网络层用requestId关联异步响应;Provider解码后根据serviceKey找到Exporter,经过Filter和线程派发才调用真实对象。
学完本页,你应该能够:
- 解释ServiceConfig、ReferenceConfig、ProxyFactory、Invoker、Exporter和Protocol的关系。
- 画出Provider启动、服务导出、注册和端口监听全过程。
- 画出Consumer订阅、Directory刷新、Cluster包装和代理创建全过程。
- 说明一次方法调用怎样变成Invocation。
- 区分Directory、Router、LoadBalance、Cluster容错的执行顺序。
- 解释Consumer和Provider Filter链分别在哪里执行。
- 说明DubboInvoker、ExchangeClient、requestId和Future怎样关联。
- 说明Provider怎样用serviceKey找到Exporter和真实实现。
- 解释IO线程、业务线程池、队列和拒绝之间的关系。
- 根据调用阶段定位无Provider、路由为空、连接失败、超时、线程池满、序列化和业务异常。
版本说明:Dubbo 2.x与3.x在Bootstrap、服务发现模型、迁移Invoker、Triple协议、Filter/Cluster内部类名等方面会演进。本页以长期稳定的抽象链路为主;具体类名、配置键和扩展装配顺序应以项目使用版本源码和官方文档为准。
二、核心对象不是一张名词表
| 对象 | 它代表什么 | 生命周期位置 |
|---|---|---|
| ServiceConfig | 一个服务如何暴露 | Provider启动配置 |
| ReferenceConfig | 一个远程接口如何引用 | Consumer启动/懒引用 |
| ProxyFactory | Java对象/接口代理与Invoker互转 | 暴露和引用两端 |
| Invoker | “可执行一次调用”的统一抽象 | 本地、远程、集群都可表示 |
| Exporter | 已导出的服务入口,持有Provider Invoker | Provider协议层 |
| Protocol | export/refer和协议调用能力 | Dubbo/Triple等协议 |
| Directory | 动态服务目录,输出候选Invoker | Consumer治理层 |
| Router | 按标签、条件、脚本等过滤候选 | Consumer治理层 |
| Cluster | 把多个Invoker伪装成一个并实现容错 | Consumer集群层 |
| LoadBalance | 在一次尝试中选择一个Invoker | Cluster调用内部 |
| Filter | 调用前后增强的Invoker包装链 | Consumer/Provider两端 |
| ExchangeClient | 请求-响应语义的通信客户端 | Consumer网络层 |
| Invocation | 接口、方法、参数和附件的调用描述 | 每次调用 |
| Result/AppResponse | 返回值或异常的调用结果 | 每次响应 |
最重要的统一抽象是Invoker:Provider真实对象能被包装成Invoker,单个远程地址能表示为Invoker,多个地址经Cluster包装后也表现为一个Invoker。因此上层代理只需调用invoker.invoke(invocation)。
三、完整分层地图
flowchart TD
A["业务接口代理"] --> B["Cluster Invoker"]
B --> C["Directory列出当前Invoker"]
C --> D["Router链过滤版本、标签和条件"]
D --> E["LoadBalance选择一次尝试的Provider"]
E --> F["Consumer Filter链"]
F --> G["Protocol Invoker"]
G --> H["ExchangeClient请求响应关联"]
H --> I["Transport、Codec与连接"]
I --> J["Provider解码和线程派发"]
J --> K["按serviceKey查Exporter"]
K --> L["Provider Filter链"]
L --> M["Proxy Invoker调用真实实现"]注册中心通常只改变Directory中的地址与元数据,不转发上面的业务请求。
四、Provider启动与服务暴露全过程
flowchart TD
A["Spring创建实现Bean并解析Dubbo配置"] --> B["ServiceConfig校验接口、实现、协议、注册中心"]
B --> C["为接口、group、version、方法构建服务URL/元数据"]
C --> D["ProxyFactory把实现对象包装为Provider Invoker"]
D --> E["Filter等扩展包装Invoker"]
E --> F["Protocol.export创建Exporter"]
F --> G["协议层把Exporter放入本地服务映射"]
G --> H["打开或复用Server端口并绑定处理器"]
H --> I["向注册/服务发现中心注册地址和元数据"]
I --> J["Provider进入可调用状态"]4.1 ServiceConfig做什么
它把注解/XML/API配置整理成一个可导出的服务描述,包括:
- interface。
- group/version。
- protocol/port。
- methods、timeout、retries等方法参数。
- registry和metadata。
- token、serialization、filter等扩展参数。
4.2 ProxyFactory为什么要把对象转成Invoker
真实实现可能是普通Java对象。ProxyFactory生成一个Provider侧Invoker:收到Invocation后,根据方法名和参数类型调用实现对象。后面的Protocol和Filter不需要知道实现是Spring Bean还是其他对象。
4.3 Exporter和服务映射
Exporter持有已导出的Invoker。Provider收到请求时,需要根据接口、group、version和端口等形成的serviceKey/服务键找到正确Exporter。如果Consumer调用的version不一致,网络端口可能可达,但Exporter查找仍失败。
4.4 端口为什么可以复用
同一协议端口可暴露多个服务。Server不是每个接口开一个端口,而是共享网络监听,请求到达后按服务标识分派到不同Exporter。
五、服务暴露每一步失败会怎样
| 阶段 | 典型失败 | 表现 |
|---|---|---|
| 配置解析 | interface/impl不匹配、版本错误 | 应用启动失败或服务未导出 |
| Proxy/Invoker | 方法签名或扩展装配错误 | export失败 |
| Protocol export | 端口冲突、绑定错误IP | Remoting异常、端口未监听 |
| 本地Exporter映射 | serviceKey冲突或重复暴露 | 调用映射异常 |
| 注册 | Registry不可用、权限/路径错误 | 本地已监听但Consumer发现不到 |
| 元数据发布 | 接口元数据缺失/不一致 | Dubbo 3服务发现或泛化调用异常 |
启动健康检查应区分“进程存活”“端口监听”“服务已导出”“注册成功”“依赖已就绪”。
六、Consumer引用服务全过程
flowchart TD
A["扫描DubboReference/ReferenceConfig"] --> B["校验接口、group、version和调用配置"]
B --> C["订阅注册中心或服务发现"]
C --> D["创建RegistryDirectory等动态目录"]
D --> E["地址与元数据转换为多个远程Protocol Invoker"]
E --> F["Router链绑定到目录"]
F --> G["Cluster.join把目录包装为Cluster Invoker"]
G --> H["Consumer Filter等包装调用链"]
H --> I["ProxyFactory为接口创建动态代理"]
I --> J["代理注入业务Bean"]Consumer注入的StockService不是Provider对象,也不是固定单地址客户端,而是代理。地址可在运行期更新,调用时才从Directory当前快照列出候选。
6.1 直接连接和注册中心引用
直连URL可直接refer一个地址,适合调试或受控场景;生产注册发现会建立动态Directory。即使注册中心暂时不可用,已有Directory缓存和连接可能继续调用,但地址变化、下线和治理规则会受影响。
6.2 启动检查与懒引用
某些配置允许启动时无Provider仍继续,或第一次调用时才初始化引用。它提高启动可用性,但把错误延迟到业务请求。核心依赖应结合Readiness和降级策略设计,不能统一关闭检查后不监控。
七、Directory如何从地址变成Invoker快照
注册中心/服务发现推送的是地址和元数据,Directory需要:
- 校验接口、group、version、protocol和可用状态。
- 将新地址refer成远程Invoker,建立或复用客户端连接。
- 复用未变化Invoker,减少连接抖动。
- 销毁已删除地址对应Invoker和连接引用。
- 原子替换调用线程可见的Invoker快照。
- 更新Router和动态配置。
flowchart TD
A["收到新地址集合"] --> B["与旧Invoker映射做Diff"]
B --> C["新增地址refer并建连接"]
B --> D["保留未变化Invoker"]
B --> E["下线地址标记并销毁"]
C --> F["形成新的不可变/安全快照"]
D --> F
E --> F
F --> G["后续调用读取新快照"]Provider从注册中心删除到所有Consumer快照更新存在传播窗口,优雅下线要先摘流、等待缓存与在途请求,再停止进程。
八、接口方法怎样变成Invocation
业务调用:
stockService.reserve("ORDER-9001", "SKU-1", 2);代理拦截后构造Invocation,通常包含:
- 目标接口/服务描述。
- 方法名。
- 参数类型。
- 参数值。
- attachments:group、version、timeout、trace、应用、租户等受控上下文。
Object基础方法如toString、equals通常由代理本地处理,不应都变成RPC。默认接口、泛化调用和异步返回的处理随代理与版本能力不同。
九、Consumer一次调用的治理链
flowchart TD
A["接口代理生成Invocation"] --> B["进入Cluster Invoker"]
B --> C["Directory.list读取当前候选"]
C --> D["Router按标签、条件、应用、机房过滤"]
D --> E{"候选是否为空"}
E -- "是" --> F["No provider/No route错误"]
E -- "否" --> G["LoadBalance选择一个Invoker"]
G --> H["执行Consumer Filter链"]
H --> I["Dubbo/Triple等Protocol Invoker"]
I --> J["编码并经ExchangeClient发送"]
J --> K{"结果、异常或超时"}
K --> L["Cluster按容错策略决定返回、重试或失败"]不同版本中Filter与Cluster包装的细节、调用次序和扩展激活可能不同,但诊断层次稳定:列目录、路由过滤、选址、一次远程尝试、容错决策。
十、Directory、Router、LoadBalance、Cluster不能混
| 层 | 输入 | 输出 | 问题 |
|---|---|---|---|
| Directory | 注册/发现后的动态地址 | 一组当前Invoker | 有哪些实例 |
| Router | 一组Invoker + Invocation | 过滤后的Invoker | 哪些实例允许本次调用 |
| LoadBalance | 过滤后的Invoker | 一个Invoker | 这次尝试选谁 |
| Cluster | Directory + 调用策略 | 一个集群Invoker/最终结果 | 失败后是否重试、并行、快速失败 |
例如Failover Cluster可能在第一次远程尝试失败后重新选择另一个Invoker。重试不是LoadBalance本身决定,而是Cluster容错策略驱动多次选择和调用。
十一、Cluster容错调用原理
抽象Failover:
for attempt in 1..maxAttempts:
invokers = directory.list(invocation)
routed = routers.route(invokers, invocation)
selected = loadBalance.select(routed, excluded)
try selected.invoke(invocation)
catch retryable RpcException -> next attempt真实实现还会处理可用性、粘滞连接、已选择列表、动态目录变化和异常分类。
写接口慎用Failover:第一次Provider可能已经提交,只是响应丢失;第二个Provider再执行会重复扣款。必须有稳定业务幂等键和结果查询,或使用Failfast等符合业务语义的策略。
十二、Filter链是Invoker装饰链
Filter不是独立网络代理,而是把下一个Invoker包装起来:
FilterA.invoke
→ FilterB.invoke
→ RealInvoker.invoke
← FilterB处理Result
← FilterA处理ResultConsumer Filter常处理:
- trace、认证、租户和超时附件。
- 客户端指标、日志、限流。
- 调用前参数/上下文校验。
Provider Filter常处理:
- 服务端认证、限流、并发控制。
- trace上下文恢复。
- 参数校验、异常映射、指标。
过滤器顺序错误可能导致鉴权前已记录错误身份、trace未传播、异常被伪装成功。敏感附件不能信任外部调用方原样传入。
十三、DubboInvoker到ExchangeClient
在Dubbo协议的稳定抽象中,单地址远程Invoker会:
- 从Invocation和URL计算方法超时、是否单向/异步等参数。
- 选择或复用ExchangeClient连接。
- 将Invocation编码为协议Request。
- 为需要响应的调用创建requestId和Future。
- 写入网络连接。
- 同步调用等待Future,异步调用返回异步结果句柄。
- 超时或网络异常映射为RpcException等结果。
Triple协议的HTTP/2流和Future实现细节不同,但仍需请求关联、超时、编解码和服务分派。
十四、RequestId与Future怎样关联
同一长连接可以同时发送多个请求,响应可能乱序:
flowchart TD
A["发送requestId=1001并注册Future-1001"] --> B["发送requestId=1002并注册Future-1002"]
B --> C["先收到responseId=1002"]
C --> D["从待响应映射取出Future-1002并完成"]
D --> E["后收到responseId=1001"]
E --> F["完成Future-1001"]待响应映射必须在完成、超时、连接关闭时清理,否则会内存泄漏。超时清理Future不等于取消Provider业务:请求可能仍在Provider线程池或已经提交。
十五、Provider收到字节后怎样找到服务
flowchart TD
A["IO线程读取协议帧"] --> B["Codec校验头、长度、序列化并解码Request"]
B --> C["ExchangeHandler识别心跳、单向或请求响应"]
C --> D["Dispatcher按线程模型派发"]
D --> E["业务线程读取Invocation附件"]
E --> F["按interface/group/version/port等形成serviceKey"]
F --> G["从Exporter映射查Provider Invoker"]
G --> H["执行Provider Filter链"]
H --> I["Proxy Invoker反射/字节码调用真实Bean"]
I --> J["Result编码为Response并写回"]找不到Exporter可能表现为服务不存在,即使端口可连。要核对interface、group、version、protocol和Provider是否已导出。
十六、Provider线程派发为什么决定稳定性
IO线程负责大量连接的读取、解码和写回,不应执行慢数据库或远程调用。Dispatcher将请求交给业务线程池;具体派发策略和名称随版本/协议配置变化。
并发约等于到达QPS × 平均处理秒数若100 QPS、平均耗时2秒,大约有200个在途业务请求。线程池200并不代表安全,因为数据库连接池可能只有50,剩余线程都在等待连接。
队列太大:把拒绝变成长时间排队,Consumer先超时,Provider稍后仍执行写操作。队列太小:突发快速拒绝。阈值必须根据SLO、下游容量和压测配置。
十七、响应返回全过程
flowchart TD
A["真实服务返回值或抛异常"] --> B["Provider Filter处理Result"]
B --> C["构造带requestId和状态的Response"]
C --> D["序列化并写入连接"]
D --> E["Consumer IO线程解码Response"]
E --> F["按requestId完成对应Future"]
F --> G["Consumer Filter和Cluster收到Result"]
G --> H["代理还原Java返回值或抛出异常"]业务异常、序列化异常、网络异常和超时的发生层不同。Provider业务已经成功但响应序列化失败,Consumer仍可能看到失败,因此写接口必须可查询与幂等。
十八、同步、异步和单向调用
| 模式 | Consumer行为 | 风险 |
|---|---|---|
| 同步 | 当前线程等待Future完成/超时 | 易理解但占等待线程 |
| 异步 | 返回Future/CompletionStage等 | 上下文传播、异常处理和并发控制 |
| 单向 | 发送后不等待业务响应 | 无法确认Provider是否执行,不适合关键写入 |
异步不会让Provider业务自动更快,只是Consumer线程不必阻塞。若无限发起异步请求而不限制in-flight,同样会耗尽连接、Future映射和Provider线程池。
十九、RPC上下文和附件传播
traceId、租户、用户、灰度标签可以放附件,但必须:
- 由认证后的可信上下文产生。
- 使用明确键名和大小限制。
- Provider收到后校验并在请求结束清理线程上下文。
- 异步线程显式传播需要的上下文。
- 不传密码、完整Token或大对象。
线程池复用时不清理ThreadLocal会把上一个租户身份泄漏到下一个请求。
二十、Provider优雅下线
flowchart TD
A["实例标记不再接新流量/从注册中心摘除"] --> B["等待Consumer收到地址变更"]
B --> C["停止接受新请求并等待在途调用"]
C --> D["关闭Exporter、Server和连接"]
D --> E["销毁业务资源并退出"]直接杀进程会让Consumer缓存仍选择旧实例、连接中请求失败。优雅下线时间要覆盖地址传播和在途请求,但不能无限等待。
二十一、按阶段定位错误
| 现象 | 可能阶段 | 首要证据 |
|---|---|---|
| No provider | Directory为空、group/version不匹配 | Consumer地址快照和订阅 |
| 有地址但路由后为空 | 标签/条件/应用路由 | Router规则和Invocation附件 |
| 连接拒绝 | 地址错误、端口未监听、防火墙 | 目标IP端口和Provider监听 |
| 请求写出失败 | 连接关闭、网络、编码前异常 | Consumer remoting日志 |
| Timeout | 排队、业务慢、响应丢失 | requestId Trace、Provider日志和线程池 |
| Thread pool exhausted | Provider业务线程/队列满 | active、queue、reject、DB池 |
| Service not found | Exporter serviceKey不匹配 | interface/group/version/port |
| SerializationException | DTO/API/序列化白名单不兼容 | 两端版本和原始错误 |
| 重复扣减 | Cluster/上游重试、响应丢失 | 业务幂等键和尝试次数 |
二十二、生产排查Runbook
22.1 No provider available
- 确认Consumer接口、group、version和protocol。
- 查看Consumer本地Directory快照,不只看注册中心控制台。
- 确认Provider已经export且注册地址可从Consumer访问。
- 检查Router、标签、应用和机房规则是否过滤全部实例。
- 检查注册中心推送、缓存和Provider优雅下线状态。
22.2 大量超时
flowchart TD
A["按traceId/requestId定位一次尝试"] --> B["区分连接等待、网络、Provider排队、业务执行、响应序列化"]
B --> C["查看Provider线程池、队列、拒绝和DB连接池"]
C --> D["查看GC、CPU、慢SQL、下游调用"]
D --> E["检查Cluster和上游是否重试放大"]
E --> F["限流止损、摘除坏实例、缩短慢事务"]22.3 Consumer超时但Provider执行成功
按业务键查询Provider事实;保持同一幂等键重试或返回待确认。不要用新请求号再次扣减。调大超时前先查P99和排队,否则只会让Consumer线程等待更久。
22.4 只有一个Provider失败
按目标地址拆分错误率和P99,检查该实例版本、CPU、GC、连接池、线程池和配置;确认LoadBalance权重和路由。必要时先摘除坏实例。
22.5 发布后方法找不到或反序列化失败
比较Consumer/Provider API包、接口方法描述、DTO包名/字段类型、serialization配置和灰度顺序。先兼容Provider再升级Consumer,最后再删除旧契约。
二十三、JDK 8 Demo:Invoker、Filter和Cluster最小模型
import java.util.ArrayList;
import java.util.List;
public class InvokerChainDemo {
interface Invoker {
String invoke(String invocation);
}
interface Filter {
String invoke(Invoker next, String invocation);
}
static Invoker wrap(final Invoker target, final Filter filter) {
return new Invoker() {
public String invoke(String invocation) {
return filter.invoke(target, invocation);
}
};
}
public static void main(String[] args) {
final List<Invoker> directory = new ArrayList<Invoker>();
directory.add(invocation -> "provider-A:" + invocation);
directory.add(invocation -> "provider-B:" + invocation);
Invoker cluster = new Invoker() {
private int index;
public String invoke(String invocation) {
Invoker selected = directory.get(index++ % directory.size());
return selected.invoke(invocation);
}
};
Invoker filtered = wrap(cluster, new Filter() {
public String invoke(Invoker next, String invocation) {
return "trace[T-1]->" + next.invoke(invocation);
}
});
System.out.println(filtered.invoke("reserve#ORDER-1"));
System.out.println(filtered.invoke("reserve#ORDER-2"));
}
}输出:
trace[T-1]->provider-A:reserve#ORDER-1
trace[T-1]->provider-B:reserve#ORDER-2该Demo使用JDK 8 lambda,抽象展示Directory、Cluster/LoadBalance和Filter包装。真实Dubbo还包含URL、Invocation、Result、Router、Protocol和网络层。
二十四、常见错误与后果
| 错误 | 后果 | 正确方向 |
|---|---|---|
| 把代理当Provider对象 | 忽略网络失败和序列化 | 按远程调用设计超时与幂等 |
| 认为Registry转发请求 | 注册中心被错误定位 | Directory缓存地址,Consumer直连 |
| 路由和负载均衡混为一谈 | 排查有实例但不可调用困难 | Router先过滤,LoadBalance再选一个 |
| 写接口默认Failover重试 | 响应丢失后重复副作用 | 幂等键、查询、符合语义的Cluster策略 |
| IO线程执行慢业务 | 多连接一起停止读写 | 派发业务线程并限制阻塞 |
| Provider队列设得巨大 | Consumer超时后Provider仍执行 | 有界队列、限流和容量匹配 |
| Future超时不清理 | 待响应映射内存泄漏 | 完成/超时/断连统一清理 |
| 上下文ThreadLocal不清理 | 租户/身份串请求 | finally清理和可信附件 |
| 直接杀Provider | 缓存窗口内大量连接失败 | 摘流、等待传播和在途请求 |
二十五、面试标准回答
25.1 一次Dubbo调用全过程
Consumer注入接口代理,方法调用被转换成Invocation并进入Cluster Invoker;Directory从本地服务快照列出Invoker,Router过滤,LoadBalance选一个,Consumer Filter处理上下文后由协议Invoker经ExchangeClient发送。Provider解码并派发业务线程,根据interface/group/version等serviceKey找到Exporter,经过Provider Filter和Proxy Invoker调用真实实现;响应带原requestId返回,Consumer完成对应Future并还原Java结果。
25.2 Invoker为什么重要
Invoker统一表示可调用对象:Provider实现可包装成本地Invoker,单个远程地址是Protocol Invoker,多个地址经Cluster包装后仍是Invoker。代理和Filter只依赖统一invoke接口,因此协议、集群和扩展可以组合。
25.3 Directory、Router、LoadBalance和Cluster区别
Directory回答当前有哪些Invoker;Router按标签、版本、条件等过滤本次允许的候选;LoadBalance从候选中为一次尝试选一个;Cluster决定一次失败后是否重试、并行或快速失败,并把多Invoker包装成单一调用入口。
25.4 Consumer超时是否代表Provider没执行
不代表。请求可能尚未发送、正在队列、已执行未返回、业务已提交但响应丢失。Future超时只结束Consumer等待,通常不能撤销Provider副作用。写接口必须有稳定幂等键和事实查询。
二十六、关联知识点
本章小结
Dubbo调用链的主线是“配置变Invoker、目录变候选、Cluster做容错、Protocol做远程调用、requestId关联响应、Exporter找到真实服务”。把每层输入输出和失败证据分清,线上问题就能从猜测变成分段定位。
