2025-10-21
微服务与分布式
0

目录

注册中心的设计初衷与理念
客户端启动时的源码分析
源码分析
实例注册源码分析
客户端实例注册
服务端实例注册
总结

在微服务架构日益普及的今天,服务之间的动态发现与调用成为系统稳定性和可扩展性的关键。注册中心作为微服务架构中的“通讯录”,承担着服务注册、发现、健康检查等核心职责。本文将深入剖析 Nacos 2.1.0 作为注册中心的设计理念、核心功能,并结合源码解析其实现机制。

注册中心的设计初衷与理念

在单体架构时代,服务调用关系固定,IP 和端口可硬编码。然而,微服务架构下,服务实例动态伸缩、频繁上下线,传统方式已无法满足需求。注册中心应运而生,其核心目标是:

  • 解耦服务提供者与消费者:通过中间注册表实现动态寻址。
  • 保障服务高可用:自动剔除不健康实例,避免调用失败。
  • 支持弹性伸缩:服务实例增减无需修改配置。
  • 统一服务治理入口:为后续熔断、限流、路由等能力打下基础。

Nacos(Naming and Configuration Service)由阿里巴巴开源,集服务发现与配置管理于一体。其注册中心模块(Naming)在 2.0+ 版本后引入 gRPC 长连接、服务端主动推送等机制,显著提升了性能与实时性。

image.png

客户端启动时的源码分析

源码环境的搭建请参考 《Spring Cloud Alibaba 版本选择与源码环境搭建》

贴个图,整体源码流程如下,可根据图中流程梳理:

image.png

源码分析

通过学习 Nacos 1.4 我们知道了我们的注册对应的类是 NacosNamingService,那我们 Nacos 2.0+ 是不是也是一样的呢?

通过查找我们发现里面也有注册实例 registerInstance 的信息,那我们打上断点看一下是否到这里,我们在对应注册方法打上断点,然后 debug 启动,如图 :

image.png

通过堆栈信息,我们查找到其注册是从 NacosAutoServiceRegistration #onApplicationEvent 方法中执行的,通过监听 WebServerInitializedEvent 事件而触发的,那这里的入口是从哪里来这里的呢?

`WebServerInitializedEvent` 事件的发布时机

WebServerInitializedEvent 事件是在 Spring 容器启动时 refresh() 方法内部中执行完 Bean 的实例化、初始化、后置处理完成后,最后到 finishRefresh() 的方法进行发布事件的,主要是通过 WebServerStartStopLifecycle web服务开启停止生命周期的 Bean 信息在启动时,发布了 ServletWebServerInitializedEvent 事件。

也就是说是项目启动后(web server 已经启动),才开始发布事件,触发的注册!!!

我们联想一下,现在的 Nacos 与 Springboot 整合,那可能使用了 SpringBoot 的自动装配,那就去查看下spring-cloud-starter-alibaba-nacos-discovery 的项目中的 spring.factoris 文件

text
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\ com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\ com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\ com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,\ com.alibaba.cloud.nacos.discovery.NacosDiscoveryHeartBeatConfiguration,\ com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClientConfiguration,\ com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration,\ com.alibaba.cloud.nacos.loadbalancer.LoadBalancerNacosAutoConfiguration,\ com.alibaba.cloud.nacos.NacosServiceAutoConfiguration,\ com.alibaba.cloud.nacos.util.UtilIPv6AutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration org.springframework.boot.SpringApplicationRunListener=\ com.alibaba.cloud.nacos.logging.NacosLoggingAppRunListener

从上面我们排查各个自动装配类,在这里我们可以通过名称来推断那个关于注册自动配置类,NacosServiceRegistryAutoConfiguration 应该和我们注册相关,我们进入看一下,查看源码

image.png

通过上述源码,NacosAutoServiceRegistration 的子类 NacosAutoServiceRegistration中实现了监听器 ApplicationListener,并监听了 WebServerInitializedEvent 事件(Spring 核心方法 refresh 的完成后广播事件),会在服务初始化时被调用到

java
public abstract class AbstractAutoServiceRegistration<R extends Registration> implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> { // 其他代码.... @Override @SuppressWarnings("deprecation") public void onApplicationEvent(WebServerInitializedEvent event) { bind(event); } @Deprecated public void bind(WebServerInitializedEvent event) { ApplicationContext context = event.getApplicationContext(); if (context instanceof ConfigurableWebServerApplicationContext) { if ("management".equals(((ConfigurableWebServerApplicationContext) context).getServerNamespace())) { return; } } this.port.compareAndSet(0, event.getWebServer().getPort()); this.start(); } // 其他代码... }

继续查看 this.start() 方法,内部其调用了 register()

java
public void start() { if (!isEnabled()) { if (logger.isDebugEnabled()) { logger.debug("Discovery Lifecycle disabled. Not starting"); } return; } // only initialize if nonSecurePort is greater than 0 and it isn't already running // because of containerPortInitializer below if (!this.running.get()) { // ApplicationContext 发布注册前事件 this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration())); register(); if (shouldRegisterManagement()) { registerManagement(); } // ApplicationContext 发布注册后事件 this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration())); this.running.compareAndSet(false, true); } } protected void register() { this.serviceRegistry.register(getRegistration()); }

getRegistration() 方法此处的调用方法在父类中有相应的实现,将当前本机的端口设置了进去

java
@Override protected NacosRegistration getRegistration() { if (this.registration.getPort() < 0 && this.getPort().get() > 0) { this.registration.setPort(this.getPort().get()); } Assert.isTrue(this.registration.getPort() > 0, "service.port has not been set"); return this.registration; }

serviceRegistry 是何方神圣呢?原来是在刚开始定义实例时,传入的 NacosServiceRegistry 实例对象

image.png

查看 NacosServiceRegistry #register 方法,发现一个特别眼熟的地方,方法内部获取了 NamingService,调用了 #registerInstance 注册实现的方法

image.png

到此处客户端启动的实例注册流程就结束了,总结一下,在客户端启动时自动装配了 NacosAutoServiceRegistration,其内部实现监听器监听了 ApplicationListener<WebServerInitializedEvent> 事件,事件内部通过 NacosServiceRegistry 服务注册器调用了 NamingService #registerInstance 注册实例的方法,也就是项目启动时会往 Nacos 服务端自动注册实例的原因。

核心类

  • NacosAutoServiceRegistration:Nacos 服务自动注册表
  • WebServerInitializedEvent:web服务初始化事件,可用于获取正在运行的服务器的本地端口
  • NacosServiceRegistry:Nacos 服务注册器
  • NacosRegistration:Nacos 注册表
  • NamingService:命名服务
  • NacosServiceManager:Nacos 服务管理器

实例注册源码分析

接下来我们从客户端、服务端2个维度来分析下实例注册的源码

客户端实例注册

紧接上文,我们在源码中调用了 NamingService #registerInstance 注册实例的方法

java
public class NacosNamingService implements NamingService { @Override public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { // 检查实例是否合法 NamingUtils.checkInstanceIsLegal(instance); // 通过客户端代理进行服务注册 clientProxy.registerService(serviceName, groupName, instance); } }

通过客户端代理执行了服务注册,其包括以下实现,通过追溯,此处对象定义的为 NamingClientProxyDelegate 实现类

image.png

查看其 NamingClientProxyDelegate #registerService 方法实现,在此处还会获取客户端代理

java
@Override public void registerService(String serviceName, String groupName, Instance instance) throws NacosException { getExecuteClientProxy(instance).registerService(serviceName, groupName, instance); } private NamingClientProxy getExecuteClientProxy(Instance instance) { return instance.isEphemeral() ? grpcClientProxy : httpClientProxy; }

上述代码中通过 Instance.isEphemeral() 方法判断实例是否为临时实例

  • 临时实例使用 gRPC 协议(NamingGrpcClientProxy)
  • 持久实例使用 HTTP 协议(NamingHttpClientProxy)

Nacos 为什么根据临时实例,采用不同协议进行服务注册?

不同协议的特点和适用场景

  • gRPC 协议适用于临时实例:
    • 临时实例需要与服务端保持长连接,以便实时上报心跳
    • gRPC支持双向通信,适合处理心跳检测和实时状态同步
    • 临时实例生命周期较短,需要更高效的通信机制
  • HTTP 协议适用于持久实例:
    • 持久实例通常生命周期较长,不需要频繁通信
    • HTTP协议简单可靠,适合处理相对稳定的注册信息
    • 减少长连接资源消耗

设计优势

  • 资源优化:根据实例特性选择合适的通信协议,避免资源浪费
  • 性能考虑:临时实例使用高效的gRPC,持久实例使用稳定的HTTP
  • 功能适配:不同类型的实例有不同的通信需求,协议选择与之匹配

这种设计体现了 Nacos 对不同场景的精细化处理,既保证了临时实例的实时性要求,又兼顾了持久实例的稳定性需求。

ephemeral 的默认值 true 表示临时实例,我们来查看临时实例通过 NamingGrpcClientProxy #registerService 注册方法,首先将当前客户端添加到实例数据缓存中

java
@Override public void registerService(String serviceName, String groupName, Instance instance) throws NacosException { NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName, instance); // 缓存当前实例 redoService.cacheInstanceForRedo(serviceName, groupName, instance); // 注册服务 doRegisterService(serviceName, groupName, instance); } // 实例数据 private final ConcurrentMap<String, InstanceRedoData> registeredInstances = new ConcurrentHashMap<>(); public void cacheInstanceForRedo(String serviceName, String groupName, Instance instance) { // 分组名称@@服务名称 String key = NamingUtils.getGroupedName(serviceName, groupName); // 实例注册数据 InstanceRedoData redoData = InstanceRedoData.build(serviceName, groupName, instance); synchronized (registeredInstances) { // 缓存实例注册数据 registeredInstances.put(key, redoData); } }

doRegisterService 方法,则是构建请求信息,真正的进行 gRPC 的调用

java
public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException { // 实例请求信息 InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName, NamingRemoteConstants.REGISTER_INSTANCE, instance); // 请求服务 requestToServer(request, Response.class); // 更新实例注册数据中的缓存标识 redoService.instanceRegistered(serviceName, groupName); }

总结

到此对客户端服务注册的源码进行一次 小总结:其核心就是通过 ephemeral 的值判断

  • 如果是 true 属于临时实例,采用的是 grpc 通信
  • 如果是 false 属于永久实例,采用的 http 通信;

同理在这我们能推断出 AP 模式是用的 grpc 模式,CP 模式是用 http 通信

服务端实例注册

服务端源码中关注的核心为:注册的实例隶属于服务下,一个服务会包含多个实例,具体的实例有对应的ip和port。

服务端 gRPC 处理服务请求的入口,其注册类型则是客户端请求实例中的 NamingRemoteConstants.REGISTER_INSTANCE

image.png

其注册实例方法比较简单

image.png

核心是找到其对应的客户端实现对象,存储实例

image.png

上面的方法封装了需要注册的信息,在方法的尾部发布了一个客户端的注册事件 ClientRegisterServiceEvent 实现了 ClientOperationEvent,我们来下看事件中都做了哪些处理呢?

在源码中找到对应的事件处理器 ClientServiceIndexesManager 进入到对应的 onEvent(...) 方法中,可以看到此处接受 ClientOperationEvent 事件的处理

java
@Override public void onEvent(Event event) { if (event instanceof ClientEvent.ClientDisconnectEvent) { // 处理客户端断开 handleClientDisconnect((ClientEvent.ClientDisconnectEvent) event); } else if (event instanceof ClientOperationEvent) { // 处理客户端操作 handleClientOperation((ClientOperationEvent) event); } }

对应的是 ClientOperationEvent 事件,进入后看到了我们熟悉的事件

java
private void handleClientOperation(ClientOperationEvent event) { Service service = event.getService(); String clientId = event.getClientId(); // 客户端注册服务事件 if (event instanceof ClientOperationEvent.ClientRegisterServiceEvent) { addPublisherIndexes(service, clientId); } // 客户端注销服务事件 else if (event instanceof ClientOperationEvent.ClientDeregisterServiceEvent) { removePublisherIndexes(service, clientId); } // 客户端订阅服务事件 else if (event instanceof ClientOperationEvent.ClientSubscribeServiceEvent) { addSubscriberIndexes(service, clientId); } // 客户端取消订阅服务事件 else if (event instanceof ClientOperationEvent.ClientUnsubscribeServiceEvent) { removeSubscriberIndexes(service, clientId); } }

查看客户端注册服务事件中 addPublisherIndexes 的处理逻辑

java
/** * 服务实例关系集合 */ private final ConcurrentMap<Service, Set<String>> publisherIndexes = new ConcurrentHashMap<>(); private void addPublisherIndexes(Service service, String clientId) { // publisherIndexes 存储了需要注册的服务信息 publisherIndexes.computeIfAbsent(service, (key) -> new ConcurrentHashSet<>()); // 将客户端添加到服务信息的集合中 publisherIndexes.get(service).add(clientId); // 发布一个服务改变的事件 NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, true)); }

到这儿我们就可以看到注册的服务是保存在 publisherIndexes 中,原来服务与实例都存储在这。此外还发布一个服务改变的事件 ServiceEvent.ServiceChangedEvent

java
@Override public void onEvent(Event event) { // 服务变更事件 if (event instanceof ServiceEvent.ServiceChangedEvent) { // 服务发生变化,推送给所有订阅者 ServiceEvent.ServiceChangedEvent serviceChangedEvent = (ServiceEvent.ServiceChangedEvent) event; // 服务 Service service = serviceChangedEvent.getService(); // 添加延迟任务,500ms delayTaskEngine.addTask(service, new PushDelayTask(service, PushConfig.getInstance().getPushTaskDelay())); // 增加服务变更次数 MetricsMonitor.incrementServiceChangeCount(service.getNamespace(), service.getGroup(), service.getName()); } // 服务订阅事件 else if (event instanceof ServiceEvent.ServiceSubscribedEvent) { // 如果服务由一个客户端订阅,则只推送此客户端 ServiceEvent.ServiceSubscribedEvent subscribedEvent = (ServiceEvent.ServiceSubscribedEvent) event; Service service = subscribedEvent.getService(); // 添加延迟任务,默认 500ms 执行一次 delayTaskEngine.addTask(service, new PushDelayTask(service, PushConfig.getInstance().getPushTaskDelay(), subscribedEvent.getClientId())); } }

通过发布 ServiceEvent.ServiceChangedEvent 服务变更的事件,结合推送延迟任务执行引擎 PushDelayTaskExecuteEngine 添加了一个任务,兜兜转转 最终会执行 PushExecuteTask #run 方法(发布订阅的内容)

java
@Override public void run() { try { // 包装数据 PushDataWrapper wrapper = generatePushData(); // 客户端管理器 ClientManager clientManager = delayTaskEngine.getClientManager(); for (String each : getTargetClientIds()) { // 获取客户端 Client client = clientManager.getClient(each); if (null == client) { // means this client has disconnect continue; } // 获取客户端的订阅者 Subscriber subscriber = clientManager.getClient(each).getSubscriber(service); // 执行推送 delayTaskEngine.getPushExecutor().doPushWithCallback(each, subscriber, wrapper, // 推送回调 new ServicePushCallback(each, subscriber, wrapper.getOriginalData(), delayTask.isPushToAll())); } } catch (Exception e) { Loggers.PUSH.error("Push task for service" + service.getGroupedServiceName() + " execute failed ", e); delayTaskEngine.addTask(service, new PushDelayTask(service, 1000L)); } }

此处会推送给所有的订阅者,通过查询 NotifySubscriberRequest 对象,客户端作为接收方更新本地服务实例信息,整体流程 应用启动时通过web初始化事件,通过grpc协议进行服务注册 -> 服务端接收请求并缓存实例,同时发布服务变更事件 -> 变更事件中,将服务信息添加到延迟任务引擎中 -> 延迟任务引擎执行,推送给所有订阅者

image.png

服务与实例之间的关系

要理解服务与实例之间的关系,我们注册上来的是实例,一个服务包含多个实例。

源码中的 ClientServiceIndexesManager #publisherIndexes 结构是 Map

  • key:是服务 service
  • value: 是对应的实例,也就是一个 Client 对应一个服务的具体实例
java
/** * 服务实例关系集合 */ private final ConcurrentMap<Service, Set<String>> publisherIndexes = new ConcurrentHashMap<>(); private void addPublisherIndexes(Service service, String clientId) { // publisherIndexes 存储了需要注册的服务信息 publisherIndexes.computeIfAbsent(service, (key) -> new ConcurrentHashSet<>()); // 将客户端添加到服务信息的集合中 publisherIndexes.get(service).add(clientId); // 发布一个 服务改变的 事件 NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service, true)); }

总结

掌握源码的执行思路,学会其中实例对象的使用,可在组件中任意使用扩展。

示例: 主动创建实例进行注册

NacosServiceManager 是 Nacos 服务的管理器,可从其内部调用 getNamingService() 方法,获取到 NamingService 对象进行实例注册

java
public class Test { @Autowired private NacosServiceManager nacosServiceManager; @Test public void contextLoads() throws Exception { NamingService naming = nacosServiceManager.getNamingService(); naming.registerInstance("nacos.test.3", "11.11.11.11", 8888, "TEST1"); naming.registerInstance("nacos.test.3", "2.2.2.2", 9999, "DEFAULT"); System.out.println(naming.getAllInstances("nacos.test.3")); } }

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!