问题引入

面试官递过来一杯水,轻描淡写地问:"你们服务间怎么通信?"

你答:"Dubbo。"

"那你说说,@DubboReference 注解是怎么让远程方法调用看起来像本地调用的?"

这是一个经典的开场,但真正的追问才刚刚开始:

  • "服务提供者启动时,Dubbo 做了哪些事?服务地址是怎么注册到注册中心的?"
  • "消费者引用服务时,为什么能直接拿到接口的代理对象?这个代理的 InvocationHandler 里做了什么?"
  • "一次 RPC 调用从发起到返回,经过了哪些组件?Cluster、Directory、Router、LoadBalance 分别在什么时候介入?"
  • "Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?@Adaptive 注解生成的适配器类长什么样?"
  • "默认的 Failover 失败重试 2 次,如果下游已经挂了,重试是不是会放大故障?"
  • "Dubbo 3.x 说应用级服务发现能减轻注册中心压力,具体是怎么做的?和接口级有什么区别?"

这些问题不是背八股能答出来的。它们要求你理解 Dubbo 从设计到实现的完整链路——而这条链路的起点,是一个看似简单的工程困境。

困境具象化:从本地方法到远程调用的鸿沟

2011 年,阿里巴巴的电商系统从单体拆分出用户中心、商品中心、订单中心。拆分后,订单服务需要调用用户服务获取收货地址——但用户服务部署在另一台机器的 20880 端口。

最原始的方案是手写 HTTP 调用:拼 URL、发 HTTP 请求、解析 JSON 响应。这种方式有几个致命问题:

  1. 调用方强耦合服务地址http://192.168.1.100:8080/user/getAddress——IP 变了怎么办?扩容了新节点怎么发现?
  2. 没有负载均衡:所有流量打到一台机器,另外两台空转。
  3. 故障无感知:下游节点挂了,调用方还在发请求,超时后才能知道。
  4. 没有统一的治理手段:限流、降级、监控——每个服务自己造轮子。

Dubbo 的诞生就是为了填平这个鸿沟:让远程调用像本地调用一样简单,同时内置服务治理的全套能力。但"像本地调用一样简单"这个承诺背后,隐藏着一个极其复杂的工程实现。

本文从一次 @DubboReference 的方法调用出发,逐层剥开 Dubbo 的核心架构与调用链路。


核心概念

Dubbo 核心架构:五大角色

Dubbo 的架构可以用一张经典图概括:

graph LR Provider[Provider<br/>服务提供者] -->|register| Registry[Registry<br/>注册中心] Consumer[Consumer<br/>服务消费者] -->|subscribe| Registry Registry -->|notify| Consumer Consumer -->|invoke| Provider Provider -->|report| Monitor[Monitor<br/>监控中心] Consumer -->|report| Monitor Container[Container<br/>服务容器] -->|run| Provider

读图导引:关注箭头方向。Provider 向 Registry 注册,Consumer 向 Registry 订阅,Registry 推送地址变更给 Consumer,Consumer 直接调用 Provider(不走 Registry),Monitor 收集调用统计。

五个角色的职责:

  • Provider(服务提供者):暴露服务的服务提供方。启动时将服务接口和实现类注册到注册中心。
  • Consumer(服务消费者):调用远程服务的服务消费方。启动时向注册中心订阅服务地址列表。
  • Registry(注册中心):服务注册与发现的中心。Dubbo 支持 Nacos、ZooKeeper、Redis 等多种注册中心实现。
  • Monitor(监控中心):统计服务的调用次数和调用时间的监控中心。Dubbo 通过 Filter 机制拦截调用,上报统计数据。
  • Container(服务容器):服务运行容器。Spring 容器是最常用的实现,Dubbo 也内嵌了 Standalone 容器。

一个常见的面试误区:Consumer 调用 Provider 时,请求不经过 Registry。Registry 只负责地址发现,实际调用是 Consumer 直连 Provider。

服务暴露(Export)与服务引用(Refer)

Dubbo 的核心可以概括为两个动作:

  • 服务暴露(Export):Provider 将接口实现类包装成 Invoker,打开网络端口监听,将地址注册到 Registry。
  • 服务引用(Refer):Consumer 向 Registry 订阅地址,创建网络连接,生成接口的代理对象。

这两个动作是对称的:暴露是"由内到外",引用是"由外到内"。

Invoker:Dubbo 的核心抽象

Invoker 是 Dubbo 中最核心的抽象接口:

java 复制代码
public interface Invoker<T> extends Node {
    Class<T> getInterface();
    Result invoke(Invocation invocation) throws RpcException;
}

Invoker 代表一个可执行体。它可以是:

  • 本地实现类的包装(AbstractProxyInvoker
  • 远程服务的代理(DubboInvoker
  • 经过 Cluster、Filter、LoadBalance 包装后的增强版 Invoker

Invocation 封装了一次调用的上下文:方法名、参数类型、参数值、附件(attachments)等。Result 封装了调用结果。

面试中常问:Dubbo 的 Invoker 和 Spring 的 Bean 有什么区别?Bean 是对象实例,Invoker 是调用抽象。一个 Bean 可以被包装成多个 Invoker(如本地 Invoker 和远程 Invoker),一个 Invoker 也可以代理多个底层实现(如 ClusterInvoker 代理多个 Provider 的 Invoker)。

Protocol:网络通信的抽象

Protocol 是 Dubbo 对 RPC 协议的抽象:

java 复制代码
@SPI("dubbo")
public interface Protocol {
    int getDefaultPort();
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
    void destroy();
}

两个核心方法:

  • export(Invoker):将 Invoker 暴露为网络服务,返回 Exporter(持有 Invoker 引用,unexport 时释放)。
  • refer(Class, URL):根据服务接口和 URL 创建远程 Invoker。

Dubbo 默认使用 DubboProtocol(基于 Dubbo 自定义协议,Netty 传输),也支持 TripleProtocol(Dubbo 3.x,基于 HTTP/2,兼容 gRPC)。

ProxyFactory:代理对象的工厂

ProxyFactory 负责生成接口的代理对象:

java 复制代码
@SPI("javassist")
public interface ProxyFactory {
    <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces);
    <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url);
}
  • getProxy:为 Consumer 生成代理对象(如 UserService 接口的代理)。
  • getInvoker:为 Provider 将实现类包装成 Invoker。

默认实现是 JavassistProxyFactory,使用 Javassist 字节码技术生成代理类,性能优于 JDK 动态代理。


原理分析

第一层:服务暴露的完整链路(Provider 视角)

当你在一个 Spring Bean 上标注 @DubboService(或旧版的 @Service),Dubbo 启动时做了什么?

1.1 从注解到 ServiceConfig

Spring 容器启动时,ServiceClassPostProcessor(Dubbo 的 BeanPostProcessor)扫描到 @DubboService 注解,解析出接口类型、版本号、超时时间等配置,创建 ServiceConfig 对象。

java 复制代码
// 简化后的核心逻辑
ServiceConfig<DemoService> service = new ServiceConfig<>();
service.setInterface(DemoService.class);
service.setRef(demoServiceImpl);  // 实际实现类
service.setVersion("1.0.0");
service.export();  // 触发服务暴露

1.2 ServiceConfig.export() 的核心逻辑

ServiceConfig.export() 是服务暴露的入口,其核心流程如下:

graph TD A[ServiceConfig.export] --> B{是否延迟暴露} B -->|是| C[提交到延迟线程池] B -->|否| D[doExport] C --> D D --> E[检查配置合法性] E --> F[组装 URL] F --> G[doExportUrls] G --> H[遍历 ProtocolConfig 列表] H --> I[doExportUrlsFor1Protocol] I --> J[创建 ProxyFactory.getInvoker] J --> K[Protocol.export] K --> L[打开 Server 监听端口] K --> M[RegistryProtocol.export] M --> N[向注册中心注册服务地址]

读图导引:从左到右跟随时间线。关键点在 K 节点——Protocol.export 走两个分支:一个是打开本地网络端口(L),另一个是向注册中心注册(N)。注意 M 节点的 RegistryProtocol 是 Protocol 的包装器(Wrapper),它在 DubboProtocol.export 的基础上增加了注册逻辑。

URL 是 Dubbo 配置的载体,一个典型的服务 URL 长这样:

复制代码
registry://nacos-server:8848/org.apache.dubbo.registry.RegistryService?
  application=demo-provider&
  registry=nacos&
  dubbo=2.0.2&
  interface=com.example.DemoService&
  version=1.0.0&
  timeout=3000

1.3 ProxyFactory.getInvoker:将实现类包装成 Invoker

ServiceConfig 调用 proxyFactory.getInvoker(ref, interfaceClass, url),默认使用 JavassistProxyFactory

java 复制代码
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    // 使用 Javassist 生成 Wrapper 类
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 
        ? proxy.getClass() : type);
    
    return new AbstractProxyInvoker<T>(proxy, type, url) {
        @Override
        protected Object doInvoke(T proxy, String methodName, 
                                   Class<?>[] parameterTypes, Object[] arguments) {
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
}

这里的 Wrapper 不是装饰者模式的 Wrapper,而是 Javassist 生成的调用包装类。它通过字节码技术直接生成调用目标方法的代码,避免了反射的性能损耗。

生成的 Wrapper 类大致长这样:

java 复制代码
public class Wrapper0 extends Wrapper {
    public Object invokeMethod(Object o, String n, Class[] p, Object[] v) {
        com.example.DemoService w = (com.example.DemoService) o;
        if ("sayHello".equals(n) && p.length == 1) {
            return w.sayHello((String) v[0]);
        }
        // ... 其他方法分支
        throw new NoSuchMethodException();
    }
}

1.4 Protocol.export:打开网络端口

ProxyFactory 生成的 Invoker 被交给 Protocol.export()。由于 URL 的协议头是 registry://,Dubbo 的 SPI 机制会加载 RegistryProtocol

java 复制代码
// RegistryProtocol.export 简化版
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    URL registryUrl = getRegistryUrl(originInvoker);
    URL providerUrl = getProviderUrl(originInvoker);
    
    // 1. 调用底层 Protocol(DubboProtocol)打开 Server
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);
    
    // 2. 获取注册中心实例
    final Registry registry = getRegistry(originInvoker);
    
    // 3. 向注册中心注册服务地址
    register(registryUrl, registeredProviderUrl);
    
    return new DestroyableExporter<>(exporter);
}

doLocalExport 内部调用 DubboProtocol.export

java 复制代码
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    URL url = invoker.getUrl();
    String key = serviceKey(url);  // 如 com.example.DemoService:1.0.0:20880
    
    // 1. 创建 DubboExporter 并缓存
    DubboExporter<T> exporter = new DubboExporter<>(invoker, key, exporterMap);
    exporterMap.put(key, exporter);
    
    // 2. 打开 Server(NettyServer)
    openServer(url);
    
    // 3. 优化序列化
    optimizeSerialization(url);
    
    return exporter;
}

openServer 会创建 NettyServer,监听指定端口(默认 20880)。NettyServer 内部初始化 Netty 的 ServerBootstrap,配置 bossGroupworkerGroup,并绑定 DubboCodec 处理 Dubbo 协议的编解码。

1.5 向注册中心注册

回到 RegistryProtocol.export,注册逻辑如下:

java 复制代码
private void register(URL registryUrl, URL registeredProviderUrl) {
    Registry registry = registryFactory.getRegistry(registryUrl);
    registry.register(registeredProviderUrl);
}

以 Nacos 为例,NacosRegistry.register 会调用 Nacos 的 NamingService.registerInstance,将服务实例(IP、端口、元数据)注册到 Nacos Server,并启动定时心跳(默认 5 秒一次)维持租约。

1.6 服务暴露总结

sequenceDiagram participant SC as ServiceConfig participant PF as ProxyFactory participant RP as RegistryProtocol participant DP as DubboProtocol participant NS as NettyServer participant Reg as Registry SC->>PF: getInvoker(impl, interface, url) PF->>PF: 生成 Javassist Wrapper PF-->>SC: AbstractProxyInvoker SC->>RP: export(invoker) RP->>DP: export(invoker) DP->>NS: openServer(url) NS-->>DP: NettyServer 监听 20880 DP-->>RP: DubboExporter RP->>Reg: register(providerUrl) Reg-->>RP: 注册成功

读图导引:关注时序。ServiceConfig 先生成 Invoker,再交给 RegistryProtocol,RegistryProtocol 先调用 DubboProtocol 打开 Server,再向 Registry 注册地址。两个动作的顺序很重要——必须先打开端口再注册,否则消费者可能拿到地址却连不上。


第二层:服务引用的完整链路(Consumer 视角)

当你在字段上标注 @DubboReference

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {
    @DubboReference(version = "1.0.0", timeout = 3000)
    private UserService userService;
    
    public Order createOrder(Long userId) {
        Address address = userService.getAddress(userId);  // 看起来像本地调用
        // ...
    }
}

Spring 注入时,userService 不是一个真实的实现类,而是一个代理对象。这个代理是怎么生成的?

2.1 @DubboReference 的注入时机

ReferenceAnnotationBeanPostProcessor 是 Dubbo 的 BeanPostProcessor,它拦截 @DubboReference 注解的字段,创建 ReferenceConfig,然后生成代理对象注入。

java 复制代码
// 简化后的注入逻辑
protected Object doGetInjectedBean(AnnotationAttributes attributes, Object bean, 
                                    String beanName, Class<?> injectedType) {
    String referenceBeanName = buildReferenceBeanName(attributes, injectedType);
    
    ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referenceBeanName, attributes, injectedType);
    
    // ReferenceBean 实现了 FactoryBean,getObject() 返回代理对象
    return getBeanFactory().getBean(referenceBeanName);
}

ReferenceBean 实现了 Spring 的 FactoryBean,其 getObject() 方法调用 ReferenceConfig.get() 获取代理对象。

2.2 ReferenceConfig.init():服务引用的入口

ReferenceConfig.init() 的核心流程:

graph TD A[ReferenceConfig.init] --> B[检查配置合法性] B --> C[组装 URL] C --> D[创建代理对象] D --> E[Protocol.refer] E --> F[RegistryProtocol.refer] F --> G[向注册中心订阅地址] G --> H[收到地址列表] H --> I[为每个地址创建 DubboInvoker] I --> J[Cluster.join 合并成单个 Invoker] J --> K[ProxyFactory.getProxy 生成代理]

读图导引:从下往上看关键节点。Protocol.refer 先走 RegistryProtocol 订阅地址,收到地址后创建多个 DubboInvoker,然后 Cluster.join 合并成一个(对用户透明),最后 ProxyFactory 生成代理对象。注意 I→J→K 的顺序:先有多个 Invoker,再有 Cluster 合并,最后生成代理。

2.3 RegistryProtocol.refer:订阅服务地址

java 复制代码
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    // 1. 组装注册中心 URL
    url = getRegistryUrl(url);
    
    // 2. 获取注册中心实例
    Registry registry = registryFactory.getRegistry(url);
    
    // 3. 订阅服务地址
    if (RegistryService.class.equals(type)) {
        return cluster.join(new StaticDirectory(registryUrl, invoker));
    }
    
    // 4. 组装目录并订阅
    Directory<T> directory = new RegistryDirectory<T>(type, url);
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    directory.subscribe(toSubscribeUrl(subscribeUrl));
    
    // 5. 通过 Cluster 合并多个 Invoker
    Invoker<T> invoker = cluster.join(directory);
    
    return invoker;
}

RegistryDirectory 是核心组件:

  • 它向注册中心订阅服务地址列表。
  • 当地址列表变化(Provider 上线/下线),注册中心推送通知,RegistryDirectory 刷新本地的 Invoker 列表。
  • 它实现了 Directory 接口,对外提供 list(Invocation) 方法返回当前可用的 Invoker 列表。

2.4 为每个 Provider 地址创建 DubboInvoker

RegistryDirectory 收到地址列表 [nacos://192.168.1.100:20880, nacos://192.168.1.101:20880] 时,会为每个地址创建一个 DubboInvoker

java 复制代码
// DubboProtocol.refer 简化版
public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
    // 1. 优化序列化
    optimizeSerialization(url);
    
    // 2. 创建 DubboInvoker
    DubboInvoker<T> invoker = new DubboInvoker<>(serviceType, url, getClients(url), invokers);
    invokers.add(invoker);
    
    return invoker;
}

getClients(url) 创建 Netty 客户端连接:

java 复制代码
private ExchangeClient[] getClients(URL url) {
    int connections = url.getParameter(CONNECTIONS_KEY, 0);
    if (connections == 0) {
        // 共享连接:一个客户端共享给多个服务
        return new ExchangeClient[]{initClient(url)};
    } else {
        // 独立连接:每个服务独享连接
        ExchangeClient[] clients = new ExchangeClient[connections];
        for (int i = 0; i < clients.length; i++) {
            clients[i] = initClient(url);
        }
        return clients;
    }
}

initClient 创建 HeaderExchangeClient,内部封装 NettyClient。NettyClient 初始化 Netty 的 Bootstrap,配置 NioEventLoopGroup,并绑定 DubboCodec

2.5 Cluster.join:将多个 Invoker 合并成一个

消费者拿到的是一个统一的 Invoker,而不是多个。这个统一感是通过 Cluster 实现的。

java 复制代码
// FailoverCluster 默认实现
public class FailoverCluster implements Cluster {
    public final static String NAME = "failover";
    
    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return new FailoverClusterInvoker<>(directory);
    }
}

FailoverClusterInvoker 包装了 Directory,在 invoke 方法中实现失败重试逻辑:

java 复制代码
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, 
                        LoadBalance loadbalance) throws RpcException {
    String methodName = invocation.getMethodName();
    // 获取重试次数,默认 2 次(加上第一次共 3 次)
    int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
    
    List<Invoker<T>> copyInvokers = invokers;
    for (int i = 0; i < len; i++) {
        // 通过 LoadBalance 选择一个 Invoker
        Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
        try {
            Result result = invoker.invoke(invocation);
            return result;
        } catch (RpcException e) {
            if (e.isBiz()) throw e;
            // 记录异常,继续重试
            if (i == len - 1) throw e;
        }
    }
    throw new RpcException("Failover failed");
}

2.6 ProxyFactory.getProxy:生成消费者代理

最后,ReferenceConfig 调用 proxyFactory.getProxy(invoker, interfaces) 生成代理对象:

java 复制代码
// JavassistProxyFactory
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
    return Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}

生成的代理类实现 UserService 接口,所有方法调用都委托给 InvokerInvocationHandler

java 复制代码
public class InvokerInvocationHandler implements InvocationHandler {
    private final Invoker<?> invoker;
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        
        // 创建 RpcInvocation
        RpcInvocation invocation = new RpcInvocation(method, args);
        invocation.setAttachment("interface", invoker.getInterface().getName());
        
        // 调用 Invoker
        return invoker.invoke(invocation).recreate();
    }
}

2.7 服务引用总结

sequenceDiagram participant RB as ReferenceBean participant RC as ReferenceConfig participant RP as RegistryProtocol participant RD as RegistryDirectory participant Reg as Registry participant DP as DubboProtocol participant NC as NettyClient participant CL as Cluster participant PF as ProxyFactory RB->>RC: get() RC->>RP: refer(interface, url) RP->>RD: new RegistryDirectory RP->>RD: subscribe(url) RD->>Reg: subscribe(interface) Reg-->>RD: notify(urlList) loop 每个 Provider 地址 RD->>DP: refer(interface, providerUrl) DP->>NC: initClient(url) NC-->>DP: NettyClient DP-->>RD: DubboInvoker end RP->>CL: join(directory) CL-->>RP: FailoverClusterInvoker RP-->>RC: ClusterInvoker RC->>PF: getProxy(invoker, interfaces) PF-->>RC: UserService 代理对象

读图导引:关注并发创建 NettyClient 的循环。RegistryDirectory 收到地址列表后,为每个地址并发创建 DubboInvoker,每个 DubboInvoker 内部持有 NettyClient。Cluster.join 将这些 Invoker 合并成一个对用户透明的 FailoverClusterInvoker。最后的代理对象持有的是 ClusterInvoker,不是底层的 DubboInvoker。


第三层:一次 RPC 调用的完整链路

现在,当你调用 userService.getAddress(userId) 时,到底发生了什么?

3.1 调用链路全景

graph LR A[Consumer 代码<br/>userService.getAddress] --> B[Proxy<br/>InvokerInvocationHandler] B --> C[Cluster Invoker<br/>FailoverClusterInvoker] C --> D[Directory<br/>RegistryDirectory] D --> E[Router<br/>ConditionRouter/TagRouter] E --> F[LoadBalance<br/>RandomLoadBalance] F --> G[Filter<br/>Consumer ContextFilter] G --> H[Client<br/>NettyClient] H -->|网络传输| I[Server<br/>NettyServer] I --> J[Filter<br/>Provider ContextFilter] J --> K[Invoker<br/>AbstractProxyInvoker] K --> L[Wrapper<br/>Javassist 生成] L --> M[实现类<br/>UserServiceImpl]

读图导引:从左到右是一次请求的完整链路。上半段(A-G)在 Consumer 端,中间 H-I 是网络传输,下半段(J-M)在 Provider 端。注意 C 节点的 Cluster Invoker 包含重试逻辑,如果 F 选中的节点调用失败,C 会重新选择另一个节点重试。

3.2 Consumer 端:从代理到网络发送

Step 1:InvokerInvocationHandler

代理对象的方法调用进入 InvokerInvocationHandler.invoke,创建 RpcInvocation

java 复制代码
RpcInvocation invocation = new RpcInvocation(method, interfaceName, parameterTypes, arguments);
invocation.setAttachment(PATH_KEY, getUrl().getPath());
invocation.setAttachment(VERSION_KEY, version);

Step 2:FailoverClusterInvoker

FailoverClusterInvoker.doInvoke 开始执行:

java 复制代码
// 1. 从 Directory 获取可用 Invoker 列表
List<Invoker<T>> invokers = list(invocation);
// 2. 选择 LoadBalance(默认 RandomLoadBalance)
LoadBalance loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class)
    .getExtension(invokers.get(0).getUrl().getMethodParameter(methodName, LOADBALANCE_KEY, DEFAULT_LOAD_BALANCE));

Step 3:Directory.list

RegistryDirectory.list(invocation) 返回当前可用的 Invoker 列表。它内部会调用 Router 进行路由过滤:

java 复制代码
public List<Invoker<T>> list(Invocation invocation) throws RpcException {
    List<Invoker<T>> invokers = doList(invocation);  // 获取缓存的 Invoker 列表
    // Router 过滤
    List<Router> localRouters = this.routers;
    if (localRouters != null && !localRouters.isEmpty()) {
        for (Router router : localRouters) {
            invokers = router.route(invokers, getConsumerUrl(), invocation);
        }
    }
    return invokers;
}

Step 4:LoadBalance.select

默认 RandomLoadBalance(加权随机):

java 复制代码
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    int length = invokers.size();
    boolean sameWeight = true;
    int[] weights = new int[length];
    int firstWeight = getWeight(invokers.get(0), invocation);
    weights[0] = firstWeight;
    int totalWeight = firstWeight;
    
    for (int i = 1; i < length; i++) {
        int weight = getWeight(invokers.get(i), invocation);
        weights[i] = weight;
        totalWeight += weight;
        if (sameWeight && weight != firstWeight) {
            sameWeight = false;
        }
    }
    
    if (totalWeight > 0 && !sameWeight) {
        // 按权重随机
        int offset = ThreadLocalRandom.current().nextInt(totalWeight);
        for (int i = 0; i < length; i++) {
            offset -= weights[i];
            if (offset < 0) {
                return invokers.get(i);
            }
        }
    }
    // 权重相同,普通随机
    return invokers.get(ThreadLocalRandom.current().nextInt(length));
}

getWeight 方法会根据 Provider 的预热时间动态调整权重。新启动的节点权重会缓慢增加,避免冷启动时流量过大。

Step 5:Filter 链

Dubbo 的 Filter 采用责任链模式,Consumer 端的 Filter 链包括:

复制代码
ConsumerContextFilter → FutureFilter → MonitorFilter → ... → DubboInvoker

每个 Filter 在 invoke 方法中执行前置逻辑,然后调用下一个 Filter,最后在 onResponse 中执行后置逻辑。

java 复制代码
@Activate(group = CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // 设置当前 Consumer 上下文
        RpcContext.setAttachment(APPLICATION_KEY, invoker.getUrl().getParameter(APPLICATION_KEY));
        
        // 调用下一个 Filter(或最终 Invoker)
        Result result = invoker.invoke(invocation);
        
        // 清理上下文
        RpcContext.removeContext();
        return result;
    }
}

Step 6:DubboInvoker.invoke

最终到达 DubboInvoker

java 复制代码
protected Result doInvoke(final Invocation invocation) throws Throwable {
    RpcInvocation inv = (RpcInvocation) invocation;
    final String methodName = inv.getMethodName();
    
    // 选择 ExchangeClient(NettyClient 的包装)
    ExchangeClient currentClient;
    if (clients.length == 1) {
        currentClient = clients[0];
    } else {
        currentClient = clients[index.getAndIncrement() % clients.length];
    }
    
    // 判断是否需要返回值
    boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
    int timeout = calculateTimeout(invocation, methodName);
    
    if (isOneway) {
        // 单向调用:不等待返回
        currentClient.send(inv, true);
        return AsyncRpcResult.newDefaultAsyncResult(invocation);
    } else {
        // 异步调用
        AsyncRpcResult asyncRpcResult = new AsyncRpcResult(inv);
        CompletableFuture<Object> responseFuture = currentClient.request(inv, timeout);
        asyncRpcResult.subscribeTo(responseFuture);
        return asyncRpcResult;
    }
}

currentClient.request 将请求交给 HeaderExchangeChannel,最终通过 Netty 发送到 Provider。

3.3 Provider 端:从网络接收到方法执行

Step 1:NettyServer 接收请求

Netty 的 ChannelHandler 收到数据后,交给 DubboCodec 解码:

java 复制代码
// DubboCodec.decodeBody 简化版
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
    byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
    Serialization s = CodecSupport.getSerialization(channel.getUrl(), proto);
    
    long id = Bytes.bytes2long(header, 4);
    if ((flag & FLAG_REQUEST) == 0) {
        // 响应解码
        // ...
    } else {
        // 请求解码
        Request req = new Request(id);
        req.setVersion(Version.getProtocolVersion());
        req.setTwoWay((flag & FLAG_TWOWAY) != 0);
        
        ObjectInput in = s.deserialize(channel.getUrl(), is);
        req.setData(decodeInvocation(channel, in));
        return req;
    }
}

解码出的 Invocation 被交给 HeaderExchangeHandler

java 复制代码
public void received(Channel channel, Object message) throws RemotingException {
    if (message instanceof Request) {
        Request request = (Request) message;
        if (request.isTwoWay()) {
            // 处理请求并发送响应
            Response response = handleRequest(exchangeChannel, request);
            channel.send(response);
        } else {
            // 单向调用
            handler.received(exchangeChannel, request.getData());
        }
    }
}

Step 2:DubboProtocol 处理请求

DubboProtocolrequestHandler 收到 Invocation

java 复制代码
private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
    public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) {
        Invocation invocation = (Invocation) message;
        
        // 1. 从 exporterMap 找到对应的 Exporter
        Invoker<?> invoker = getInvoker(channel, invocation);
        
        // 2. 设置远程地址到 RpcContext
        RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
        
        // 3. 调用 Invoker
        Result result = invoker.invoke(invocation);
        return result.getResponseFuture();
    }
};

getInvoker 根据 Invocation 中的接口名、版本号、端口,从 exporterMap 中找到对应的 DubboExporter,取出其持有的 Invoker。

Step 3:Provider Filter 链

Provider 端的 Filter 链包括:

复制代码
ContextFilter → ExceptionFilter → TimeoutFilter → MonitorFilter → ... → AbstractProxyInvoker

ContextFilter 负责将 Invocation 中的附件设置到当前线程的 RpcContext

java 复制代码
@Activate(group = PROVIDER, order = -10000)
public class ContextFilter implements Filter {
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        Map<String, Object> attachments = invocation.getObjectAttachments();
        if (attachments != null) {
            RpcContext.getContext().setObjectAttachments(attachments);
        }
        return invoker.invoke(invocation);
    }
}

Step 4:AbstractProxyInvoker → Wrapper → 实现类

最终到达 AbstractProxyInvoker

java 复制代码
public Result invoke(Invocation invocation) throws RpcException {
    try {
        Object value = doInvoke(proxy, invocation.getMethodName(), 
                                invocation.getParameterTypes(), invocation.getArguments());
        CompletableFuture<Object> future = wrapWithFuture(value);
        return new AsyncRpcResult(future, invocation);
    } catch (InvocationTargetException e) {
        return AsyncRpcResult.newDefaultAsyncResult(e.getTargetException(), invocation);
    } catch (Throwable e) {
        throw new RpcException(e);
    }
}

doInvoke 调用 Javassist 生成的 Wrapper.invokeMethod,最终执行到 UserServiceImpl.getAddress

3.4 响应返回

Provider 执行完方法后,结果沿 Filter 链返回,经过 DubboCodec 编码,通过 Netty 发回 Consumer。Consumer 端的 NettyClient 收到响应后,HeaderExchangeHandler 将响应与之前的请求关联(通过 requestId),唤醒等待的 CompletableFuture,最终 AsyncRpcResult 将结果返回给调用方。


第四层:Dubbo SPI 扩展机制

面试中最容易深挖的点之一是:"Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?"

4.1 JDK SPI 的痛点

JDK 的 SPI 通过 ServiceLoader 加载实现:

java 复制代码
ServiceLoader<Protocol> loader = ServiceLoader.load(Protocol.class);
for (Protocol protocol : loader) {
    // 一次性加载所有实现
}

JDK SPI 有两个明显缺陷:

  1. 一次性加载所有实现:即使只用其中一个,也会把所有实现类都实例化。
  2. 无法按名称选择:遍历获取,不能根据配置直接拿到指定实现。

4.2 Dubbo SPI 的核心设计

Dubbo 的 SPI 配置文件放在 META-INF/dubbo/ 目录下,格式是 Key-Value 对:

复制代码
# META-INF/dubbo/org.apache.dubbo.rpc.Protocol
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
http=org.apache.dubbo.rpc.protocol.http.HttpProtocol
hessian=org.apache.dubbo.rpc.protocol.hessian.HessianProtocol
# ...

使用时通过名称直接获取:

java 复制代码
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("dubbo");

ExtensionLoader 是 Dubbo SPI 的核心引擎,它实现了三个能力:

能力一:按需加载

java 复制代码
public T getExtension(String name) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    // 从缓存获取 Holder
    final Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 按需创建
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

能力二:自适应扩展(@Adaptive)

@Adaptive 注解标记在类或方法上,表示这是一个适配器。Dubbo 会动态生成适配器类。

Protocol 接口为例:

java 复制代码
@SPI("dubbo")
public interface Protocol {
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}

Dubbo 会为 Protocol 生成一个 Protocol$Adaptive 类:

java 复制代码
public class Protocol$Adaptive implements Protocol {
    public <T> Exporter<T> export(Invoker<T> arg0) throws RpcException {
        if (arg0 == null) throw new IllegalArgumentException();
        URL url = arg0.getUrl();
        // 从 URL 中获取协议名称
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null) throw new IllegalStateException();
        // 通过 ExtensionLoader 获取对应实现
        Protocol extension = ExtensionLoader.getExtensionLoader(Protocol.class)
            .getExtension(extName);
        return extension.export(arg0);
    }
    
    public <T> Invoker<T> refer(Class<T> arg0, URL arg1) throws RpcException {
        if (arg1 == null) throw new IllegalArgumentException();
        URL url = arg1;
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        Protocol extension = ExtensionLoader.getExtensionLoader(Protocol.class)
            .getExtension(extName);
        return extension.refer(arg0, arg1);
    }
}

关键点:适配器类不是直接做业务,而是根据 URL 中的参数决定委托给哪个具体实现。这使得 Dubbo 可以在运行时根据配置切换实现,而不需要硬编码。

比如 URL 是 registry://...,适配器就会委托给 RegistryProtocol;URL 是 dubbo://...,就委托给 DubboProtocol

能力三:AOP 包装(@Wrapper)

Dubbo 支持通过 @Wrapper 注解对扩展点进行 AOP 包装。比如 ProtocolFilterWrapper

java 复制代码
public class ProtocolFilterWrapper implements Protocol {
    private final Protocol protocol;
    
    public ProtocolFilterWrapper(Protocol protocol) {
        this.protocol = protocol;
    }
    
    public <T> Exporter<T> export(Invoker<T> invoker) {
        // 先构建 Filter 链,再调用底层 Protocol
        return protocol.export(buildInvokerChain(invoker, SERVICE_FILTER_KEY, PROVIDER));
    }
}

META-INF/dubbo/org.apache.dubbo.rpc.Protocol 中:

复制代码
filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

当通过 ExtensionLoader.getExtension("dubbo") 获取 Protocol 时,Dubbo 会自动将 DubboProtocol 包装在 ProtocolFilterWrapperProtocolListenerWrapper 中,形成装饰者链

复制代码
ProtocolListenerWrapper(ProtocolFilterWrapper(DubboProtocol))

这种设计非常巧妙:Filter 机制的介入对上层完全透明,通过配置文件即可扩展。

4.3 SPI 扩展机制总结

graph TD A[ExtensionLoader] --> B[加载配置<br/>META-INF/dubbo/] B --> C[解析 Key-Value] C --> D[创建扩展实例] D --> E{是否有 @Wrapper} E -->|是| F[包装成装饰者链] E -->|否| G[直接返回] F --> H[ProtocolFilterWrapper] H --> I[ProtocolListenerWrapper] I --> J[具体实现<br/>DubboProtocol] A --> K[生成 @Adaptive 适配器] K --> L[Protocol$Adaptive] L --> M[根据 URL 动态路由]

读图导引:关注两条主线。左线展示普通扩展的加载和包装流程,@Wrapper 会形成装饰者链。右线展示 @Adaptive 适配器的生成,适配器根据 URL 参数动态决定委托给哪个实现。两条线的结合点是 ExtensionLoader——它是整个 SPI 机制的中枢。


第五层:负载均衡策略与集群容错

5.1 负载均衡策略

Dubbo 提供四种负载均衡策略,都是 LoadBalance 接口的实现:

策略 名称 特点 适用场景
RandomLoadBalance 加权随机 默认策略,按权重随机选择 大多数场景
RoundRobinLoadBalance 加权轮询 按权重轮询,平滑加权 需要均匀分配
LeastActiveLoadBalance 最少活跃调用 选择当前活跃调用数最少的 长请求场景
ConsistentHashLoadBalance 一致性哈希 相同参数的请求路由到同一节点 缓存场景

RandomLoadBalance(默认):

前面已经展示了源码。核心是按权重随机,权重默认相同。如果配置了 weight=100weight=200,前者获得 1/3 流量,后者获得 2/3。

LeastActiveLoadBalance

java 复制代码
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    int length = invokers.size();
    int leastActive = -1;
    int leastCount = 0;
    int[] leastIndexes = new int[length];
    int[] weights = new int[length];
    
    for (int i = 0; i < length; i++) {
        Invoker<T> invoker = invokers.get(i);
        // 获取活跃调用数
        int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
        int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);
        
        if (leastActive == -1 || active < leastActive) {
            leastActive = active;
            leastCount = 1;
            leastIndexes[0] = i;
            weights[0] = weight;
        } else if (active == leastActive) {
            leastIndexes[leastCount] = i;
            weights[leastCount] = weight;
            leastCount++;
        }
    }
    
    if (leastCount == 1) {
        return invokers.get(leastIndexes[0]);
    }
    // 多个相同最少活跃的,按权重随机
    return randomSelectByWeight(invokers, leastIndexes, weights, leastCount);
}

ConsistentHashLoadBalance

使用一致性哈希环,将请求参数(默认是第一个参数)做哈希,映射到环上的某个节点。相同参数的请求总是路由到同一个 Provider,适合有本地缓存的场景。

java 复制代码
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    String methodName = RpcUtils.getMethodName(invocation);
    String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
    
    // 获取请求参数
    Object[] args = invocation.getArguments();
    // 构建一致性哈希选择器
    ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
    if (selector == null || selector.identityHashCode != identityHashCode) {
        selectors.put(key, new ConsistentHashSelector<>(invokers, methodName, identityHashCode));
        selector = (ConsistentHashSelector<T>) selectors.get(key);
    }
    return selector.select(invocation);
}

5.2 集群容错模式

Cluster 是 Dubbo 对集群容错的抽象,将多个 Provider 的 Invoker 合并成一个高可用的 Invoker。

模式 名称 行为 适用场景
FailoverCluster 失败重试 失败时重试其他节点,默认重试 2 次 读操作
FailfastCluster 快速失败 只调用一次,失败立即抛异常 写操作
FailsafeCluster 失败安全 失败返回空结果,不抛异常 日志/监控
FailbackCluster 失败自动恢复 失败记录到队列,定时重试 异步通知
ForkingCluster 并行调用 并行调用多个节点,返回最快结果 实时性要求高
BroadcastCluster 广播调用 逐个调用所有节点 通知所有提供者

FailoverCluster(默认):

前面已展示源码。默认重试 2 次(共 3 次调用)。

面试追问:如果下游已经挂了,重试会不会放大故障?。这就是"重试风暴"问题。生产环境中,读操作可以用 Failover,写操作应该用 Failfast 避免重复写入。

ForkingCluster

java 复制代码
public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, 
                        LoadBalance loadbalance) throws RpcException {
    // 并行调用 forks 个节点
    int forks = getUrl().getParameter(FORKS_KEY, DEFAULT_FORKS);
    int timeout = getUrl().getParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
    
    final List<Invoker<T>> selected = new ArrayList<>();
    for (int i = 0; i < forks; i++) {
        Invoker<T> invoker = select(loadbalance, invocation, invokers, selected);
        selected.add(invoker);
    }
    
    RpcContext.getContext().setForks(true);
    try {
        return invokeWithForks(selected, invocation, timeout);
    } finally {
        RpcContext.getContext().setForks(false);
    }
}

第六层:Dubbo 3.x 的变革

6.1 应用级服务发现

Dubbo 2.x 采用接口级服务发现:每个接口在注册中心注册一条记录。

复制代码
/com.example.UserService/providers/dubbo://192.168.1.100:20880
/com.example.OrderService/providers/dubbo://192.168.1.100:20880

如果一台机器暴露 100 个接口,注册中心就要存储 100 条记录。当集群规模扩大到几千台机器、几万个接口时,注册中心的压力非常大。

Dubbo 3.x 引入应用级服务发现:注册中心只存储应用级别的地址映射。

复制代码
/provider-app/instances/192.168.1.100:20880

消费者想知道 UserService 的地址,先从注册中心拿到 provider-app 的实例列表,再通过元数据中心(如 Nacos Config、ZooKeeper)获取该应用暴露的接口元数据。

graph LR subgraph "Dubbo 2.x 接口级" A1[Consumer] -->|订阅| R1[注册中心] R1 -->|推送 1000 条接口地址| A1 end subgraph "Dubbo 3.x 应用级" A2[Consumer] -->|订阅应用地址| R2[注册中心] R2 -->|推送 10 条应用实例| A2 A2 -->|拉取接口元数据| M[元数据中心] end

读图导引:对比左右两个架构。Dubbo 2.x 的注册中心推送大量接口级地址,注册中心压力大。Dubbo 3.x 将注册中心减负为只推送应用实例,接口元数据通过元数据中心按需拉取。这种"推轻拉重"的设计显著降低了注册中心的网络压力和内存占用。

6.2 Triple 协议

Dubbo 3.x 引入 Triple 协议,基于 HTTP/2 和 gRPC 规范:

  • 兼容 gRPC:可以与 gRPC 服务直接互通。
  • 支持流式调用:Server Stream、Client Stream、Bi-directional Stream。
  • 更好的云原生支持:基于 HTTP/2,更容易穿透防火墙和负载均衡器。
protobuf 复制代码
service UserService {
    rpc GetAddress(GetAddressRequest) returns (GetAddressResponse);
    rpc ListAddresses(ListRequest) returns (stream Address);
}

实战/源码

实战 1:自定义 Filter 实现调用日志

java 复制代码
@Activate(group = {CommonConstants.CONSUMER, CommonConstants.PROVIDER})
public class LoggingFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
    
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        long start = System.currentTimeMillis();
        String interfaceName = invoker.getInterface().getName();
        String methodName = invocation.getMethodName();
        
        logger.info("[Dubbo] 请求开始: {}.{}, 参数: {}", 
            interfaceName, methodName, Arrays.toString(invocation.getArguments()));
        
        try {
            Result result = invoker.invoke(invocation);
            long cost = System.currentTimeMillis() - start;
            logger.info("[Dubbo] 请求结束: {}.{}, 耗时: {}ms, 结果: {}",
                interfaceName, methodName, cost, 
                result.hasException() ? "异常: " + result.getException() : result.getValue());
            return result;
        } catch (Exception e) {
            long cost = System.currentTimeMillis() - start;
            logger.error("[Dubbo] 请求异常: {}.{}, 耗时: {}ms", 
                interfaceName, methodName, cost, e);
            throw e;
        }
    }
}

配置 META-INF/dubbo/org.apache.dubbo.rpc.Filter

复制代码
logging=com.example.LoggingFilter

然后在 @DubboService@DubboReference 中指定:

java 复制代码
@DubboService(filter = "logging")
public class UserServiceImpl implements UserService { }

实战 2:自定义 LoadBalance 实现灰度发布

java 复制代码
public class GrayLoadBalance implements LoadBalance {
    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // 从 RpcContext 获取灰度标记
        String grayTag = RpcContext.getContext().getAttachment("gray");
        
        if ("true".equals(grayTag)) {
            // 优先选择带 gray 标签的节点
            List<Invoker<T>> grayInvokers = invokers.stream()
                .filter(inv -> "true".equals(inv.getUrl().getParameter("gray")))
                .collect(Collectors.toList());
            if (!grayInvokers.isEmpty()) {
                return grayInvokers.get(ThreadLocalRandom.current().nextInt(grayInvokers.size()));
            }
        }
        
        // 默认随机
        return invokers.get(ThreadLocalRandom.current().nextInt(invokers.size()));
    }
}

配置 META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance

复制代码
gray=com.example.GrayLoadBalance

使用:

java 复制代码
@DubboReference(loadbalance = "gray")
private UserService userService;

实战 3:CompletableFuture 异步调用

Dubbo 支持返回 CompletableFuture 的异步调用:

java 复制代码
public interface UserService {
    CompletableFuture<Address> getAddressAsync(Long userId);
}

@Service
public class UserServiceImpl implements UserService {
    @Override
    public CompletableFuture<Address> getAddressAsync(Long userId) {
        return CompletableFuture.supplyAsync(() -> {
            // 异步查询数据库
            return addressDao.findByUserId(userId);
        });
    }
}

Consumer 端:

java 复制代码
@DubboReference
private UserService userService;

public void createOrder(Long userId) {
    CompletableFuture<Address> future = userService.getAddressAsync(userId);
    future.thenAccept(address -> {
        // 异步处理结果
        System.out.println("地址: " + address);
    });
}

常见问题

Q1:Dubbo 和 OpenFeign 有什么区别?

维度 Dubbo OpenFeign
协议 自定义二进制协议(Dubbo/Triple)或 HTTP HTTP REST
序列化 Hessian2、Protobuf、JSON 通常是 JSON
传输层 Netty(TCP) HTTP 客户端(如 OkHttp)
服务发现 内置,支持多注册中心 依赖 Spring Cloud Discovery
负载均衡 内置多种策略 依赖 Ribbon/Spring Cloud LoadBalancer
容错机制 内置多种 Cluster 策略 依赖 Hystrix/Resilience4j
性能 更高(二进制协议 + TCP) 较低(HTTP + JSON)
生态 更适合 Java 生态,Dubbo 3.x 支持多语言 语言无关,通用性更强

一句话总结:Dubbo 是专用的 RPC 框架,追求极致性能和治理能力;OpenFeign 是声明式 HTTP 客户端,追求简单和通用性。国内微服务场景通常 Dubbo 为主,跨语言场景可能选 Feign + gRPC。

Q2:Dubbo 的注册中心挂了,服务还能调用吗?

,但有条件。

Dubbo 的 Consumer 会缓存从注册中心获取的 Provider 地址列表。如果注册中心挂了,Consumer 仍然可以使用本地缓存的地址列表继续调用。

但有两个限制:

  1. 新节点无法发现:如果有新 Provider 启动,Consumer 无法感知。
  2. 故障节点无法剔除:如果某个 Provider 节点挂了,Consumer 仍然会向它发请求,直到调用失败触发重试。

生产环境建议:

  • 注册中心做高可用部署(如 Nacos 三节点集群)。
  • 配合 Dubbo 的直连提供者url="dubbo://ip:port")做兜底。
  • 开启 Provider 的心跳检测,Consumer 端及时剔除不可用的 Invoker。

Q3:为什么 Dubbo 用 Javassist 而不是 JDK 动态代理?

Javassist 相比 JDK 动态代理有两个优势:

  1. 性能更好:Javassist 生成的是直接调用目标方法的字节码(w.sayHello(args)),JDK 动态代理通过反射调用(method.invoke(target, args))。反射有性能开销,高 QPS 场景差距明显。

  2. 不依赖接口:JDK 动态代理要求目标类实现接口,Javassist 可以直接对类做字节码增强。

但 Javassist 也有缺点:

  • 启动时生成字节码有耗时,首次调用可能稍慢。
  • 生成的类占用 PermGen/Metaspace,大量服务可能导致内存压力。

Dubbo 默认使用 Javassist,但也支持 JDK 代理(配置 proxy="jdk")。

Q4:Dubbo 的线程模型是怎样的?

Dubbo 的 Provider 端使用线程池模型处理请求:

graph TD A[Netty IO 线程<br/>NioEventLoop] -->|解码| B[请求队列] B --> C[业务线程池<br/>ThreadPool] C --> D[执行方法] D --> E[结果队列] E --> F[Netty IO 线程<br/>编码发送]

读图导引:注意 Netty IO 线程和业务线程池的分工。IO 线程只负责网络读写和编解码,不执行业务逻辑。业务逻辑交给独立的线程池,避免阻塞 IO 线程。这是 Dubbo 能支持高并发的基础设计。

默认配置:

  • IO 线程数:CPU 核数 + 1
  • 业务线程池:固定大小 200 线程(fixed 类型),队列长度 0(直接拒绝策略)

可以通过配置调整:

xml 复制代码
<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="500" queues="100" />

dispatcher 参数控制请求分发策略:

  • all:所有消息(请求、响应、连接、断开)都派发到线程池(默认)。
  • direct:所有消息直接在 IO 线程处理,不派发到线程池。
  • message:只有请求和响应派发到线程池。
  • execution:只有请求派发到线程池。
  • connection:连接和断开事件派发到线程池。

Q5:Dubbo 3.x 的应用级服务发现怎么迁移?

Dubbo 3.x 默认启用应用级服务发现,但兼容接口级。迁移路径:

  1. 双注册阶段:Provider 同时注册接口级和应用级地址。

    yaml 复制代码
    dubbo:
      application:
        register-mode: all  # interface, instance, all
  2. Consumer 升级:Consumer 升级到 Dubbo 3.x,优先消费应用级地址。

  3. Provider 切流:确认所有 Consumer 都支持应用级后,Provider 关闭接口级注册。

    yaml 复制代码
    dubbo:
      application:
        register-mode: instance

总结

本文从 @DubboReference 的魔法出发,逐层穿透 Dubbo 的核心实现:

  1. 服务暴露ServiceConfig 解析配置,通过 ProxyFactory 将实现类包装成 AbstractProxyInvokerRegistryProtocol 先调用 DubboProtocol 打开 NettyServer,再向注册中心注册地址。

  2. 服务引用ReferenceConfig 通过 RegistryProtocol 向注册中心订阅地址,RegistryDirectory 维护地址列表,为每个地址创建 DubboInvokerCluster.join 合并成统一的 FailoverClusterInvoker,最后 ProxyFactory 生成代理对象。

  3. 调用链路:代理对象 → InvokerInvocationHandlerClusterInvokerDirectory.listRouter.routeLoadBalance.selectFilter 链 → DubboInvokerNettyClient → 网络传输 → NettyServerDubboProtocolFilter 链 → AbstractProxyInvokerWrapper → 实现类。

  4. SPI 机制ExtensionLoader 实现按需加载、自适应扩展(@Adaptive 动态生成适配器类)和 AOP 包装(@Wrapper 实现装饰者链),这是 Dubbo 高度可扩展的根基。

  5. 集群容错:默认 FailoverCluster 失败重试 2 次,存在重试风暴风险。读操作用 Failover,写操作用 Failfast

  6. Dubbo 3.x 变革:应用级服务发现将注册中心从接口级地址推送减负为应用实例推送,配合 Triple 协议实现与 gRPC 生态互通。