«

Nacos源码学习计划-Day07-服务下线+第一阶段总结

ZealSinger 发布于 阅读:113 技术文档


当我们在关闭Nacos客户端的时候,就会同之服务端进行服务下线操作,并且在服务下线的时候,我们知道其他客户端中存在缓存的,所以其实也是需要通知其余客户端更新缓存信息的(当然,前面我们有分析过,在新版本的Nacos中,客户端的缓存已经不再是首查,而是作为兜底的存在,所以这个情况下相对而言就没那么需要通知客户端进行缓存更新操作)

这一章主要就是来看Nacos服务下线相关的内容,自然也还是从客户端如何触发下线和服务端如何处理下线请求两个方面进行学习

客户端服务下线源码分析

首先需要定位下线的相关操作的代码,当我们停止一个注册到了Nacos的客户端代码的时候可以看一下控制台输出

在控制台里面就发现了,有一行[DEREGISTER-SERVICE]销毁服务的 log 日志,有了这一个关键点,利用 IDEA 的检索功能,搜索是哪行代码打印出来的,搜索结果如图

image-20251026160823288

可以找到对应的输出这个销毁日志的地方,NamingHttpClientProxy中的deregisterService方法,这个方法的逻辑就老熟人了,Nacos客户端中所有的对于Nacos服务端发送Http请求的基本都是这个模板,所以这里很明显就是发送下线请求的地方,那么我们接下来就是顺着这个方法一层一层往上走,看看是哪里调用的

@Override
   public void deregisterService(String serviceName, String groupName, Instance instance) throws NacosException {
       NAMING_LOGGER
              .info("[DEREGISTER-SERVICE] {} deregistering service {} with instance: {}", namespaceId, serviceName,
                       instance);
       if (instance.isEphemeral()) {
           return;
      }
       final Map<String, String> params = new HashMap<>(16);
       params.put(CommonParams.NAMESPACE_ID, namespaceId);
       params.put(CommonParams.SERVICE_NAME, NamingUtils.getGroupedName(serviceName, groupName));
       params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
       params.put(IP_PARAM, instance.getIp());
       params.put(PORT_PARAM, String.valueOf(instance.getPort()));
       params.put(EPHEMERAL_PARAM, String.valueOf(instance.isEphemeral()));
       
       reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.DELETE);
  }

顺着看,发现调用改方法的地方就两个,都去看一下发现NacosNamingService中才是真正调用的地方

image-20251026161400127

NacosNamingService中调用该方法在类内部层层调用,实际上对应的方法应该是deregisterInstance

@Override
   public void deregisterInstance(String serviceName, String groupName, String ip, int port, String clusterName)
           throws NacosException {
       Instance instance = new Instance();
       instance.setIp(ip);
       instance.setPort(port);
       instance.setClusterName(clusterName);
       deregisterInstance(serviceName, groupName, instance);
  }

然后deregisterInstance被调用的地方有三个,前两个都是在NacosNamingService内部进行的调用,那么真正的外部调用就是在最后那个即NacosServiceRegistry中

image-20251026161952244

NacosServiceRegistry的deregister方法中调用了上述的deregisterInstance方法,而这个NacosServiceRegistry也就是我们当初看服务注册的时候,对应的register方法所在的类,说明我们的追踪是没问题的

然后继续看deregister被谁调用了,可以发现是AbstractAutoServiceRegistration调用了该方法,实在找不到可以直接看当初我们register的调用链路,代码设计肯定是满足单一职责的,注册和注销自然是一个业务,所以两者肯定是同一个类进行的调用的

在AbstractAutoServiceRegistration的stop方法中调用了deregister方法,而这个stop方法是被一个有@PreDestroy注解的destroy方法调用

@PreDestroy
public void destroy() {
stop();
}

@PreDestroy注解是Java EE 5引入的一部分,它并不是Spring框架特有的,而是Java自己的注解。这个注解用于标记在Bean销毁之前需要执行的方法,通常用于释放资源或执行清理操作。

所以到这里就很明显了,Nacos客户端下线功能是通过@PreDestroy当Bean生命周期结束的时候自动发送一个下线请求到Nacos服务端

服务端处理下线请求

有了之前的学习,我们现在找对应的接口应该问题不大,服务端对应的接口就是如下

 

@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";
      }
       
       serviceManager.removeInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
       return "ok";
  }

可以看到 主要的就是在 removeInstance 方法内部,其代码如下

updateIpAddresses()方法中,判断了如果是 Remove,会把对应的 Instance 移除掉,但是此时并不是直接操作内存注册表,只是返回的结果中,已经把对应的 Instance 删除了,然后再和注册逻辑一样,利用异步任务、内存队列的方式,去修改注册表。

public void removeInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
           throws NacosException {
   // 从实例表serviceMap根据namespace和serviceName获取到对应的服务实例
       Service service = getService(namespaceId, serviceName);
       
   // 加锁操作 线程安全的移除实例
       synchronized (service) {
           removeInstance(namespaceId, serviceName, ephemeral, service, ips);
      }
  }



private void removeInstance(String namespaceId, String serviceName, boolean ephemeral, Service service,
           Instance... ips) throws NacosException {
       // 和注册时候的逻辑一致 创建一个唯一的key 可以区分临时or非临时 且能通过namespace和serviceName确定唯一一个实例
       String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
        // 在这个 instanceList 中,就不会包含需要删除的 instance 实例了
       List<Instance> instanceList = substractIpAddresses(service, ephemeral, ips);
       // 将上面的instanceList包装成 Instances
       Instances instances = new Instances();
       instances.setInstanceList(instanceList);
       // 调用和注册一样的逻辑,把 instanceList 中的 Instance,用写时复制的机制,修改内存注册表
       consistencyService.put(key, instances);
  }


private List<Instance> substractIpAddresses(Service service, boolean ephemeral, Instance... ips)
           throws NacosException {
   // 这个方法在注册的逻辑中看过了 其作用就是获取当前最新的Instance实例列表 也就是不包含需要删除的那个Instance的实例列表
       return updateIpAddresses(service, UtilsAndCommons.UPDATE_INSTANCE_ACTION_REMOVE, ephemeral, ips);
  }

大家有没有想过,我们服务注册、服务下线,客户端是怎么去感知的?

通过“服务发现”那篇文章可得知,客户端其实是有定时任务去更新客户端本地注册表的,但是这样也还是会有几秒钟的延迟,那有没有可能,在服务端注册表发生了变动,可以主动通知客户端去感知呢?

其实是有的,不管是服务注册、还是服务下线,它俩最后走的代码逻辑都是一样的,在利用写时复制修改完成注册表之后,服务端会发布一个变动事件,然后通过 udp的方式,去通知每一个客户端,从而让客户端感知速度更快

变动事件

在服务上线和服务下线的最后,我们都是走到写时复制的代码中,我们再来看看写时复制逻辑中我们之前没分析的变动事件的逻辑

在Service类中的updateIPs方法中,有如下这么段逻辑

image-20251026214800583

我们点进去看看 这段逻辑就是调用的PushService中的serviceChanged方法,可以看到这里面最后就是发布了一个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的查询功能,我们看看有没有类监听这个事件的,可以看到在PushService中onApplicationEvent方法就是监听ServiceChangeEvent事件,该方法的大致逻辑如下,方法逻辑比较多,这里省去和我们当前变动事件无关的部分

@Override
public void onApplicationEvent(ServiceChangeEvent event) {

   Future future = GlobalExecutor.scheduleUdpSender(() -> {
       try {
           // 遍历需要通知的 客户端
           for (PushClient client : clients.values()) {
               // 从名字其实就能知道是udp推送
               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);
}

第一阶段小结

编程 Java 项目