Nacos服务注册与心跳机制源码分析

Nacos是 Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos 致力于帮助您发现、配置和管理微服务。

注册中心的接口心跳检测原理_注册中心的接口心跳检测原理

服务注册

在nacos的依赖JAR包中,采用了自动装配在spring.factories中

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\
  com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration,\
  com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\
  com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\
  com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,\
  com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClientConfiguration,\
  com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration,\
  com.alibaba.cloud.nacos.NacosServiceAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
  com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration

其中NacosServiceRegistryAutoConfiguration类中,声明三个Bean对象,分别是NacosServiceRegistry、NacosRegistration、NacosAutoServiceRegistration

注册中心的接口心跳检测原理_开发语言_02


同时该类继承了接口ApplicationListener监听器,跟踪事件监听的回调逻辑:

@Override
	@SuppressWarnings("deprecation")
	public void onApplicationEvent(WebServerInitializedEvent event) {
		bind(event);
	}

WebServerInitializedEvent该事件在容器完成刷新工作后发布的,具体方法为ServletWebServerApplicationContext#finishRefresh

protected void finishRefresh() {
		super.finishRefresh();
		WebServer webServer = startWebServer();
		if (webServer != null) {
			publishEvent(new ServletWebServerInitializedEvent(webServer, this));
		}
	}

从上文可以看到WebServerInitializedEvent事件是在开启web容器后,由Spring框架发布。

同时在回调的处理逻辑中,会发送两个消息InstancePreRegisteredEvent、InstanceRegisteredEvent。两个消息中间,则是服务注册的逻辑,来到NacosServiceRegistry#register中,此时用到了NamingService

public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        NamingUtils.checkInstanceIsLegal(instance);
        String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
        if (instance.isEphemeral()) {
            BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
            beatReactor.addBeatInfo(groupedServiceName, beatInfo);
        }
        serverProxy.registerService(groupedServiceName, groupName, instance);
    }

serverProxy.registerService(groupedServiceName, groupName, instance);

这里看到如果是临时节点,增加了BeatInfo(先记住这点),这与Nacos中临时节点使用的主动心跳逻辑是对得上的。

默认会进行3次尝试注册,直至成功

public static String webContext = "/nacos";
public static String nacosUrlBase = webContext + "/v1/ns"; 
public static String nacosUrlInstance = nacosUrlBase + "/instance";

HttpRestResult<String> restResult = nacosRestTemplate
                    .exchangeForm(url, header, Query.newInstance().initParams(params), body, method, String.class);

调用Rest接口:/nacos/v1/ns/instance;

所以总结下来原理就是监听Spring的内部事件,通过nacos提供的rest接口,调用注册。

服务监听

由于不可能每次访问服务,则要现查或去查库,数据是需要在本地缓存的,所以就存在缓存与实际数据的一致性问题。Nacos针对临时节点和永久节点采用了不同的心跳策略。

两者区别

临时实例只是临时存在于注册中心中,会在服务下线或不可用时被注册中心剔除,临时实例会与注册中心保持心跳,注册中心会在⼀段时间没有收到来自客户端的心跳后会将实例设置为不健康,然后在⼀段时间后进行剔除。

永久实例在被删除之前会永久的存在于注册中心,且有可能并不知道注册中心存在,不会主动向注册中心上报心跳,那么这个时候就需要注册中心主动进行探活。

策略区别

  • 针对临时节点是主动心跳

自动装配类,NacosDiscoveryEndpointAutoConfiguration会声明NacosDiscoveryHealthIndicator

还记得上节中说到在注册时,如是临时节点,会增加一个BeatInfo

public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        //fix #1733
        if ((existBeat = dom2Beat.remove(key)) != null) {
            existBeat.setStopped(true);
        }
        dom2Beat.put(key, beatInfo);
        executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
    }

在该逻辑,发现有一个线程周期调用逻辑(5秒)

@Override
        public void run() {
            if (beatInfo.isStopped()) {
                return;
            }
            long nextTime = beatInfo.getPeriod();
            try {
                //发送心跳/nacos/v1/ns/instance/beat
                JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
                long interval = result.get("clientBeatInterval").asLong();
                boolean lightBeatEnabled = false;
                if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
                    lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean();
                }
                BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
                if (interval > 0) {
                    nextTime = interval;
                }
                int code = NamingResponseCode.OK;
                if (result.has(CommonParams.CODE)) {
                    code = result.get(CommonParams.CODE).asInt();
                }
                if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
                    Instance instance = new Instance();
                    instance.setPort(beatInfo.getPort());
                    instance.setIp(beatInfo.getIp());
                    instance.setWeight(beatInfo.getWeight());
                    instance.setMetadata(beatInfo.getMetadata());
                    instance.setClusterName(beatInfo.getCluster());
                    instance.setServiceName(beatInfo.getServiceName());
                    instance.setInstanceId(instance.getInstanceId());
                    instance.setEphemeral(true);
                    try {
                        serverProxy.registerService(beatInfo.getServiceName(),
                                NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
                    } catch (Exception ignore) {
                    }
                }
            } catch (NacosException ex) {
                NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                        JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());
                
            }
            executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
        }
  • 永久节点是主动探测(ping、http指定接口)

Nacos 中使用 SDK 对于永久实例的注册实际也是使用 OpenAPI 的方式进行注册,这样可以保证即使是客户端下线后也不会影响永久实例的健康检查。
对于永久实例的的监看检查,Nacos 采用的是注册中心探测机制,注册中心会在永久服务初始化时根据客户端选择的协议类型注册探活的定时任务。Nacos 现在内置提供了三种探测的协议,

即Http、TCP 以及 MySQL

⼀般而言 Http 和 TCP 已经可以涵盖绝大多数的健康检查场景。MySQL 主要用于特殊的业务场景,例如数据库的主备需要通过服务名对外提供访问,需要确定当前访问数据库是否为主库时,那么我们此时的健康检查接口,是⼀个检查数据库是否为主库的 MySQL命令。
永久实例会在被主动删除前⼀直存在于注册中心,那么我们健康检查并不会去删除实例,所以我们只需要在负责的节点永久实例健康状态变更的时候通知到其余的节点即可。另外 Nacos 也是提供了对应的不希望去校验其健康状态的白名单配置。