持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情
前言
之前学习Eureka的注册与发现,了解了关于Eureka客户端和服务的注册与发现,内部是通过定时任务发送心跳机制注册在服务端,为其他客户端提供服务的,由于Eureka的闭源,包括考虑到它在开发过程中的其实存在着很多的问题,包括服务的不能去做下线操作,没有可视化控制台,只能支持AP的原则,后面的日常开发中还是使用Nacos,无论是服务的上线、下线,配置中心,CP、AP的原则等…都给我们在日常开发中提供了很多方便之处,所以今天从最基本的Nacos客户端注册的原理来学习下。
源码分析
1.NacosNamingService
首先找到nacos-client包,开启客户端的源码分析,首先看naming目录下的NacosNamingService类实现NamingService接口,提供了很多registerInstance方法来提供客户端服务的注册
接下来主要看这个registerInstance方法,入参包括serviceName(服务名)、 groupName(分组名称)、Instance(注册实例) 。 BeatInfo类去接收构建心跳实例信息,服务名、端口号、ip、集群name
addBeatInfo()方法还是通过new了一个ConcurrentHashMap用来存储实例信息,Period这个比较关键,相当于一个心跳周期,心跳的发送间隔、健康检查间隔。
preserved.heart.beat.interval: 1000 #该实例在客户端上报心跳的间隔时间。(单位:毫秒)
preserved.heart.beat.timeout: 3000 #该实例在不发送心跳后,从健康到不健康的时间。(单位:毫秒)
preserved.ip.delete.timeout: 3000 #该实例在不发送心跳后,被nacos下掉该实例的时间。(单位:毫秒)
在看Period的周期里面的配置,就是一些心跳检查和健康检查的间隔时间的配置,心跳周期5s、心跳超市时间15s、实例删除的超时时间30s
Schedule延时定时任务会根据Period周期里面的定义的时间,去定时执行任务。
2.客户端的心跳发送
BeatTask内部类实现Runnable方法,开启一个线程来进行心跳发送,发送的方法就是sendBeat。
可以看到也是对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.服务端的心跳检查
processClientBeat心跳检查方法,主要ClientBeatProcessor类是一个线程,run方法里大致就是,如果是Cluster集群从集群里面获取客户端实例,然后遍历实例,如果客户端实例的ip和端口号和服务端一样的话,instance.setLastBeat(System./currentTimeMillis/()); 设置实例上一次发送心态的时间,进行续约操作,如果!instance.isHealthy() 健康状态不是处于健康,那么则设置为健康状态,instance.setHealthy(true);
服务端在接收到客户端服务的初始化的时候,会去用ClientBeatCheckTask 这个线程类的run方法,当前时间和上一次发送心跳的时间差如果大于最大的心跳时间15s,来判断是否将实例的健康状态设置为健康或不健康。
如果超时过了preserved.ip.delete.timeout默认30s,则进行实例的删除。
总结
这就是Nacos的客户端向服务端发送心跳,以及服务端接收心跳,进行服务的心跳检查的简单源码分析,从源码可以看出,发送的心态机制离不开定时任务和延迟线程队列。