一、引言
本章节是第一阶段最后一篇,那么我们今天要学习的源码内容是 “服务下线”.
当Nacos客户端下线的时候,是要去通知服务端,告诉服务端 “ 我已经下线,不可用了 ”。并且在服务下线时,还要去通知其他客户端服务更新本地缓存列表,避免调用到已经下线的实例。
本章重点:
- Nacos 客户端是怎么下线通知服务端的 ?
- Nacos 服务端收到客户端的下线通知,做了什么操作 ?
- 服务下线时,Nacos 服务端是怎么通知其他客户端更新本地缓存列表的 ?
二、目录
目录
一、引言
二、目录
三、客户端服务下线源码分析
四、服务端服务下线源码分析
五、变动事件发布源码分析
六、本章总结
七、第一阶段总结
三、客户端服务下线源码分析
主线任务:Nacos 客户端是怎么下线通知服务端的 ?
首先我们要先找到服务下线的代码入口在哪里 ?
当我们关闭Nacos客户端服务的时候,日志会打印出 [DEREGISTER-SERVICE] :销毁服务的意思,那我们直接根据这个 进行全局搜索,找到代码位置
可以看到这里发起调用 Nacos 服务端删除实例接口,那我们接着往上看,看看这个接口哪里调用了 ?
public void deregisterService(String serviceName, Instance instance) throws NacosException {NAMING_LOGGER.info("[DEREGISTER-SERVICE] {} deregistering service {} with instance: {}", namespaceId, serviceName,instance);// 组装请求参数final Map<String, String> params = new HashMap<String, String>(8);params.put(CommonParams.NAMESPACE_ID, namespaceId);params.put(CommonParams.SERVICE_NAME, serviceName);params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());params.put("ip", instance.getIp());params.put("port", String.valueOf(instance.getPort()));params.put("ephemeral", String.valueOf(instance.isEphemeral()));// 调用Nacos服务端删除接口方法,请求地址:/nacos/v1/ns/instancereqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.DELETE);
}
调用链路:destroy() -> stop() -> deregister() -> namingService.deregisterInstance(serviceId, group, registration.getHost(),
registration.getPort(), nacosDiscoveryProperties.getClusterName()) -> deregisterInstance(serviceName, groupName, instance); ->
serverProxy.deregisterService(NamingUtils.getGroupedName(serviceName, groupName), instance) -> reqApi(UtilAndComs.nacosUrlInstance,
params, HttpMethod.DELETE);
最终看到是在 AbstractAutoServiceRegistration 类中的 destroy() 方法中调用了,这个方法还被 @PreDestroy修饰。
@PreDestroy:当Spring容器销毁的时候,会回调被这些注解修饰的方法
小结:
在 AbstractAutoServiceRegistration 类中 destroy() 方法被@PreDestroy修饰,在Spring容器销毁的时候会去执行这个方法,从而调用 Nacos 服务端的删除实例接口,地址:/nacos/v1/ns/instance
四、服务端服务下线源码分析
主线任务:Nacos 服务端收到客户端的下线通知,做了什么操作 ?
通过请求路径得知,最终是在服务端 InstanceController 类中的 deregister 方法
@CanDistro
@DeleteMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String deregister(HttpServletRequest request) throws Exception {// 获取参数Instance instance = getIpAddress(request);String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);NamingUtils.checkServiceNameFormat(serviceName);Service service = serviceManager.getService(namespaceId, serviceName);if (service == null) {Loggers.SRV_LOG.warn("remove instance from non-exist service: {}", serviceName);return "ok";}// 调用删除 instance 实例方法serviceManager.removeInstance(namespaceId, serviceName, instance.isEphemeral(), instance);return "ok";
}
可以看到下面整体逻辑方法跟服务注册代码基本一样,不同的点就在于 substractIpAddresses(service, ephemeral, ips); 这个方法, action 参数 一个传的是 add,一个传的是 remover,后面就跟注册服务代码逻辑完全就是一样的了
public void removeInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)throws NacosException {Service service = getService(namespaceId, serviceName);synchronized (service) {// 删除 instanceremoveInstance(namespaceId, serviceName, ephemeral, service, ips);}
}private void removeInstance(String namespaceId, String serviceName, boolean ephemeral, Service service,Instance... ips) throws NacosException {// 和注册服务逻辑一样,创建KeyString key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);// 在这个 instanceList 当中会移除不需要包含的 instance 实例List<Instance> instanceList = substractIpAddresses(service, ephemeral, ips);// 包装数据 Instances 对象Instances instances = new Instances();instances.setInstanceList(instanceList);// 后面就和注册服务逻辑完全一样,整体还是利用 异步任务 + 内存队列 的设计,最后包装成任务丢入到阻塞队列当中。// 丢入到阻塞队列后,后台开启一条线程,不断从队列中获取任务,最后利 用写使复制的方式,把数据写入到 Nacos 注册表当中!consistencyService.put(key, instances);
}private List<Instance> substractIpAddresses(Service service, boolean ephemeral, Instance... ips)throws NacosException {// 在 updateIpAddresses 方法中,如果action 为 remove,会在最后返回把对应的 instance 删除// 调用 updateIpAddresses 方法,这里 action 传的是 remove (注册服务这里传的是 add)return updateIpAddresses(service, UtilsAndCommons.UPDATE_INSTANCE_ACTION_REMOVE, ephemeral, ips);
}
那有的小伙伴就会好奇了,也没看到 删除 Naocs实例数据的代码,怎么就把服务实例移除了!
重点还是在 substractIpAddresses 这个方法,在这个方法当中会把不需要 instance 实例列表进行移除,返回的 instanceList 就是最终需要替换的数据。然后就和服务注册一样的代码逻辑,异步任务 + 内存队列的设计,利用写时替换的方式,更新Nacos注册表的数据。
// 在这个 instanceList 当中会移除不需要包含的 instance 实例
List<Instance> instanceList = substractIpAddresses(service, ephemeral, ips);private List<Instance> substractIpAddresses(Service service, boolean ephemeral, Instance... ips)throws NacosException {// 在 updateIpAddresses 方法中,如果action 为 remove,会在最后返回把对应的 instance 删除// 调用 updateIpAddresses 方法,这里 action 传的是 remove (注册服务这里传的是 add)return updateIpAddresses(service, UtilsAndCommons.UPDATE_INSTANCE_ACTION_REMOVE, ephemeral, ips);
}
小结:
Nacos 服务端收到客户端服务下线的接口请求后,会把 instance 实例列表进行移除,然后就和服务注册代码逻辑一样,利用写时替换的方式,更新Nacos注册表的数据。
五、变动事件发布源码分析
通过服务发现的篇章我们可以得知,Nacos的客户端服务是有定时任务去维护本地缓存列表的。
这样的话,本地缓存列表还是有延时的 ,不能完全跟Nacos注册表数据保持一致 ?
其实在服务注册、服务下线,更改完Nacos注册表数据,服务端是会发布一个变动事件,然后通过 udp 的方式,去通知每一个客户端服务,从而让客户端感知速度更快。
接下来我们就分析一下这段代码,看看如何来实现的?
在异步onChange方法中,最后调用了updateIPs方法,在这个方法中,有这么一段代码,修改完Nacos注册表数据,就会去 利用 udp 方式来通知客户端。那我们来看下这段代码是怎么实现的 ?
// 针对每一个 clusterName,修改实例列表
for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {List<Instance> entryIPs = entry.getValue();clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
}setLastModifiedMillis(System.currentTimeMillis());
// 利用 udp 方式来通知客户端
getPushService().serviceChanged(this);
在 serviceChanged 方法中,去发布了一个事件 ServiceChangeEvent 方法,那我们具体看 ServiceChangeEvent 方法中的逻辑。
public void serviceChanged(Service service) {// merge some change events to reduce the push frequency:if (futureMap.containsKey(UtilsAndCommons.assembleFullServiceName(service.getNamespaceId(), service.getName()))) {return;}// 发布 服务改变 事件this.applicationContext.publishEvent(new ServiceChangeEvent(this, service));
}
在 IDEA 中对这个 ServiceChangeEvent 方法进行全局搜索
在 onApplicationEvent 中,我们就看主要代码,主要用 udp 方式去通知每一个客户端服务!
@Override
public void onApplicationEvent(ServiceChangeEvent event) {Future future = GlobalExecutor.scheduleUdpSender(() -> {try {// 遍历需要通知的 客户端for (PushClient client : clients.values()) {udpPush(ackEntry);}} catch (Exception e) {Loggers.PUSH.error("[NACOS-PUSH] failed to push serviceName: {} to client, error: {}", serviceName, e);} finally {futureMap.remove(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));}}, 1000, TimeUnit.MILLISECONDS);
}
从这段代码我们就能看出,在 Nacos 服务端如果注册表中发生了变动,是会主动去通知客户端的,但是协议使用的是:UDP,这种协议比较轻量化,它无需建立连接就可以发送封装的 IP 数据包的方法,这种方式传输其实是不靠谱的。不靠谱也没关系,每一个客户端本地还有一个定时任务会去更新本地实例列表缓存的,所以影响不大。
六、本章总结
当Spring容器销毁的时候,首先我们知道Nacos客户端服务下线,是会调用服务端删除实例的接口,在这个接口当中,会把 instance 实例列表进行移除,然后就和服务注册代码逻辑一样,利用写时替换的方式,更新Nacos注册表的数据。在Nacos注册数据表变动后,服务端是发布一个事件,然后利用 udp 的方式去通知每一个客户端服务。
七、第一阶段总结
前面已经讲了带上本章节,一共八节,我们来总结下分析过的源码内容:
客户端:
注册服务:Spring容器启动,Nacos客户端利用事件监听,从而调用Nacos服务端 服务实例注册接口。在调用服务注册之前,客户端会开启一个 心跳健康检查异步任务。
服务之间调用:在客户端进行服务之间调用时,Nacos整合了Ribbon,从而查询Nacos服务实例列表,来维护本地缓存数据,然后进行负载均衡服务调用。
服务下线:在Spring容器销毁的时候,会触发Nacos销毁的方法,会去调用服务端服务下线接口,从而完成服务下线流程
服务端:
我们分析几个核心功能:服务注册、服务查询、服务下线、心跳健康。
服务注册:在服务注册的时候,我们讲了是利用 异步任务+内存队列的设计来完成的,最后是通过 写时复制来往Nacos注册表当中写入数据。
服务查询:服务查询查询的话,是直接从Nacos注册表当中获取Instance 列表。
服务下线:在服务下线的时候,会把 instance 实例列表进行移除,利用写时替换的方式,更新Nacos注册表的数据。在Nacos注册数据表变动后,服务端是发布一个事件,然后利用 udp 的方式去通知每一个客户端服务。
心跳健康:服务端会开启心跳健康检查任务,把 lastBeat 跟当前时间比超过 15s,就会被标识为不健康的实例,把lastBeat 跟当前时间比超过 30s,Nacos 会把该 Instance 从注册表当中进行删除。
第一阶段源码分析图: