在微服务架构日益普及的今天,服务之间的动态发现与调用成为系统稳定性和可扩展性的关键。注册中心作为微服务架构中的“通讯录”,承担着服务注册、发现、健康检查等核心职责。本文将深入剖析 Nacos 2.1.0 作为注册中心的设计理念、核心功能,并结合源码解析其实现机制。
在单体架构时代,服务调用关系固定,IP 和端口可硬编码。然而,微服务架构下,服务实例动态伸缩、频繁上下线,传统方式已无法满足需求。注册中心应运而生,其核心目标是:
Nacos(Naming and Configuration Service)由阿里巴巴开源,集服务发现与配置管理于一体。其注册中心模块(Naming)在 2.0+ 版本后引入 gRPC 长连接、服务端主动推送等机制,显著提升了性能与实时性。

源码环境的搭建请参考 《Spring Cloud Alibaba 版本选择与源码环境搭建》
贴个图,整体源码流程如下,可根据图中流程梳理:

通过学习 Nacos 1.4 我们知道了我们的注册对应的类是 NacosNamingService,那我们 Nacos 2.0+ 是不是也是一样的呢?
通过查找我们发现里面也有注册实例 registerInstance 的信息,那我们打上断点看一下是否到这里,我们在对应注册方法打上断点,然后 debug 启动,如图 :

通过堆栈信息,我们查找到其注册是从 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 文件
textorg.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 应该和我们注册相关,我们进入看一下,查看源码

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

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

到此处客户端启动的实例注册流程就结束了,总结一下,在客户端启动时自动装配了 NacosAutoServiceRegistration,其内部实现监听器监听了 ApplicationListener<WebServerInitializedEvent> 事件,事件内部通过 NacosServiceRegistry 服务注册器调用了 NamingService #registerInstance 注册实例的方法,也就是项目启动时会往 Nacos 服务端自动注册实例的原因。
核心类
接下来我们从客户端、服务端2个维度来分析下实例注册的源码
紧接上文,我们在源码中调用了 NamingService #registerInstance 注册实例的方法
javapublic 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 实现类

查看其 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 协议适用于临时实例:
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 的值判断
grpc 通信同理在这我们能推断出 AP 模式是用的 grpc 模式,CP 模式是用 http 通信
服务端源码中关注的核心为:注册的实例隶属于服务下,一个服务会包含多个实例,具体的实例有对应的ip和port。
服务端 gRPC 处理服务请求的入口,其注册类型则是客户端请求实例中的 NamingRemoteConstants.REGISTER_INSTANCE

其注册实例方法比较简单

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

上面的方法封装了需要注册的信息,在方法的尾部发布了一个客户端的注册事件 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 事件,进入后看到了我们熟悉的事件
javaprivate 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协议进行服务注册 -> 服务端接收请求并缓存实例,同时发布服务变更事件 -> 变更事件中,将服务信息添加到延迟任务引擎中 -> 延迟任务引擎执行,推送给所有订阅者

服务与实例之间的关系
要理解服务与实例之间的关系,我们注册上来的是实例,一个服务包含多个实例。
源码中的 ClientServiceIndexesManager #publisherIndexes 结构是 Map
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 对象进行实例注册
javapublic 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 许可协议。转载请注明出处!