乐于分享
好东西不私藏

Dubbo 3.3.x 源码深度解读(八):注册中心与服务发现——从接口级到应用级

Dubbo 3.3.x 源码深度解读(八):注册中心与服务发现——从接口级到应用级

前面七篇我们搞懂了Dubbo的骨架、血型、开门、找门、协议、容错、负载均衡。今天来聊Dubbo的"婚介所"——注册中心。Provider要"登记征婚",Consumer要"查资料相亲",全靠注册中心撮合。Dubbo 3.x最大的变化之一,就是从"每个接口都登记"(接口级)变成了"整个应用只登记一次"(应用级)。这就像从"每个员工单独办入职"进化到"整个团队一起办入职"。

一、注册中心的"媒婆"职责

1.1 注册中心是干什么的?

在Dubbo里,注册中心干了三件事:

职责
Provider做什么
Consumer做什么
像什么
服务注册
启动时把地址注册上去
挂"征婚启事"
服务发现
启动时查询有哪些Provider
查"征婚启事"
服务变更通知
订阅后实时收到Provider变化
实时推送"有新征婚者"

1.2 常见的注册中心

注册中心
特点
Dubbo支持
Nacos
阿里开源,功能全面,社区活跃
✅ 推荐
ZooKeeper
Apache项目,稳定可靠
✅ 老牌
Consul
HashiCorp,服务网格友好
etcd
K8s原生,云原生友好
Redis
简单轻量

Dubbo 3.3.x默认推荐Nacos,但ZooKeeper依然是很多老项目的首选。


二、Registry接口:注册中心的抽象

2.1 核心接口

// org.apache.dubbo.registry.RegistrypublicinterfaceRegistryextendsNodeRegistryService{// RegistryService定义了注册中心的基本操作}// org.apache.dubbo.registry.RegistryServicepublicinterfaceRegistryService{// 注册服务URLvoidregister(URL url);// 注销服务URLvoidunregister(URL url);// 订阅服务变化(Consumer用)voidsubscribe(URL url, NotifyListener listener);// 取消订阅voidunsubscribe(URL url, NotifyListener listener);// 查询已注册的服务列表List<URL> lookup(URL url);}

Registry接口定义了四个基本操作:注册、注销、订阅、查询。所有注册中心实现(NacosRegistry、ZookeeperRegistry等)都要实现这四个方法。

2.2 RegistryFactory:SPI扩展点

// org.apache.dubbo.registry.RegistryFactory@SPI("dubbo")publicinterfaceRegistryFactory{@Adaptive({"protocol"})Registry getRegistry(URL url);}

RegistryFactory是SPI扩展点,通过URL的protocol参数决定用哪个注册中心。比如:

nacos://127.0.0.1:8848  → NacosRegistryFactory → NacosRegistryzookeeper://127.0.0.1:2181 → ZookeeperRegistryFactory → ZookeeperRegistry

三、接口级注册 vs 应用级注册

3.1 Dubbo 2.x的接口级注册

在Dubbo 2.x时代,Provider每暴露一个接口,就往注册中心写一条数据:

/dubbo/com.example.DemoService/providers    ├── dubbo://192.168.1.101:20880/com.example.DemoService?version=1.0.0    ├── dubbo://192.168.1.102:20880/com.example.DemoService?version=1.0.0    └── dubbo://192.168.1.103:20880/com.example.DemoService?version=1.0.0/dubbo/com.example.OrderService/providers    ├── dubbo://192.168.1.101:20881/com.example.OrderService?version=1.0.0    ├── dubbo://192.168.1.102:20881/com.example.OrderService?version=1.0.0    └── dubbo://192.168.1.103:20881/com.example.OrderService?version=1.0.0

假设一个应用暴露了10个接口,有100个Provider实例,注册中心里就有 10 × 100 = 1000条数据

问题

  • 注册中心压力大:数据量 = 接口数 × 实例数,动辄几万条
  • 推送频繁:一个Provider上下线,要推送所有接口的变更
  • 内存占用高:Consumer要缓存所有接口的Provider列表

3.2 Dubbo 3.x的应用级注册

Dubbo 3.x引入了应用级服务发现(Application-Level Service Discovery):

/services/demo-provider/192.168.1.101:20880    ├── interface: com.example.DemoService    ├── interface: com.example.OrderService    ├── interface: com.example.UserService    └── .../services/demo-provider/192.168.1.102:20880    ├── interface: com.example.DemoService    ├── interface: com.example.OrderService    ├── interface: com.example.UserService    └── ...

现在一个应用只注册一条数据,里面包含了这个应用暴露的所有接口。

优势

维度
接口级(Dubbo 2.x)
应用级(Dubbo 3.x)
注册数据量
接口数 × 实例数
实例数
推送频率
接口级变更推送
应用级变更推送
Consumer内存占用
缓存所有接口Provider
缓存应用实例 + 元数据
启动速度
慢(要订阅所有接口)
快(只订阅应用)
网关友好性
差(网关要感知接口)
好(网关只感知应用)

3.3 Dubbo 3.x的双注册模式

为了兼容Dubbo 2.x,Dubbo 3.x支持双注册

// 同时注册接口级和应用级<dubbo:application name="demo-provider" registry="N/A">    <dubbo:registry address="nacos://127.0.0.1:8848" register-mode="all" />    <!-- all = 接口级 + 应用级 -->    <!-- interface= 仅接口级 -->    <!-- instance = 仅应用级 --></dubbo:application>

Dubbo 3.x Consumer默认优先使用应用级,如果找不到再降级到接口级。


四、NacosRegistry源码追踪

4.1 NacosRegistry的结构

// org.apache.dubbo.registry.nacos.NacosRegistrypublicclassNacosRegistryextendsFailbackRegistry{private NamingService namingService;  // Nacos的NamingServicepublicNacosRegistry(URL url){super(url);// 初始化Nacos NamingService        Properties properties = new Properties();        properties.put(SERVER_ADDR, url.getAddress());this.namingService = NacosFactory.createNamingService(properties);    }@OverridepublicvoiddoRegister(URL url){// 应用级注册:注册为Nacos服务实例        Instance instance = new Instance();        instance.setIp(url.getHost());        instance.setPort(url.getPort());        instance.setMetadata(getMetadata(url));        String serviceName = getServiceName(url);  // 应用名        String group = getGroup(url);        namingService.registerInstance(serviceName, group, instance);    }@OverridepublicvoiddoUnregister(URL url){// 注销实例        namingService.deregisterInstance(            getServiceName(url),             getGroup(url),             url.getHost(),             url.getPort()        );    }@OverridepublicvoiddoSubscribe(URL url, NotifyListener listener){// 应用级订阅        String serviceName = getServiceName(url);// 获取当前所有实例        List<Instance> instances = namingService.getAllInstances(serviceName, getGroup(url));// 通知Listener(Directory)更新Provider列表        listener.notify(toUrls(instances));// 订阅实时变更        namingService.subscribe(serviceName, getGroup(url), new EventListener() {@OverridepublicvoidonEvent(Event event){// Nacos推送变更时触发if (event instanceof NamingEvent) {                    NamingEvent namingEvent = (NamingEvent) event;                    List<Instance> instances = namingEvent.getInstances();                    listener.notify(toUrls(instances));                }            }        });    }// 将Nacos Instance列表转换为Dubbo URL列表private List<URL> toUrls(List<Instance> instances){        List<URL> urls = new ArrayList<>();for (Instance instance : instances) {            URL url = new URLBuilder()                .protocol(instance.getMetadata().get(PROTOCOL_KEY))                .host(instance.getIp())                .port(instance.getPort())                .addParameters(instance.getMetadata())                .build();            urls.add(url);        }return urls;    }}

NacosRegistry类,循环结构处理了批量数据或迭代逻辑,是核心业务流程的体现。条件判断逻辑根据不同状态执行不同分支,体现了业务规则的分支处理。方法的返回值传递了处理结果,调用方可据此进行后续操作。该方法是NacosRegistry的核心逻辑入口,通过合理的参数设计和返回值约定,实现了与调用方的解耦。继承/实现关系体现了面向对象的设计原则,通过多态实现灵活的扩展能力。@Override标注表明这是对父类或接口方法的重写,遵循了里氏替换原则。

NacosRegistry的核心逻辑:

  1. 注册:把Provider的IP+Port+元数据注册为Nacos Instance
  2. 订阅:查询当前所有Instance,并订阅实时变更
  3. 变更通知:Nacos推送变更时,转换为Dubbo URL,通知Directory更新

4.2 Nacos的两种服务模型

Nacos支持两种服务发现模型,Dubbo 3.x都用到了:

模型
用途
对应Dubbo
NamingService
服务发现(IP+Port级别)
应用级注册
ConfigService
配置管理(KV级别)
接口级注册、路由规则、配置规则

NamingService(服务发现):

// 注册服务实例(应用级)namingService.registerInstance("demo-provider""DEFAULT_GROUP", instance);// 查询服务实例List<Instance> instances = namingService.getAllInstances("demo-provider""DEFAULT_GROUP");

Nacos应用级服务注册的API。registerInstance将服务实例注册到Nacos,参数包括服务名(demo-provider,应用名而非接口名)、分组(DEFAULT_GROUP)和Instance对象(包含IP、端口、元数据等)。getAllInstances查询指定服务的所有实例列表。这是Dubbo 3.x应用级服务发现的基础——注册中心只存储应用→实例列表的映射,大大减少了注册数据量(从 接口数×实例数 降到 实例数)。Consumer通过getAllInstances获取应用实例列表后,再从实例的MetadataService获取接口列表,建立接口→实例的映射。与Dubbo 2.x的接口级注册(每个接口一条注册记录)相比,应用级注册显著降低了注册中心的存储压力和网络推送量,特别适合大规模微服务部署。

ConfigService(配置管理):

// 发布配置(接口级)configService.publishConfig("com.example.DemoService""DEFAULT_GROUP", providerUrlStr);// 监听配置变更configService.addListener("com.example.DemoService""DEFAULT_GROUP"new Listener() {@OverridepublicvoidreceiveConfigInfo(String config){// 配置变更时触发    }});

Nacos接口级配置的发布和监听,这是Dubbo 2.x兼容模式下的服务发现方式。publishConfig将Provider URL字符串作为配置发布到Nacos Config Service,key为接口名(com.example.DemoService)。addListener监听配置变更,当Provider上下线时配置变更触发receiveConfigInfo回调,Consumer据此更新本地的Invoker列表。这种模式利用Nacos的配置管理能力实现接口级服务发现——每个接口的Provider列表作为一条配置存储。虽然功能上可行,但配置中心并非为服务发现设计——大量接口配置变更时推送压力大,且不支持健康检查等注册中心特性。Dubbo 3.x推荐使用Nacos的Naming服务(registerInstance/getAllInstances)实现应用级服务发现,而非Config服务。


五、服务发现的延迟与缓存

5.1 服务发现的延迟问题

服务发现不是实时的,有几层延迟:

Provider宕机    ↓ ~0ms(瞬间)Provider的JVM停止    ↓ ~5s(Nacos心跳间隔)Nacos检测到心跳超时,标记为不健康    ↓ ~0ms(Nacos内部)Nacos推送变更事件给Consumer    ↓ ~0-500ms(网络延迟)Consumer的Directory收到通知,更新Invoker列表    ↓ ~0ms(内存操作)新的请求不再路由到故障Provider

总延迟约5秒。如果Provider是正常优雅停机(先unregister再shutdown),延迟几乎为0。

5.2 Directory的本地缓存

Consumer端的Directory会缓存Provider列表,避免每次都去注册中心查询:

// RegistryDirectory.javapublicclassRegistryDirectory<TextendsAbstractDirectory<T{// 当前缓存的Invoker列表(volatile保证可见性)privatevolatile List<Invoker<T>> invokers;// 本地缓存的Provider URL列表privatevolatile List<URL> cachedInvokerUrls;@Overridepublic List<Invoker<T>> list(Invocation invocation) throws RpcException {// 返回本地缓存的Invoker列表// 不需要每次都去注册中心查询return invokers;    }@Overridepublicsynchronizedvoidnotify(List<URL> urls){// 注册中心推送变更时更新缓存this.cachedInvokerUrls = urls;        refreshInvoker(urls);  // 刷新Invoker列表    }}

抽象类RegistryDirectory,使用synchronized关键字保证线程安全,确保同一时间只有一个线程能执行临界区代码。volatile修饰符保证了字段的内存可见性,所有线程读取的都是最新值而非缓存副本。缓存机制减少了对底层数据源的重复访问,是提升系统性能的常用手段。条件判断逻辑根据不同状态执行不同分支,体现了业务规则的分支处理。方法的返回值传递了处理结果,调用方可据此进行后续操作。该方法是RegistryDirectory的核心逻辑入口,通过合理的参数设计和返回值约定,实现了与调用方的解耦。继承/实现关系体现了面向对象的设计原则,通过多态实现灵活的扩展能力。@Override标注表明这是对父类或接口方法的重写,遵循了里氏替换原则。

Directory的缓存策略:

  • 启动时:从注册中心拉取全量Provider列表
  • 运行时:依赖注册中心的推送更新(Nacos是长轮询,ZooKeeper是Watcher通知)
  • 故障时:如果注册中心挂了,Consumer还能用本地缓存继续调用(虽然看不到新上线的Provider)

六、元数据中心:Dubbo 3.x的新角色

6.1 为什么需要元数据中心?

应用级注册只注册了"有哪些应用实例",但没有注册"每个应用暴露了哪些接口"。Consumer需要知道"demo-provider这个应用暴露了DemoService接口",这个信息存在哪?

元数据中心(MetadataCenter)就是干这个的:

注册中心(Nacos NamingService)    └── 有哪些应用实例(IP+Port)元数据中心(Nacos ConfigService / Redis / Etcd)    └── 每个应用暴露了什么接口    └── 每个接口的方法、参数、返回值    └── 服务的元数据(version、group、timeout等)

6.2 MetadataReport接口

// org.apache.dubbo.metadata.report.MetadataReport@SPI("default")publicinterfaceMetadataReport{// 发布服务元数据voidpublishServiceDefinition(URL url);// 获取服务元数据String getServiceDefinition(String interfaceName, String version, String group);}

publishServiceDefinition方法的实现,这个方法的参数设计和返回值约定体现了清晰的接口契约,便于调用方正确使用和扩展。代码注释中提到了org.apache.dubbo.metadata.report.MetadataReport,这是理解该方法业务语义的重要线索。代码中使用了字符串常量"default",通常用于配置标识或协议关键字。整体设计遵循了单一职责原则,方法的输入输出语义明确,便于单元测试和维护。在实际生产环境中,执行效率直接影响系统的吞吐量和响应延迟。

Dubbo 3.x的Consumer在引用服务时:

  1. 从注册中心获取应用实例列表(IP+Port)
  2. 从元数据中心获取接口元数据(有哪些方法、参数类型等)
  3. 组装成完整的Invoker

6.3 元数据中心的实现

实现
基于
用途
NacosMetadataReport
Nacos ConfigService
把接口元数据存在Nacos配置里
RedisMetadataReport
Redis
存在Redis Hash里
EtcdMetadataReport
etcd
存在etcd Key-Value里

七、总结:注册中心的"婚介所"工作流

阶段
Provider
注册中心
Consumer
比喻
启动
组装URL,export()
接收注册请求
挂征婚启事
注册
RegistryProtocol.register()
存储实例信息
登记资料
订阅
维护订阅关系
subscribe()
登记相亲需求
推送
notify()
Directory.notify()
推送匹配对象
调用
Invoker.invoke()
通过Directory获取Invoker
相亲见面
下线
unregister()
删除实例,推送变更
收到通知,移除Invoker
撤下启事

Dubbo 3.x的应用级注册,是微服务架构从"接口粒度"向"应用粒度"演进的重要标志。它大幅降低了注册中心的压力,提升了Consumer的启动速度,同时也为Service Mesh和云原生架构打下了基础。

下一篇预告:远程通信层:Remoting与Netty4——看Dubbo是怎么基于Netty实现高性能网络通信的,以及请求-响应是怎么编码解码的。


Dubbo 3.3.x 源码深度解读系列(八) | 持续更新中