Nacos Server 端服务注册流程源码分析

前言:

上一篇我们从 Nacos Client 分析了了服务注册流程,本篇我们从 Nacos Server 端来分析 Nacos 服务注册流程。

Nacos 系列文章传送门:

Nacos 初步认识和 Nacos 部署细节

Nacos 配置管理模型 – 命名空间(Namespace)、配置分组(Group)和配置集ID(Data ID)

Nacos 注册中心和配置中心【实战】

服务启动何时触发 Nacos 的注册流程?

Nacos Client 端服务注册流程源码分析

Nacos Server 端服务注册流程源码分析

上一篇我们有分析到 Nacos Client 提交注册的地址是:/nacos/v1/ns/instance,我们在 nacos-server 源码中找到该接口,也就找到了服务端的注册逻辑,它位于 naming 模块中的 controllers 包下的 InstanceController 中,源码如下:

//com.alibaba.nacos.naming.controllers.InstanceController#register
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
	//获取注册服务的 namespaceId  默认值是 public
	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 = parseInstance(request);
	//注册服务
	serviceManager.registerInstance(namespaceId, serviceName, instance);
	return "ok";
}


//com.alibaba.nacos.naming.controllers.InstanceController#parseInstance
private Instance parseInstance(HttpServletRequest request) throws Exception {
	//获取注册服务的名称
	String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
	//获取 app 没有配置默认 DEFAULT
	String app = WebUtils.optional(request, "app", "DEFAULT");
	//获取注册服务的 ip port 健康状态 权重等信息封装为 instance
	Instance instance = getIpAddress(request);
	//设置 app
	instance.setApp(app);
	//设置 服务名称
	instance.setServiceName(serviceName);
	// Generate simple instance id first. This value would be updated according to
	// INSTANCE_ID_GENERATOR.
	//设置 InstanceId 格式:getIp() + "#" + getPort() + "#" + getClusterName() + "#" + getServiceName()
	instance.setInstanceId(instance.generateInstanceId());
	//设置当前时间为最后一次心跳时间
	instance.setLastBeat(System.currentTimeMillis());
	//获取元数据
	String metadata = WebUtils.optional(request, "metadata", StringUtils.EMPTY);
	//元数据为空判断
	if (StringUtils.isNotEmpty(metadata)) {
		//机械元数据后 设置元数据
		instance.setMetadata(UtilsAndCommons.parseMetadata(metadata));
	}
	//实例格式校验
	//权重范围校验 MAX_WEIGHT_VALUE = 10000.0D MIN_WEIGHT_VALUE = 0.00D;
	instance.validate();
	
	return instance;
}

register 方法会从请求对象中拿到注册的参数,例如IP、权重、健康状况等,然后封装为 instance对象,调用 ServiceManager#registerInstance 完成服务注册。

ServiceManager#registerInstance 方法源码分析

ServiceManager#registerInstance 方法会尝试从服务注册表 serviceMap 中获取到服务实例,如果没有就会创建一个 Service,并设置好属性 GroupName namespaceId serviceName,然后存储到 ServiceManager 的服务注册表 ConcurrentHashMap 中,并调用 Service#init 方法实现心跳检测(设置实例健康状态、剔除心跳超时的服务实例),使用监听机制完成数据一致性监听。

//com.alibaba.nacos.naming.core.ServiceManager#registerInstance
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
	
	//创建空服务
	//会尝试从服务注册表 serviceMap 中获取到服务实例 如果没有就会创建一个 Service 并设置好属性 GroupName namespaceId serviceName 然后存储到 ServiceManager 的一个 ConcurrentHashMap 中(服务注册表)
	createEmptyService(namespaceId, serviceName, instance.isEphemeral());
	//从注册表中获取服务 先根据 namespaceId 取得到 Map<String,Service> 然后再根据 serviceName 获取 Service
	Service service = getService(namespaceId, serviceName);
	//为空判断
	if (service == null) {
		//为空抛出参数无效异常
		throw new NacosException(NacosException.INVALID_PARAM,
				"service not found, namespace: " + namespaceId + ", service: " + serviceName);
	}
	//添加 instance 服务实例到注册表
	addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}

//com.alibaba.nacos.naming.core.ServiceManager#createEmptyService
public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {
	createServiceIfAbsent(namespaceId, serviceName, local, null);
}

//com.alibaba.nacos.naming.core.ServiceManager#createServiceIfAbsent
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
		throws NacosException {
	
	//根据 namespaceId 从 serviceMap 中获取  Map<String,Service> map 接着使用 serviceName 从 map 中获取 Service
	Service service = getService(namespaceId, serviceName);
	//service 为空判断
	if (service == null) {
		
		Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
		//创建一个空的 Service
		service = new Service();
		//设置 ServiceName
		service.setName(serviceName);
		//设置 namespaceId
		service.setNamespaceId(namespaceId);]
		//设置 GroupName
		service.setGroupName(NamingUtils.getGroupName(serviceName));
		// now validate the service. if failed, exception will be thrown
		service.setLastModifiedMillis(System.currentTimeMillis());
		service.recalculateChecksum();
		//集群为空判断
		if (cluster != null) {
			//设置到集群中
			cluster.setService(service);
			service.getClusterMap().put(cluster.getName(), cluster);
		}
		//service 校验
		service.validate();
		//put  serviceMap
		putServiceAndInit(service);
		if (!local) {
			addOrReplaceService(service);
		}
	}
}

//com.alibaba.nacos.naming.core.ServiceManager#putServiceAndInit
private void putServiceAndInit(Service service) throws NacosException {
	//保存 service
	putService(service);
	//再次获取 service 
	service = getService(service.getNamespaceId(), service.getName());
	//初始化 service
	service.init();
	//一致性监听
	consistencyService
			.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
	consistencyService
			.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
	Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}

//com.alibaba.nacos.naming.core.ServiceManager#putService
public void putService(Service service) {
	//根据 NamespaceId 判断 serviceMap 中是否有 service
	if (!serviceMap.containsKey(service.getNamespaceId())) {
		//不包含 sync 保证线程安全
		synchronized (putServiceLock) {
			//再次检查 service 是否存储  double check
			if (!serviceMap.containsKey(service.getNamespaceId())) {
				//不存在 NamespaceId 为空 value 为一个新的 map
				serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>());
			}
		}
	}
	//根据 NamespaceId 获取到 Map CAS 把 service 存入到 serviceMap
	serviceMap.get(service.getNamespaceId()).putIfAbsent(service.getName(), service);
}

Service#init 方法源码分析

Service#init 方法把 Service 封装到 ClientBeatCheckTask 对象中,ClientBeatCheckTask 是一个Runnable 线程对象,然后使用定时任务5s执行一次健康检查, ClientBeatCheckTask 的作用是检查并更新实例的状态,如果实例心跳超时,则将超时实例删除。

//com.alibaba.nacos.naming.core.Service#init
public void init() {
	//健康检查
	//clientBeatCheckTask 是一个Runnable 它持有 service 作用是检查并更新实例的状态 如果已过期 则删除
	HealthCheckReactor.scheduleCheck(clientBeatCheckTask);
	for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
		//设置 service 
		entry.getValue().setService(this);
		//初始化集群
		entry.getValue().init();
	}
}



//com.alibaba.nacos.naming.healthcheck.HealthCheckReactor#scheduleCheck(com.alibaba.nacos.naming.healthcheck.ClientBeatCheckTask)
public static void scheduleCheck(ClientBeatCheckTask task) {
	//定时检查服务的状态 5 秒一次
	futureMap.computeIfAbsent(task.taskKey(),
			k -> GlobalExecutor.scheduleNamingHealth(task, 5000, 5000, TimeUnit.MILLISECONDS));
}

ClientBeatCheckTask #run 方法源码分析

ClientBeatCheckTask 的 run 方法是真正执行健康检测的方法,它会判断当前时间和实例最后一次心跳的差值是否大于心跳超时时间,如果大于则设置当前实例不健康,并发布服务实例状态变更事件和心跳超时事件(默认15秒);如果当前时间和实例最后一次心跳的差值是否大于实例删除时间(默认30秒),则删除实例。

//com.alibaba.nacos.naming.healthcheck.ClientBeatCheckTask#run
@Override
public void run() {
	try {
		if (!getDistroMapper().responsible(service.getName())) {
			return;
		}
		//是否开启监控检查
		if (!getSwitchDomain().isHealthCheckEnabled()) {
			return;
		}
		//获取service的所有实例
		List<Instance> instances = service.allIPs(true);
		
		// first set health status of instances:
		//设置实例的健康状态
		for (Instance instance : instances) {
			//当前时间-实例最后的心跳时间 是否大于 实例心跳超时时间 默认 15 秒
			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());
						//发布服务状态变更事件 通知 nacos-client 服务已经下线
						getPushService().serviceChanged(service);
						//发布心跳超时事件
						ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance));
					}
				}
			}
		}
		//实例是否过期
		if (!getGlobalConfig().isExpireInstance()) {
			return;
		}
		
		// then remove obsolete instances:
		//删除过期的实例
		for (Instance instance : instances) {
			//实例被标记 跳过
			if (instance.isMarked()) {
				continue;
			}
			//当前时间-实例最后心跳时间 是否大于实例删除时间  默认 30 秒
			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);
	}
	
}

ServiceManager#addInstance 方法源码分析

ServiceManager#addInstance 方法中会拿到 service 中的 List 实例列表,然后重新设置到 Instances 中,调用 consistencyService 完成 Nacos 集群中的数据同步,值得注意的是在重新设置 Instance 值的时候使用了 CopyOnWriteArrayList,使用 CopyOnWriteArrayList 完成对实例状态更新,会用新的 Instance 列表直接覆盖旧 Instance 列表,这样在更新过程中,旧 Instance 列表不受影响,用户依然可以读取。

//com.alibaba.nacos.naming.core.ServiceManager#addInstance
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
		throws NacosException {
	//构建实例的 key 就是拼接 key
	String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
	//获取service
	Service service = getService(namespaceId, serviceName);
	//synchronized 保证线程安全
	synchronized (service) {
		//获取 service 中的所有 instance 包括当前对象
		List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
		//创建 Instances 对象
		Instances instances = new Instances();
		//重新赋值  这里使用了  CopyOnWriteArrayList
		instances.setInstanceList(instanceList);
		//使用 consistencyService.put() 方法完成 Nacos 集群的数据同步 保证集群数据一致性
		consistencyService.put(key, instances);
	}
}

public void setInstanceList(List<Instance> instanceList) {
	this.instanceList = new CopyOnWriteArrayList<>(instanceList);
}

至此,Nacos Server 端的注册主流程已经结束,但 Nacos Server 是如何通知 Nacos Client 服务下线以及如何保证 Nacos 集群中数据一致性,本篇暂不做分析,敬请期待后续分析。

欢迎提出建议及对错误的地方指出纠正。

  • 48
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值