Nacos 进阶篇---Nacos服务下线做了哪些事情 ?(八)
一、引言
本章节是第一阶段最后一篇,那么我们今天要学习的源码内容是 “服务下线”.
当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/instance
reqApi(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) {
// 删除 instance
removeInstance(namespaceId, serviceName, ephemeral, service, ips);
}
}
private void removeInstance(String namespaceId, String serviceName, boolean ephemeral, Service service,
Instance... ips) throws NacosException {
// 和注册服务逻辑一样,创建Key
String 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 从注册表当中进行删除。
第一阶段源码分析图:
更多推荐
所有评论(0)