Skip to content

Dubbo完整调用链:从服务暴露到Invoker返回

一、学完本页要真正会什么

Dubbo让远程调用写起来像Java接口,但内部不是一次普通方法调用。Provider要把实现对象转换成Invoker并通过Protocol导出;Consumer要把地址列表转换成远程Invoker,再由Directory、Router、Cluster和LoadBalance选择;网络层用requestId关联异步响应;Provider解码后根据serviceKey找到Exporter,经过Filter和线程派发才调用真实对象。

学完本页,你应该能够:

  1. 解释ServiceConfig、ReferenceConfig、ProxyFactory、Invoker、Exporter和Protocol的关系。
  2. 画出Provider启动、服务导出、注册和端口监听全过程。
  3. 画出Consumer订阅、Directory刷新、Cluster包装和代理创建全过程。
  4. 说明一次方法调用怎样变成Invocation。
  5. 区分Directory、Router、LoadBalance、Cluster容错的执行顺序。
  6. 解释Consumer和Provider Filter链分别在哪里执行。
  7. 说明DubboInvoker、ExchangeClient、requestId和Future怎样关联。
  8. 说明Provider怎样用serviceKey找到Exporter和真实实现。
  9. 解释IO线程、业务线程池、队列和拒绝之间的关系。
  10. 根据调用阶段定位无Provider、路由为空、连接失败、超时、线程池满、序列化和业务异常。

版本说明:Dubbo 2.x与3.x在Bootstrap、服务发现模型、迁移Invoker、Triple协议、Filter/Cluster内部类名等方面会演进。本页以长期稳定的抽象链路为主;具体类名、配置键和扩展装配顺序应以项目使用版本源码和官方文档为准。

二、核心对象不是一张名词表

对象它代表什么生命周期位置
ServiceConfig一个服务如何暴露Provider启动配置
ReferenceConfig一个远程接口如何引用Consumer启动/懒引用
ProxyFactoryJava对象/接口代理与Invoker互转暴露和引用两端
Invoker“可执行一次调用”的统一抽象本地、远程、集群都可表示
Exporter已导出的服务入口,持有Provider InvokerProvider协议层
Protocolexport/refer和协议调用能力Dubbo/Triple等协议
Directory动态服务目录,输出候选InvokerConsumer治理层
Router按标签、条件、脚本等过滤候选Consumer治理层
Cluster把多个Invoker伪装成一个并实现容错Consumer集群层
LoadBalance在一次尝试中选择一个InvokerCluster调用内部
Filter调用前后增强的Invoker包装链Consumer/Provider两端
ExchangeClient请求-响应语义的通信客户端Consumer网络层
Invocation接口、方法、参数和附件的调用描述每次调用
Result/AppResponse返回值或异常的调用结果每次响应

最重要的统一抽象是Invoker:Provider真实对象能被包装成Invoker,单个远程地址能表示为Invoker,多个地址经Cluster包装后也表现为一个Invoker。因此上层代理只需调用invoker.invoke(invocation)

三、完整分层地图

mermaid
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启动与服务暴露全过程

mermaid
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端口冲突、绑定错误IPRemoting异常、端口未监听
本地Exporter映射serviceKey冲突或重复暴露调用映射异常
注册Registry不可用、权限/路径错误本地已监听但Consumer发现不到
元数据发布接口元数据缺失/不一致Dubbo 3服务发现或泛化调用异常

启动健康检查应区分“进程存活”“端口监听”“服务已导出”“注册成功”“依赖已就绪”。

六、Consumer引用服务全过程

mermaid
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需要:

  1. 校验接口、group、version、protocol和可用状态。
  2. 将新地址refer成远程Invoker,建立或复用客户端连接。
  3. 复用未变化Invoker,减少连接抖动。
  4. 销毁已删除地址对应Invoker和连接引用。
  5. 原子替换调用线程可见的Invoker快照。
  6. 更新Router和动态配置。
mermaid
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

业务调用:

java
stockService.reserve("ORDER-9001", "SKU-1", 2);

代理拦截后构造Invocation,通常包含:

  • 目标接口/服务描述。
  • 方法名。
  • 参数类型。
  • 参数值。
  • attachments:group、version、timeout、trace、应用、租户等受控上下文。

Object基础方法如toStringequals通常由代理本地处理,不应都变成RPC。默认接口、泛化调用和异步返回的处理随代理与版本能力不同。

九、Consumer一次调用的治理链

mermaid
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这次尝试选谁
ClusterDirectory + 调用策略一个集群Invoker/最终结果失败后是否重试、并行、快速失败

例如Failover Cluster可能在第一次远程尝试失败后重新选择另一个Invoker。重试不是LoadBalance本身决定,而是Cluster容错策略驱动多次选择和调用。

十一、Cluster容错调用原理

抽象Failover:

text
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包装起来:

text
FilterA.invoke
  → FilterB.invoke
    → RealInvoker.invoke
  ← FilterB处理Result
← FilterA处理Result

Consumer Filter常处理:

  • trace、认证、租户和超时附件。
  • 客户端指标、日志、限流。
  • 调用前参数/上下文校验。

Provider Filter常处理:

  • 服务端认证、限流、并发控制。
  • trace上下文恢复。
  • 参数校验、异常映射、指标。

过滤器顺序错误可能导致鉴权前已记录错误身份、trace未传播、异常被伪装成功。敏感附件不能信任外部调用方原样传入。

十三、DubboInvoker到ExchangeClient

在Dubbo协议的稳定抽象中,单地址远程Invoker会:

  1. 从Invocation和URL计算方法超时、是否单向/异步等参数。
  2. 选择或复用ExchangeClient连接。
  3. 将Invocation编码为协议Request。
  4. 为需要响应的调用创建requestId和Future。
  5. 写入网络连接。
  6. 同步调用等待Future,异步调用返回异步结果句柄。
  7. 超时或网络异常映射为RpcException等结果。

Triple协议的HTTP/2流和Future实现细节不同,但仍需请求关联、超时、编解码和服务分派。

十四、RequestId与Future怎样关联

同一长连接可以同时发送多个请求,响应可能乱序:

mermaid
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收到字节后怎样找到服务

mermaid
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将请求交给业务线程池;具体派发策略和名称随版本/协议配置变化。

text
并发约等于到达QPS × 平均处理秒数

若100 QPS、平均耗时2秒,大约有200个在途业务请求。线程池200并不代表安全,因为数据库连接池可能只有50,剩余线程都在等待连接。

队列太大:把拒绝变成长时间排队,Consumer先超时,Provider稍后仍执行写操作。队列太小:突发快速拒绝。阈值必须根据SLO、下游容量和压测配置。

十七、响应返回全过程

mermaid
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优雅下线

mermaid
flowchart TD
    A["实例标记不再接新流量/从注册中心摘除"] --> B["等待Consumer收到地址变更"]
    B --> C["停止接受新请求并等待在途调用"]
    C --> D["关闭Exporter、Server和连接"]
    D --> E["销毁业务资源并退出"]

直接杀进程会让Consumer缓存仍选择旧实例、连接中请求失败。优雅下线时间要覆盖地址传播和在途请求,但不能无限等待。

二十一、按阶段定位错误

现象可能阶段首要证据
No providerDirectory为空、group/version不匹配Consumer地址快照和订阅
有地址但路由后为空标签/条件/应用路由Router规则和Invocation附件
连接拒绝地址错误、端口未监听、防火墙目标IP端口和Provider监听
请求写出失败连接关闭、网络、编码前异常Consumer remoting日志
Timeout排队、业务慢、响应丢失requestId Trace、Provider日志和线程池
Thread pool exhaustedProvider业务线程/队列满active、queue、reject、DB池
Service not foundExporter serviceKey不匹配interface/group/version/port
SerializationExceptionDTO/API/序列化白名单不兼容两端版本和原始错误
重复扣减Cluster/上游重试、响应丢失业务幂等键和尝试次数

二十二、生产排查Runbook

22.1 No provider available

  1. 确认Consumer接口、group、version和protocol。
  2. 查看Consumer本地Directory快照,不只看注册中心控制台。
  3. 确认Provider已经export且注册地址可从Consumer访问。
  4. 检查Router、标签、应用和机房规则是否过滤全部实例。
  5. 检查注册中心推送、缓存和Provider优雅下线状态。

22.2 大量超时

mermaid
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最小模型

java
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"));
    }
}

输出:

text
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找到真实服务”。把每层输入输出和失败证据分清,线上问题就能从猜测变成分段定位。