问题引入
面试官递过来一杯水,轻描淡写地问:"你们服务间怎么通信?"
你答:"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 响应。这种方式有几个致命问题:
- 调用方强耦合服务地址:
http://192.168.1.100:8080/user/getAddress——IP 变了怎么办?扩容了新节点怎么发现? - 没有负载均衡:所有流量打到一台机器,另外两台空转。
- 故障无感知:下游节点挂了,调用方还在发请求,超时后才能知道。
- 没有统一的治理手段:限流、降级、监控——每个服务自己造轮子。
Dubbo 的诞生就是为了填平这个鸿沟:让远程调用像本地调用一样简单,同时内置服务治理的全套能力。但"像本地调用一样简单"这个承诺背后,隐藏着一个极其复杂的工程实现。
本文从一次 @DubboReference 的方法调用出发,逐层剥开 Dubbo 的核心架构与调用链路。
核心概念
Dubbo 核心架构:五大角色
Dubbo 的架构可以用一张经典图概括:
读图导引:关注箭头方向。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() 是服务暴露的入口,其核心流程如下:
读图导引:从左到右跟随时间线。关键点在 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,配置 bossGroup 和 workerGroup,并绑定 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 服务暴露总结
读图导引:关注时序。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() 的核心流程:
读图导引:从下往上看关键节点。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 服务引用总结
读图导引:关注并发创建 NettyClient 的循环。RegistryDirectory 收到地址列表后,为每个地址并发创建 DubboInvoker,每个 DubboInvoker 内部持有 NettyClient。Cluster.join 将这些 Invoker 合并成一个对用户透明的 FailoverClusterInvoker。最后的代理对象持有的是 ClusterInvoker,不是底层的 DubboInvoker。
第三层:一次 RPC 调用的完整链路
现在,当你调用 userService.getAddress(userId) 时,到底发生了什么?
3.1 调用链路全景
读图导引:从左到右是一次请求的完整链路。上半段(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 处理请求
DubboProtocol 的 requestHandler 收到 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 有两个明显缺陷:
- 一次性加载所有实现:即使只用其中一个,也会把所有实现类都实例化。
- 无法按名称选择:遍历获取,不能根据配置直接拿到指定实现。
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 包装在 ProtocolFilterWrapper 和 ProtocolListenerWrapper 中,形成装饰者链:
ProtocolListenerWrapper(ProtocolFilterWrapper(DubboProtocol))
这种设计非常巧妙:Filter 机制的介入对上层完全透明,通过配置文件即可扩展。
4.3 SPI 扩展机制总结
读图导引:关注两条主线。左线展示普通扩展的加载和包装流程,@Wrapper 会形成装饰者链。右线展示 @Adaptive 适配器的生成,适配器根据 URL 参数动态决定委托给哪个实现。两条线的结合点是 ExtensionLoader——它是整个 SPI 机制的中枢。
第五层:负载均衡策略与集群容错
5.1 负载均衡策略
Dubbo 提供四种负载均衡策略,都是 LoadBalance 接口的实现:
| 策略 | 名称 | 特点 | 适用场景 |
|---|---|---|---|
| RandomLoadBalance | 加权随机 | 默认策略,按权重随机选择 | 大多数场景 |
| RoundRobinLoadBalance | 加权轮询 | 按权重轮询,平滑加权 | 需要均匀分配 |
| LeastActiveLoadBalance | 最少活跃调用 | 选择当前活跃调用数最少的 | 长请求场景 |
| ConsistentHashLoadBalance | 一致性哈希 | 相同参数的请求路由到同一节点 | 缓存场景 |
RandomLoadBalance(默认):
前面已经展示了源码。核心是按权重随机,权重默认相同。如果配置了 weight=100 和 weight=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)获取该应用暴露的接口元数据。
读图导引:对比左右两个架构。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 仍然可以使用本地缓存的地址列表继续调用。
但有两个限制:
- 新节点无法发现:如果有新 Provider 启动,Consumer 无法感知。
- 故障节点无法剔除:如果某个 Provider 节点挂了,Consumer 仍然会向它发请求,直到调用失败触发重试。
生产环境建议:
- 注册中心做高可用部署(如 Nacos 三节点集群)。
- 配合 Dubbo 的直连提供者(
url="dubbo://ip:port")做兜底。 - 开启 Provider 的心跳检测,Consumer 端及时剔除不可用的 Invoker。
Q3:为什么 Dubbo 用 Javassist 而不是 JDK 动态代理?
Javassist 相比 JDK 动态代理有两个优势:
-
性能更好:Javassist 生成的是直接调用目标方法的字节码(
w.sayHello(args)),JDK 动态代理通过反射调用(method.invoke(target, args))。反射有性能开销,高 QPS 场景差距明显。 -
不依赖接口:JDK 动态代理要求目标类实现接口,Javassist 可以直接对类做字节码增强。
但 Javassist 也有缺点:
- 启动时生成字节码有耗时,首次调用可能稍慢。
- 生成的类占用 PermGen/Metaspace,大量服务可能导致内存压力。
Dubbo 默认使用 Javassist,但也支持 JDK 代理(配置 proxy="jdk")。
Q4:Dubbo 的线程模型是怎样的?
Dubbo 的 Provider 端使用线程池模型处理请求:
读图导引:注意 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 默认启用应用级服务发现,但兼容接口级。迁移路径:
-
双注册阶段:Provider 同时注册接口级和应用级地址。
yamldubbo: application: register-mode: all # interface, instance, all -
Consumer 升级:Consumer 升级到 Dubbo 3.x,优先消费应用级地址。
-
Provider 切流:确认所有 Consumer 都支持应用级后,Provider 关闭接口级注册。
yamldubbo: application: register-mode: instance
总结
本文从 @DubboReference 的魔法出发,逐层穿透 Dubbo 的核心实现:
-
服务暴露:
ServiceConfig解析配置,通过ProxyFactory将实现类包装成AbstractProxyInvoker,RegistryProtocol先调用DubboProtocol打开NettyServer,再向注册中心注册地址。 -
服务引用:
ReferenceConfig通过RegistryProtocol向注册中心订阅地址,RegistryDirectory维护地址列表,为每个地址创建DubboInvoker,Cluster.join合并成统一的FailoverClusterInvoker,最后ProxyFactory生成代理对象。 -
调用链路:代理对象 →
InvokerInvocationHandler→ClusterInvoker→Directory.list→Router.route→LoadBalance.select→Filter链 →DubboInvoker→NettyClient→ 网络传输 →NettyServer→DubboProtocol→Filter链 →AbstractProxyInvoker→Wrapper→ 实现类。 -
SPI 机制:
ExtensionLoader实现按需加载、自适应扩展(@Adaptive动态生成适配器类)和 AOP 包装(@Wrapper实现装饰者链),这是 Dubbo 高度可扩展的根基。 -
集群容错:默认
FailoverCluster失败重试 2 次,存在重试风暴风险。读操作用Failover,写操作用Failfast。 -
Dubbo 3.x 变革:应用级服务发现将注册中心从接口级地址推送减负为应用实例推送,配合 Triple 协议实现与 gRPC 生态互通。