一、引言

   本章节是第一阶段最后一篇,那么我们今天要学习的源码内容是 “服务下线”.

   当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 从注册表当中进行删除。

第一阶段源码分析图:

GitHub 加速计划 / na / nacos
29.83 K
12.75 K
下载
Nacos是由阿里巴巴开源的服务治理中间件,集成了动态服务发现、配置管理和服务元数据管理功能,广泛应用于微服务架构中,简化服务治理过程。
最近提交(Master分支:3 个月前 )
4334cd16 * Support custom client configuration timeout.(#12748) * Add UT.(#12748) 19 天前
b04d2266 23 天前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐