Nacos客户端和服务端心跳机制(源码分析)

792 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

前言

之前学习Eureka的注册与发现,了解了关于Eureka客户端和服务的注册与发现,内部是通过定时任务发送心跳机制注册在服务端,为其他客户端提供服务的,由于Eureka的闭源,包括考虑到它在开发过程中的其实存在着很多的问题,包括服务的不能去做下线操作,没有可视化控制台,只能支持AP的原则,后面的日常开发中还是使用Nacos,无论是服务的上线、下线,配置中心,CP、AP的原则等…都给我们在日常开发中提供了很多方便之处,所以今天从最基本的Nacos客户端注册的原理来学习下。

源码分析

1.NacosNamingService

35B9DC46-200D-4CE1-82F3-444DE53E38CE.png

59EC2B24-2051-4D49-A370-EB71A396BAED.png

首先找到nacos-client包,开启客户端的源码分析,首先看naming目录下的NacosNamingService类实现NamingService接口,提供了很多registerInstance方法来提供客户端服务的注册

20C4BE28-57E1-48F5-9E70-C7C90E8A9745.png

接下来主要看这个registerInstance方法,入参包括serviceName(服务名)、 groupName(分组名称)、Instance(注册实例) 。 BeatInfo类去接收构建心跳实例信息,服务名、端口号、ip、集群name

5B47395E-090B-4369-8A7A-BC37D2EE7658.png

addBeatInfo()方法还是通过new了一个ConcurrentHashMap用来存储实例信息,Period这个比较关键,相当于一个心跳周期,心跳的发送间隔、健康检查间隔。

B6A3723F-F73F-494B-9311-5EC011798680.png

preserved.heart.beat.interval: 1000 #该实例在客户端上报心跳的间隔时间。(单位:毫秒)
preserved.heart.beat.timeout: 3000 #该实例在不发送心跳后,从健康到不健康的时间。(单位:毫秒)
preserved.ip.delete.timeout: 3000 #该实例在不发送心跳后,被nacos下掉该实例的时间。(单位:毫秒)

在看Period的周期里面的配置,就是一些心跳检查和健康检查的间隔时间的配置,心跳周期5s、心跳超市时间15s、实例删除的超时时间30s

Schedule延时定时任务会根据Period周期里面的定义的时间,去定时执行任务。

2.客户端的心跳发送

75D40276-36FB-4B80-8762-323FB3FC771A.png BeatTask内部类实现Runnable方法,开启一个线程来进行心跳发送,发送的方法就是sendBeat。

E4CA8171-94CD-474C-A299-1DE21F943783.png 可以看到也是对BeatInfo对象进行encode,组装param参数,reqApi方法put请求/v1/ns/instance/beat nacos服务端的接口,发送心跳实例

3.服务端发现客户端心跳

//**/
/ * Create a beat for instance./
/ */
/ */*@param*/request http request/
/ */*@return*/detail information of instance/
/ */*@throws*/Exception any error during handle/
/ *//
@CanDistro
@PutMapping(“/beat”)
@Secured(parser = NamingResourceParser.class, 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 #2533
            clientBeat.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: {}”, clientBeat, serviceName);
    Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port);
    
    if (instance == null) {
        if (clientBeat == null) {
            result.put(CommonParams./CODE/, NamingResponseCode./RESOURCE_NOT_FOUND/);
            return result;
        }
        
        Loggers./SRV_LOG/.warn(“[CLIENT-BEAT] The instance has been removed for health mechanism, “
                + “perform data compensation operations, beat: {}, serviceName: {}”, clientBeat, serviceName);
        
        instance = new Instance();
        instance.setPort(clientBeat.getPort());
        instance.setIp(clientBeat.getIp());
        instance.setWeight(clientBeat.getWeight());
        instance.setMetadata(clientBeat.getMetadata());
        instance.setClusterName(clusterName);
        instance.setServiceName(serviceName);
        instance.setInstanceId(instance.getInstanceId());
        instance.setEphemeral(clientBeat.isEphemeral());
        
        serviceManager.registerInstance(namespaceId, serviceName, instance);
    }
    
    Service service = serviceManager.getService(namespaceId, serviceName);
    
    if (service == null) {
        throw new NacosException(NacosException./SERVER_ERROR/,
                “service not found: “ + serviceName + “@“ + namespaceId);
    }
    if (clientBeat == null) {
        clientBeat = new RsInfo();
        clientBeat.setIp(ip);
        clientBeat.setPort(port);
        clientBeat.setCluster(clusterName);
    }
    service.processClientBeat(clientBeat);
    
    result.put(CommonParams./CODE/, NamingResponseCode./OK/);
    if (instance.containsMetadata(PreservedMetadataKeys./HEART_BEAT_INTERVAL/)) {
        result.put(SwitchEntry./CLIENT_BEAT_INTERVAL/, instance.getInstanceHeartBeatInterval());
    }
    result.put(SwitchEntry./LIGHT_BEAT_ENABLED/, switchDomain.isLightBeatEnabled());
    return result;
}

就是前面客户通过reqApi方法调的那个/v1/ns/instance/beat这个接口,我们找到Nacos服务端接口来看下。 我把整个方法的代码贴了出来,主要有这么几个方法:

1.serviceManager.getInstance,检查实例是否存在

2.serviceManager.registerInstance,如果不存在就去将这个实例注册到注册中心里去

3.serviceManager.getService,去注册中心获取实例

4.service.processClientBeat ,进行心跳检查

4.服务端的心跳检查

5260A222-2C08-44E5-A114-A408A2AEBE5E.png

8FF1279A-45E4-40B3-A259-4FC5CFBB6EA7.png

processClientBeat心跳检查方法,主要ClientBeatProcessor类是一个线程,run方法里大致就是,如果是Cluster集群从集群里面获取客户端实例,然后遍历实例,如果客户端实例的ip和端口号和服务端一样的话,instance.setLastBeat(System./currentTimeMillis/()); 设置实例上一次发送心态的时间,进行续约操作,如果!instance.isHealthy() 健康状态不是处于健康,那么则设置为健康状态,instance.setHealthy(true);

57206E2C-29BD-4952-B106-FD3A8541D5D4.png

服务端在接收到客户端服务的初始化的时候,会去用ClientBeatCheckTask 这个线程类的run方法,当前时间和上一次发送心跳的时间差如果大于最大的心跳时间15s,来判断是否将实例的健康状态设置为健康或不健康。

2D2B06D4-CF06-45CA-868C-3489D2E33460.png

如果超时过了preserved.ip.delete.timeout默认30s,则进行实例的删除。

总结

这就是Nacos的客户端向服务端发送心跳,以及服务端接收心跳,进行服务的心跳检查的简单源码分析,从源码可以看出,发送的心态机制离不开定时任务和延迟线程队列。