目录
一、前言
1、Nacos项目结构
二、微服务注册以及健康检查流程
1、流程图
2、处理微服务注册请求
2.1、InstanceController的register()注册入口
2.2、InstanceOperatorServiceImpl的registerInstance()逻辑
3、ServiceManager组件
3.1、registerInstance()逻辑
3.2、createServiceIfAbsent()创建服务
3.3、putServiceAndInit(Service service)
3.4、Service.init()逻辑
3.5、定时任务调度
3.6、ClientBeatCheckTask的run()逻辑,服务健康检查剔除下线
3.7、addInstance()
3.8、DelegateConsistencyServiceImpl的put()
4、DistroConsistencyServiceImpl组件
4.1、put()逻辑
4.2、onPut()逻辑
4.3、DataStore组件的put()逻辑
4.4、DistroConsistencyServiceImpl内部类Notifier
4.5、Service的onChange()方法
4.6、updateIPs()
5、处理心跳请求
5.1、beat(HttpServletRequest request)逻辑
5.2、InstanceOperatorServiceImpl的handleBeat()
5.3、调用处理心跳任务
5.4、ClientBeatProcessor的run()逻辑
上一篇讲解了通过SpringBoot的自动装配将Nacos的API、Client集成到了我们自己的微服,使之成为一个更加完善的微服务系统,可以通过http请求将自己的服务信息、心跳发送给Nacos服务注册中心做处理以及维护。
目前许许多多的公司已经开始使用了Nacos作为微服务的注册配置中心,在面试的时候也喜欢拿Nacos与Eureka做比较,所以是有必要理解Nacos的原理的。

client模块是提供对外的微服务去集成的,集成原理就是SpringBoot的自动装配以及Spring的一些核心流程。在使用SpringCloud时我们需要自己去手动写注册配置中心;在Nacos它帮我们写了,并且也是基于SpringBoot项目有注册、配置启动类,相应的模块如上图所示。
既然Nacos也是基于SpringBoot的,那么我们都是比较熟悉了吧。上一节有指出微服务自己发送的http请求的入参中都有各自的URL,分别是/nacos/v1/ns/instance/beat和/nacos/v1/ns/instance。那么我们去服务注册中心找找看看。

InstanceController跟我们平常见的Controller是一样的,顾名思义,它是专门处理服务实例的控制器,我们就找找注册入口。
@CanDistro@PostMapping@Secured(action = ActionTypes.WRITE)public String register(HttpServletRequest request) throws Exception {// 命名空间final String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);// 服务名final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);// 检查命名格式NamingUtils.checkServiceNameFormat(serviceName);// 服务实例final Instance instance = HttpRequestInstanceBuilder.newBuilder().setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build();// 注册服务getInstanceOperator().registerInstance(namespaceId, serviceName, instance);// 发布事件NotifyCenter.publishEvent(new NamingTraceEvent.RegisterInstanceTraceEvent(System.currentTimeMillis(), "",false, namespaceId, NamingUtils.getGroupName(serviceName), NamingUtils.getServiceName(serviceName),instance.toInetAddr()));return "ok";}
主要逻辑:
- 通过WebUtils获取命名空间以及服务名,里面会做一些处理,如解码、不存在抛异常等
- 通过NamingUtils校验服务名是否合法
- 获取服务端操作实例,构建由 InstanceExtensionHandler 处理的新实例和链。
- 发布事件
点击getInstanceOperator().registerInstance(namespaceId, serviceName, instance);进来
它有两个实现类,上面说了使用的是服务端实例,进入下面处理逻辑
@Overridepublic void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {// 解析并设置实例com.alibaba.nacos.naming.core.Instance coreInstance = parseInstance(instance);// 委托ServiceManager注册服务serviceManager.registerInstance(namespaceId, serviceName, coreInstance);}
解析并设置实例,InstanceOperatorServiceImpl委托ServiceManager注册服务。
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {// 如果服务不存在,则创建一个服务createEmptyService(namespaceId, serviceName, instance.isEphemeral());// 从本地缓存serviceMap【Map(namespace, Map(group::serviceName, Service))类型】中获取服务Service service = getService(namespaceId, serviceName);// 校验服务不能为null,若空抛异常checkServiceIsNull(service, namespaceId, serviceName);// 将服务的实例信息添加到DataStore的dataMap缓存中addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);}
主要逻辑:
- 如果服务不存在,则创建一个服务
- 从本地缓存serviceMap【Map(namespace, Map(group::serviceName, Service))类型】中获取服务
- 校验服务不能为null,若空抛异常
- 将服务的实例信息添加到DataStore的dataMap缓存中

public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)throws NacosException {// 从本地缓存serviceMap【Map(namespace, Map(group::serviceName, Service))类型】中获取服务Service service = getService(namespaceId, serviceName);// 服务空新建if (service == null) {Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);service = new Service();service.setName(serviceName);service.setNamespaceId(namespaceId);service.setGroupName(NamingUtils.getGroupName(serviceName));// now validate the service. if failed, exception will be thrownservice.setLastModifiedMillis(System.currentTimeMillis());service.recalculateChecksum();// 集群非空if (cluster != null) {// 将服务添加到集群cluster.setService(service);// 将集群对象设置到服务clusterMap字段service.getClusterMap().put(cluster.getName(), cluster);}// 判断服务是否有效。service.validate();// 存储服务信息和初始化,点进去putServiceAndInit(service);if (!local) {addOrReplaceService(service);}}}
主要逻辑:
从本地缓存serviceMap【Map(namespace, Map(group::serviceName, Service))类型】中获取服务
服务空新建:集群非空,则将服务添加到集群,将集群对象设置到服务clusterMap字段;判断服务是否有效;存储服务信息和初始化




主要逻辑:
- 将服务信息保存到本地缓存serviceMap中
- 从本地缓存serviceMap【Map(namespace, Map(group::serviceName, Service))类型】中获取服务
- 根据命名空间、服务名以及ephemeral(决定使用哪个实现类)通过KeyBuilder 构建唯一key,然后调用DistroConsistencyServiceImpl的listen将key与listen(Service)绑定。

该方法中调用HealthCheckReactor的scheduleCheck(BeatCheckTask task)方法开始一个定时任务,入参为ClientBeatCheckTask的对象BeatCheckTask类型。



判断任务是否NacosHealthCheckTask类型,如果是则包装拦截链。从3.4就知道该任务不是NacosHealthCheckTask类型。GlobalExecutor是一个全局执行器的封装类,封装了好几个执行器,比较有意思,然后负责提交执行任务,固定延时5秒执行的定时任务做服务的剔除。
@Overridepublic void run() {try {// If upgrade to 2.0.X stop health check with v1if (ApplicationUtils.getBean(UpgradeJudgement.class).isUseGrpcFeatures()) {return;}if (!getDistroMapper().responsible(service.getName())) {return;}if (!getSwitchDomain().isHealthCheckEnabled()) {return;}List instances = service.allIPs(true);// first set health status of instances:设置服务健康状态for (Instance instance : instances) {// 如果系统当前时间与上次心跳时间差,大于设置的阈值,则设置服务实例为不健康状态if (System.currentTimeMillis() - instance.getLastBeat() > instance.getInstanceHeartBeatTimeOut()) {if (!instance.isMarked()) {if (instance.isHealthy()) {instance.setHealthy(false);Loggers.EVT_LOG.info("{POS} {IP-DISABLED} valid: {}:{}@{}@{}, region: {}, msg: client timeout after {}, last beat: {}",instance.getIp(), instance.getPort(), instance.getClusterName(),service.getName(), UtilsAndCommons.LOCALHOST_SITE,instance.getInstanceHeartBeatTimeOut(), instance.getLastBeat());getPushService().serviceChanged(service);}}}}if (!getGlobalConfig().isExpireInstance()) {return;}// then remove obsolete instances:然后删除过时的实例:for (Instance instance : instances) {if (instance.isMarked()) {continue;}// 通过判断当前时间和实例最后一次心跳时间的间隔是否大于阈值(默认15s),决定是否进行服务剔除/下线;if (System.currentTimeMillis() - instance.getLastBeat() > instance.getIpDeleteTimeout()) {// delete instance删除实例Loggers.SRV_LOG.info("[AUTO-DELETE-IP] service: {}, ip: {}", service.getName(),JacksonUtils.toJson(instance));// 异步删除实例deleteIp(instance);}}} catch (Exception e) {Loggers.SRV_LOG.warn("Exception while processing client beat time out.", e);}}
由3.4可见任务的逻辑在这里,这里就是服务健康检查。ClientBeatCheckTask也是Runnable的间接实现类,run()方法主要逻辑:
- 从service获取所有服务实例,并进行遍历
- 如果系统当前时间与上次心跳时间差,大于设置的阈值,则设置服务实例为不健康状态
- 通过判断当前时间和实例最后一次心跳时间的间隔是否大于阈值(默认15s),决定是否进行服务剔除/下线,若需剔除下线则异步删除实例。
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)throws NacosException {String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);Service service = getService(namespaceId, serviceName);synchronized (service) {List instanceList = addIpAddresses(service, ephemeral, ips);Instances instances = new Instances();instances.setInstanceList(instanceList);// 保存服务信息,key为namespaceId和serviceName的拼接consistencyService.put(key, instances);}
}
主要逻辑:
由于存在多个服务实例同时注册的场景,所以要加一个synchronized锁。
- 根据命名空间、服务名、ephemeral(默认true),通过KeyBuilder拼接key
- 根据命名空间、服务名从本地缓存serviceMap获取服务
- 比较并获得新的实例列表,设置到instances
- 保存服务信息,key为namespaceId和serviceName的拼接


ConsistencyService是一个接口,有好几个实现类。由上图可见在ServiceManager中默认注入的实现类是DelegateConsistencyServiceImpl,所以我们会进入DelegateConsistencyServiceImpl处理。

3.7中步骤一拼接key,ephemeral(默认true),源码里面会拼接ephemeral。故这里会获取ephemeralConsistencyService,即会调用其put()方法。但是EphemeralConsistencyService是一个接口(继承了ConsistencyService接口的put()方法),唯一实现类为DistroConsistencyServiceImpl,所以我们看看其put()方法逻辑。

put()方法里面的逻辑基本上都封装了出去,调用onPut()持久化服务实例,调用distroProtocol.sync()方法同步Nacos集群间数据。

主要逻辑:
- 若匹配到key,则保存到DataStore的dataMap字段中缓存起来
- 如果listener中没有这个key的话直接返回,key是在创建Service时添加进去的,见ServiceManager#putServiceAndInit()方法
- 通知NacoClient服务端服务实例信息发生变更,这里是先添加任务

这里没有多少逻辑,单纯放到一个map缓存起来。
1)addTask()逻辑


Notifier是DistroConsistencyServiceImpl的内部类,实现了Runnable接口。
这里的主要逻辑是往阻塞队列添加新的任务,但是添加的任务究竟是怎么执行的呢?熟悉Runnable接口的都知道肯定会用到它或者它的实现类,我们不妨点击一下Notifier。
![]()
发现了惊喜,在DistroConsistencyServiceImpl中有一个@PostConstruct修饰的init()方法,即在DistroConsistencyServiceImpl类构造器执行之后会执行这个方法启动Notifier通知器。那么Runnable里面有个子类必须实现的run()方法,那么这里的逻辑是?
2)Notifier的run()方法

调用阻塞队列的take()方法取出任务,然后调用Notifier的handle()方法处理服务变更。
3)handle(Pair
private void handle(Pair pair) {try {String datumKey = pair.getValue0();DataOperation action = pair.getValue1();// 任务开始受理,那么移除datumKeyservices.remove(datumKey);int count = 0;// 因为任务由listeners受理的,在ServiceManager的putServiceAndInit(Service service)// 方法中已经添加了该datumKey,如果没有则return不用受理if (!listeners.containsKey(datumKey)) {return;}for (RecordListener listener : listeners.get(datumKey)) {count++;try {// 通知数据发生更改if (action == DataOperation.CHANGE) {listener.onChange(datumKey, dataStore.get(datumKey).value);continue;}// 通知数据删除if (action == DataOperation.DELETE) {listener.onDelete(datumKey);continue;}} catch (Throwable e) {Loggers.DISTRO.error("[NACOS-DISTRO] error while notifying listener of key: {}", datumKey, e);}}if (Loggers.DISTRO.isDebugEnabled()) {Loggers.DISTRO.debug("[NACOS-DISTRO] datum change notified, key: {}, listener count: {}, action: {}",datumKey, count, action.name());}} catch (Throwable e) {Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);}}
任务开始受理,那么从缓存services移除datumKey。因为任务由listeners受理的,在ServiceManager的putServiceAndInit(Service service)方法中已经添加了该datumKey,如果没有则return不用受理。通知数据的变更/删除由listener处理的,那么我们看下其中的逻辑。
ServiceManager的putServiceAndInit(Service service)的逻辑里面,将datumKey与listener绑定,可见listener为Service,所以我们看下Service的onChange()方法。
@Overridepublic void onChange(String key, Instances value) throws Exception {Loggers.SRV_LOG.info("[NACOS-RAFT] datum is changed, key: {}, value: {}", key, value);for (Instance instance : value.getInstanceList()) {if (instance == null) {// Reject this abnormal instance list:throw new RuntimeException("got null instance " + key);}if (instance.getWeight() > 10000.0D) {instance.setWeight(10000.0D);}if (instance.getWeight() < 0.01D && instance.getWeight() > 0.0D) {instance.setWeight(0.01D);}}// 更新服务IPupdateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));recalculateChecksum();}
遍历实例所在的服务列表,在一定范围内设置权重,主要调用updateIPs()更新服务IP
public void updateIPs(Collection instances, boolean ephemeral) {Map> ipMap = new HashMap<>(clusterMap.size());for (String clusterName : clusterMap.keySet()) {ipMap.put(clusterName, new ArrayList<>());}for (Instance instance : instances) {try {if (instance == null) {Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");continue;}// 如果实例所在集群名称为空,则设置集群名称为DEFAULTif (StringUtils.isEmpty(instance.getClusterName())) {instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);}// 如果服务实例所在的集群实例clusterMap没有缓存,则新建设置默认配置并缓存到clusterMapif (!clusterMap.containsKey(instance.getClusterName())) {Loggers.SRV_LOG.warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",instance.getClusterName(), instance.toJson());Cluster cluster = new Cluster(instance.getClusterName(), this);cluster.init();getClusterMap().put(instance.getClusterName(), cluster);}// 获取集群下所有实例List clusterIPs = ipMap.get(instance.getClusterName());if (clusterIPs == null) {clusterIPs = new LinkedList<>();ipMap.put(instance.getClusterName(), clusterIPs);}//clusterIPs.add(instance);} catch (Exception e) {Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);}}for (Map.Entry> entry : ipMap.entrySet()) {// make every ip mineList entryIPs = entry.getValue();// 更新实例列表。clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);}setLastModifiedMillis(System.currentTimeMillis());// 调用PushService,通知客户端服务发生改变getPushService().serviceChanged(this);ApplicationUtils.getBean(DoubleWriteEventListener.class).doubleWriteToV2(this, ephemeral);StringBuilder stringBuilder = new StringBuilder();for (Instance instance : allIPs()) {stringBuilder.append(instance.toIpAddr()).append('_').append(instance.isHealthy()).append(',');}Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),stringBuilder.toString());}
主要逻辑:
- 如果实例所在集群名称为空,则设置集群名称为DEFAULT
- 如果服务实例所在的集群实例clusterMap没有缓存,则新建设置默认配置并缓存到clusterMap
- 获取集群下所有实例
- 更新实例列表,这里还有一部分逻辑
- 调用PushService,通知客户端服务发生改变
@CanDistro@PutMapping("/beat")@Secured(action = ActionTypes.WRITE)public ObjectNode beat(HttpServletRequest request) throws Exception {// 主要获取处理心跳入参,并校验ObjectNode result = JacksonUtils.createEmptyJsonNode();result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval());String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);RsInfo clientBeat = null;if (StringUtils.isNotBlank(beat)) {clientBeat = JacksonUtils.toObj(beat, RsInfo.class);}String clusterName = WebUtils.optional(request, CommonParams.CLUSTER_NAME, UtilsAndCommons.DEFAULT_CLUSTER_NAME);String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));if (clientBeat != null) {if (StringUtils.isNotBlank(clientBeat.getCluster())) {clusterName = clientBeat.getCluster();} else {// fix #2533clientBeat.setCluster(clusterName);}ip = clientBeat.getIp();port = clientBeat.getPort();}String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);NamingUtils.checkServiceNameFormat(serviceName);Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}, namespaceId: {}", clientBeat,serviceName, namespaceId);BeatInfoInstanceBuilder builder = BeatInfoInstanceBuilder.newBuilder();builder.setRequest(request);// 处理心跳int resultCode = getInstanceOperator().handleBeat(namespaceId, serviceName, ip, port, clusterName, clientBeat, builder);// 返回处理结果result.put(CommonParams.CODE, resultCode);result.put(SwitchEntry.CLIENT_BEAT_INTERVAL,getInstanceOperator().getHeartBeatInterval(namespaceId, serviceName, ip, port, clusterName));result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());return result;}
主要获取处理心跳入参,并校验。由于是服务端,所以使用InstanceOperatorServiceImpl处理心跳逻辑。
@Overridepublic int handleBeat(String namespaceId, String serviceName, String ip, int port, String cluster,RsInfo clientBeat, BeatInfoInstanceBuilder builder) throws NacosException {com.alibaba.nacos.naming.core.Instance instance = serviceManager.getInstance(namespaceId, serviceName, cluster, ip, port);// 如果服务实例还没有注册先注册if (instance == null) {if (clientBeat == null) {return NamingResponseCode.RESOURCE_NOT_FOUND;}Loggers.SRV_LOG.warn("[CLIENT-BEAT] The instance has been removed for health mechanism, "+ "perform data compensation operations, beat: {}, serviceName: {}", clientBeat, serviceName);instance = parseInstance(builder.setBeatInfo(clientBeat).setServiceName(serviceName).build());serviceManager.registerInstance(namespaceId, serviceName, instance);}// 从serviceMap获取服务Service service = serviceManager.getService(namespaceId, serviceName);// 校验服务不能为空,为空抛异常处理serviceManager.checkServiceIsNull(service, namespaceId, serviceName);if (clientBeat == null) {clientBeat = new RsInfo();clientBeat.setIp(ip);clientBeat.setPort(port);clientBeat.setCluster(cluster);}// 委托Service处理心跳service.processClientBeat(clientBeat);return NamingResponseCode.OK;}
主要逻辑:
- 如果服务实例还没有注册先注册
- 根据命名空间、服务名从本地缓存serviceMap获取服务
- 校验服务不能为空,为空抛异常处理
- 若clientBeat为空则新建封装,委托Service的processClientBeat(clientBeat)处理心跳


ClientBeatProcessor也是Runnable的子类,构建ClientBeatProcessor后,底层还是委托GlobalExecutor的执行器封装中心提交执行任务的。
@Overridepublic void run() {Service service = this.service;if (Loggers.EVT_LOG.isDebugEnabled()) {Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", rsInfo.toString());}String ip = rsInfo.getIp();String clusterName = rsInfo.getCluster();int port = rsInfo.getPort();Cluster cluster = service.getClusterMap().get(clusterName);List instances = cluster.allIPs(true);for (Instance instance : instances) {if (instance.getIp().equals(ip) && instance.getPort() == port) {if (Loggers.EVT_LOG.isDebugEnabled()) {Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", rsInfo.toString());}// 设置当前心跳时间,用于服务健康检查比对是否大于设置阈值以决定服务是否剔除下线instance.setLastBeat(System.currentTimeMillis());// 既然接收到了客户端的心跳请求,证明服务可用了,这里做恢复服务健康状态并通知客户端// 服务可用了if (!instance.isMarked() && !instance.isHealthy()) {instance.setHealthy(true);Loggers.EVT_LOG.info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, region: {}, msg: client beat ok",cluster.getService().getName(), ip, port, cluster.getName(),UtilsAndCommons.LOCALHOST_SITE);getPushService().serviceChanged(service);NotifyCenter.publishEvent(new NamingTraceEvent.HealthStateChangeTraceEvent(System.currentTimeMillis(),service.getNamespaceId(), service.getGroupName(), service.getName(), instance.getIp(),true, HealthStateChangeReason.HEARTBEAT_REFRESH));}}}}
设置当前心跳时间,用于服务健康检查比对是否大于设置阈值以决定服务是否剔除下线(步骤3.6)。既然接收到了客户端的心跳请求,证明服务可用了,这里做恢复服务健康状态并通知客户端服务可用了