«

Nacos源码学习计划-Day02-客户端自动注册和客户端心跳检测原理

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


 

如何找到源码阅读入口

对于一般的业务项目,其实我们可能是根据需要修改的业务看对应的接口功能,当自己学习的时候,可能是根据前端的页面进行一点点的一个接口一个接口的看,这个其实对于这种中间件/框架的源码而言,其实不是很实用。

类似于Nacos源码,我们首先采用的方式进行,最好是结合着当前服务的功能来猜。Nacos我们知道,他的作用是配置中心和注册中心,我们自己的一个项目,引入Nacos的依赖之后,在启动的时候就会自动的注册到Nacos上,是不是就可以猜测:Naocs肯定是监听了一个注册连接,让后对于我们服务的注册需求进行处理然后缓存,那么对于这个注册连接功能,肯定是依赖中会被自动注入,才能说我们的服务启动的时候,也顺带一起进行注册中心的注册

我们拿来一个需要Nacos依赖服务,通过Maven来查看一下nacos-discovery依赖里面的内容

(因为Spring在新版本中移除了META/INF/spring.factories,转而使用autoconfiguration.imports,所以这里新版本和旧版本Nacos中查看自动装配类不一样)

在高版本中,找autoconfiguration.imports文件,如下

image-20250906005034285

在低版本中找META/INF/spring.factories,如下

image-20250906005238893

可以看到,这里所写的全类名对应的类,都是Nacos一开始就需要加载的,所以这里就可以成为我们的阅读源码的入口,根据名字判断,这里有NacosServiceRegistryAutoConfiguration和注册相关的 ;NacosDiscoveryAutoConfiguration和服务发现相关的,对于我们有很好的指引

启动Nacos服务

自然,能本地通过跑代码的方式启动Nacos,对于我们学习Nacos源码是必不可少的,所以我们要能本地跑起Nacos服务。

启动服务自然就需要找到服务的启动类,一眼望去,Naocs的模块那么多,怎么找启动类?

image-20250906010209087

我们可以利用nacos-server.jar包,把 Jar 进行解压后,在META-INF目录下有MANIFEST.MF文件,打开后就可以看到Start-Class:com.alibaba.nacos.Nacos,然后再 IDEA 搜索一下这个类,就找到啦!这个也是属于看源码技巧之一!

然后我们要单点模式启动Nacos,所以还需要加一个JVM参数

-Dnacos.standalone=true

image-20250906010327786

启动之后,能成功跑起来,能访问Nacos默认的管理页面就基本没问题,最好找一个SpringBoot服务注册上去试一下

image-20250906014949408

成功跑起来后,就可以开始我们的源码学习了

Nacos客户端自动注册的原理

我们首先来了解一下客户端自动注册的原理。从哪里开始看?前面有说到的imports文件中,可以看到有个自动配置类为NacosServiceRegistryAutoConfiguration,从名字上分析知道,这个和我们的自动注册肯定是有关系的,那么我去找找这个部分的源码

我们CTRL + 数标左键 跳转到NacosServiceRegistryAutoConfiguration这个配置类的代码中,可以看到,在这个配置类中注册了三个Bean,分别是NacosServiceRegister ; NacosRegistration ; NacosAutoServiceRegistration,我们分别来分析这个几个类

image-20250906220944195

客户端为何会触发自动注册到Nacos

我们先来看NacosServiceRegistry

// 1.4.1的Bean注册的时候代码如下   
@Bean
  public NacosServiceRegistry NacosServiceRegistry(
         NacosDiscoveryProperties  NacosDiscoveryProperties) {
     return new  Nacos  ServiceRegistry( Nacos DiscoveryProperties);
  }

可以看到,注册的该Bean需要借助到 NacosDiscoveryProperties 这个Bean作为参数,然后我们来看看这个Bean定义,可以看到,其成员就是我们引入Nacos作为服务中心的时候yml配置文件中所需要的参数

image-20250906221445969

在这个类对象中,可以看到一个成员方法register,register的方法逻辑我们暂且不细看,大致看一眼发现,需要一个Registration对象为入参,主要逻辑是获取一些参数例如nameService , serviceId ,group等等,最后执行了一个namingService.registerInstance(serviceId, group, instance)方法,从这个方法名可以看出来,实际上的注册真正逻辑应该就是这个方法而来

到这里其实看代码的时候,会存在很多问题,入参Registration对象是什么?怎么来的?其余的所需要的参数信息怎么来的?registerInstance方法的底层逻辑是什么?

可以看到这里有很多的问题,但是我们还是和之前说过的,看源码需要有目的性,我们的当前的主线任务是弄懂客户端是如何自动注册的,既然知道了registerInstance方法是注册的方法,那么就需要知道在哪里调用了register方法,因为只有调用register方法的地方才会触发registerInstance方法。

对于上面的非主线任务,我们可以先放一放,如果执着于非主线任务,就会越看越深然后混乱,跳不出来了

image-20250906221920886

我们此时回到刚刚的配置类中,可以看到第二个Bean,可以看到NacosRegistration这个Bean对象其实和上述register方法所需要的入参对象是同一个类型的。因为这里和主线无关,所以我们可以做个标记留个印象在这里,暂时先不细看

然后来看看第三个Bean对象,NacosAutoServiceRegistration从命名来看,就是和Auto自动注册相关的,而且其入参就是前面两个Bean,也就是说前面两个Bean参与了第三个Bean的构建,那么在NacosAutoServiceRegistration中就很有可能拥有前面两个Bean对象的相关属性的获取或者方法的调用

image-20250906232449474

然后我们去看NacosAutoServiceRegistration的源码,可以看到其底层也有个register方法,在这个register方法中,会调用入参中的NacosServiceRegistry的register方法,也就是我们上面说到的那个register方法

public NacosAutoServiceRegistration(ServiceRegistry<Registration> serviceRegistry,
     AutoServiceRegistrationProperties autoServiceRegistrationProperties,
     NacosRegistration registration) {
  // 调用了父类构造方法
  super(serviceRegistry, autoServiceRegistrationProperties);
  this.registration = registration;
}

// 父类构造方法
protected  AbstractAutoServiceRegistration (ServiceRegistry<R> serviceRegistry, AutoServiceRegistrationProperties properties) {
  // 在这里把传入的第一个 Bean 对象,复制给了 this.serviceRegistry 属性  
  this .serviceRegistry = serviceRegistry;  
  this .properties = properties;
}

image-20250906232418880

image-20250906232628243

那么现在主线就是找NacosAutoServiceRegistration的register方法是在哪里调用的了

利用Idea的跳转功能发现,该register是对父类的register方法的重写,然后CTRL + ALT +F7可以发现,其在AbstractAutoServiceRegistration类的start方法中被调用

image-20250906232951676

该start方法在AbstractAutoServiceRegistration的onApplicationEvent方法中被调用(上述很多跳转的时候,都是存在两个及其以上的地方被调用,根据溯源判断,一些restart重启方法,还是同类方法调用肯定不是我们需要的)

而onApplicationEvent这个方法,是实现的Spring中的ApplicationListener接口的方法,也就是说溯源到最后就是利用Spring的监听机制

onApplicationEvent的入参标识监听的事件类型,这里是WebServerInitializedEvent代表服务已经初始化事件,Spring 容器启动的最后会执行 finishRefresh 方法,然后会发布一个事件,Nacos 客户端这里的监听器就会起作用,监听到这个事件最终调用start方法从而实现对于register方法的调用

image-20250906233820706

那么到目前位置,我们知道了客户端自动注册的基本核心链路了,换句话而言就是自动执行register方法的链路

整个流程简单来说:

image-20250906235451155

register方法的逻辑(客户端如何进行的注册)

现在就是看register方法的实际逻辑了

@Override
public void register(Registration registration) {

if (StringUtils.isEmpty(registration.getServiceId())) {
log.warn("No service to register for nacos client...");
return;
}
       // 如下开始获取几个有关的参数
       
       // 第一个NamingService 对象 内部包含了注册实例的真正方法 registerInstance namingService()方法内部的具体逻辑一下字看不明白,但是可以看到内部其实用了单例模式,还是使用了volatile的双重锁模式
NamingService namingService = namingService();
       // 这个就是利用入参获取 服务ID/服务名称
String serviceId = registration.getServiceId();
       // 获取服务分组 也就是Nacos管理平台上能看到的那个分组信息
String group = nacosDiscoveryProperties.getGroup();
       
       // 获取Instance一个实例对象 该对象中封装了IP,PORT,Weigth权重,enable是否可用,healthy健康状态等等属性
Instance instance = getNacosInstanceFromRegistration(registration);

try {
           /*
           之前有说到,这个是最核心的方法,真正的注册实例的操作
           该方法来自于NamingService接口,该接口也只有一个实现类NacosNamingService
           */
namingService.registerInstance(serviceId, group, instance);
           // 下面的都可以不要看了暂时
log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
instance.getIp(), instance.getPort());
}
catch (Exception e) {
if (nacosDiscoveryProperties.isFailFast()) {
log.error("nacos registry, {} register failed...{},", serviceId,
registration.toString(), e);
rethrowRuntimeException(e);
}
else {
log.warn("Failfast is false. {} register failed...{},", serviceId,
registration.toString(), e);
}
}
}

我们跳入到registerInstance方法中,发现内容如下

/*
在高版本Nacos中,进行了逻辑整合,可能你看到的registerInstance方法源码
就下面两行代码逻辑 第一行逻辑和我们下面的第一行逻辑一样 其实就是一个判断心跳合理性的一个方法,暂时不需要过多理会
第二行逻辑就是注册实例,你会发现会有三个实现类的跳转,和我们下面逻辑类似的,该方法在NamingHttpClientProxy中的实现,将下面的除了第一行之外的逻辑都整合到了clientProxy.registerService·

NamingUtils.checkInstanceIsLegal(instance);
clientProxy.registerService(serviceName, groupName, instance);
*/
@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws  Nacos Exception {
   // 第一行代码也是检查什么,先不管,抓主线
   NamingUtils.checkInstanceIsLegal(instance);
   
   // 获取了 分组服务名字,具体做什么也暂时不知道
   String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
   
   // 判断了一个属性
   if (instance.isEphemeral()) {
       // 如果 isEphemeral 为true, buildBeatInfo 翻译过来就是:构建心跳信息
       BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
       // 添加心跳信息
       beatReactor.addBeatInfo(groupedServiceName, beatInfo);
  }
   
   // 上面代码都是分支代码,有个印象就行,现在没必要都点进去看
   // 核心在这里,又调用了 serverProxy 的注册方法
   serverProxy.registerService(groupedServiceName, groupName, instance);
}

然后看registerService的逻辑(在高版本中对应的在NamingHttpClientProxy中),其逻辑如下

public void registerService(String serviceName, String groupName, Instance instance) throws  Nacos Exception {
   
   NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
           instance);
   
   // 创建个Map 准备组装参数
   final Map<String, String> params = new HashMap<String, String>(16);
   params.put(CommonParams.NAMESPACE_ID, namespaceId);
   params.put(CommonParams.SERVICE_NAME, serviceName);
   params.put(CommonParams.GROUP_NAME, groupName);
   params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
   
   // ip、port就是刚刚那个instance里面的属性
   params.put("ip", instance.getIp());
   params.put("port", String.valueOf(instance.getPort()));
   params.put("weight", String.valueOf(instance.getWeight()));
   params.put("enable", String.valueOf(instance.isEnabled()));
   params.put("healthy", String.valueOf(instance.isHealthy()));
   params.put("ephemeral", String.valueOf(instance.isEphemeral()));
   params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
   
   
   // 请求地址、参数、Post,这很明显就是HTTP呀
   // UtilAndComs.NacosUrlInstance 这行代码解释一下
   // 这里是UtilAndComs常量类 NacosUrlInstance是/Nacos/v1/ns/instance
 
   reqApi(UtilAndComs.NacosUrlInstance, params, HttpMethod.POST);
   
}


// 高版本中的代码逻辑如下 整体上其实是类似的 就是map中某些参数的格式和内容不太一样 然后就是随着功能的拓展新增了一些key-value键值对
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
       NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
               instance);
       String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);// 封装为groupName@@serviceName
       if (instance.isEphemeral()) {
           throw new UnsupportedOperationException(
                   "Do not support register ephemeral instances by HTTP, please use gRPC replaced.");
      }
       final Map<String, String> params = new HashMap<>(32);
       params.put(CommonParams.NAMESPACE_ID, namespaceId);
       params.put(CommonParams.SERVICE_NAME, groupedServiceName);
       params.put(CommonParams.GROUP_NAME, groupName);
       params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
       params.put(IP_PARAM, instance.getIp());
       params.put(PORT_PARAM, String.valueOf(instance.getPort()));
       params.put(WEIGHT_PARAM, String.valueOf(instance.getWeight()));
       params.put(REGISTER_ENABLE_PARAM, String.valueOf(instance.isEnabled()));
       params.put(HEALTHY_PARAM, String.valueOf(instance.isHealthy()));
       params.put(EPHEMERAL_PARAM, String.valueOf(instance.isEphemeral()));
       params.put(META_PARAM, JacksonUtils.toJson(instance.getMetadata()));
       reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
  }

通过上面的分析,我们其实就已经可以了解到,register方法的底层其实就是封装了HTTP请求参数,然后再最后的reqApi()方法中发起HTTP请求

如果你有看过Nacos的文档,并且尝试手动注册服务实例到Nacos,那么这部分内容应该比较好理解,可以看到,这个部分其实也就是主动注册实例逻辑的实现,这个也是属于Nacos结构中OpenAPI部分对外提供的功能接口之一

image-20250907153124168

reqApi方法链路分析(支线任务)

reqApi中,利用传入的参数UtilAndComs.nacosUrlInstance作为方法URL,params作为请求体内容,HttpMethod.POST指定请求形式

reqApi的底层是调用的callServer方法,这个方法内会调用内部的nacosRestTemplate通过exchangeForm方法中的execute方法执行HTTP请求

image-20250907182110980

可以看到executor方法有三个实现类,DefaultXXX是利用的Apache实现的HTTP请求相关功能 ; JdkXXX自然就是利用的JDK实现的 ; InterXXX是一个拓展功能的HTTPClientRequest

image-20250907160526857

Nacos客户端如何发送服务心跳

在之前的逻辑中我们可以看到(1.4.1版本),在registerService中有这么一段逻辑

 @Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws  Nacos Exception {
   NamingUtils.checkInstanceIsLegal(instance);
   String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
   // ephemeral 这个属性默认就是为true
   if (instance.isEphemeral()) {
       // 构建BeatInfo对象
       BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
       // 添加BeatInfo对象
       beatReactor.addBeatInfo(groupedServiceName, beatInfo);
  }
   serverProxy.registerService(groupedServiceName, groupName, instance);
}

instance对象的isEphemeral()方法逻辑,就是返回成员属性ephemeral,该成员是个布尔类型的数据,默认为True(实际上这个属性是代表临时节点还是非临时节点),他的具体含义我们暂且先不讨论,我们看看if代码中的内容,首先是第一行代码即buildBeatInfo方法,其作用是构建BeatInfo对象,从命名可以看出来,和心跳有关

public BeatInfo buildBeatInfo(String groupedServiceName, Instance instance) {
   // 从set方法中也能猜测到该对象包含了IP Port等信息
   BeatInfo beatInfo = new BeatInfo() ;
   beatInfo.setServiceName(groupedServiceName) ;
   beatInfo.setIp(instance.getIp()) ;
   beatInfo.setPort(instance.getPort()) ;
   beatInfo.setCluster(instance.getClusterName()) ;
   beatInfo.setWeight(instance.getWeight()) ;
   beatInfo.setMetadata(instance.getMetadata()) ;
   beatInfo.setScheduled(false) ;
   // 注意点,下面会仔细讲
   beatInfo.setPeriod(instance.getInstanceHeartBeatInterval()) ;
   return beatInfo ;
}

然后我们看最后那个setPeriod的逻辑,可以看到方法getInstanceHeartBeatInterval返回一个常量,默认值为5000(一个时间参数 5000ms),将其赋值给了BeatInfo的period成员

public static final long DEFAULT_HEART_BEAT_INTERVAL = TimeUnit.SECONDS.toMillis(5);


// 这里 getInstanceHeartBeatInterval() 的返回值是5000
beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());

// 读取常量
public long getInstanceHeartBeatInterval() {
   return getMetaDataByKeyWithDefault(PreservedMetadataKeys.HEART_BEAT_INTERVAL,Constants.DEFAULT_HEART_BEAT_INTERVAL);
}

然后是if代码块中的第二行代码addBeatInfo方法

 // 添加BeatInfo对象
beatReactor.addBeatInfo(groupedServiceName, beatInfo);

// addBeatInfo 方法具体实现
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
   NAMING_LOGGER.info(" [BEAT] adding beat: {} to beat map.", beatInfo) ;
   String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort()) ;
   BeatInfo existBeat = null ;
   //fix #1733
   if ((existBeat = dom2Beat.remove(key)) != null) {
       existBeat.setStopped(true) ;
  }
   dom2Beat.put(key, beatInfo) ;
   // 以上代码都是分支代码,第一遍不需要详细去看
   
   // 主线代码
   executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS) ;
   MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size()) ;
}

我们直接抓紧主线任务,从倒数第二行开始看,发现new了一个BeatTask对象

executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS) ;

// BeatTask类的定义 可以看到实现了Runnable接口 也就是说这个是个任务对象
class BeatTask implements Runnable {
   
   BeatInfo beatInfo ;
   // 可以利用BeatInfo进行构建
   public BeatTask(BeatInfo beatInfo) {
       this.beatInfo = beatInfo ;
  }
   
   @Override
   public void run() {
       // 判断是否需要停止
       if (beatInfo.isStopped()) {
           return ;
      }
       
       // 获取下一次执行的时间,也就是我们上面设置那个常量5000ms,同样还是5s
       long nextTime = beatInfo.getPeriod() ;
       try {
           
           // serverProxy.sendBeat 发送心跳,这里就很关键,这个方法内部其实也是利用了requApi()方法发送了一个HTTP请求
           JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled) ;

           // 获取服务端返回的code状态码
           int code = NamingResponseCode.OK ;
           if (result.has(CommonParams.CODE)) {
               code = result.get(CommonParams.CODE).asInt() ;
          }
           
           // 如果code = RESOURCE_NOT_FOUND 没有找到
           // 很可能是之前注册的信息,已经被 Nacos 服务端移除了,所以返回这个错误信息
           if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
               // 然后重新组装参数,并且重新发起注册请求
               Instance instance = new Instance() ;
               instance.setPort(beatInfo.getPort()) ;
               instance.setIp(beatInfo.getIp()) ;
               instance.setWeight(beatInfo.getWeight()) ;
               instance.setMetadata(beatInfo.getMetadata()) ;
               instance.setClusterName(beatInfo.getCluster()) ;
               instance.setServiceName(beatInfo.getServiceName()) ;
               instance.setInstanceId(instance.getInstanceId()) ;
               instance.setEphemeral(true) ;
               try {
                   // 重新注册
                   serverProxy.registerService(beatInfo.getServiceName(),
                           NamingUtils.getGroupName(beatInfo.getServiceName()), instance) ;
              }
          }
      }
       
       // 又重新放入延迟任务当中,并且还是5秒,所以一直是个循环的状态
       executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS) ;
  }
}

可以看到上述方法中执行了sendBeat这个很关键的方法,其源码内容如下

public JsonNode sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws  Nacos  Exception {
   
   if (NAMING_LOGGER.isDebugEnabled()) {
       NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", namespaceId, beatInfo.toString());
  }
   // 组装参数 Map<String, String> params = new HashMap<String, String>(8);
   Map<String, String> bodyMap = new HashMap<String, String>(2);
   if (!lightBeatEnabled) {
       bodyMap.put("beat", JacksonUtils.toJson(beatInfo));
  }
   params.put(CommonParams.NAMESPACE_ID, namespaceId);
   params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName());
   params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster());
   params.put("ip", beatInfo.getIp());
   params.put("port", String.valueOf(beatInfo.getPort()));
   
   // 发起请求,参数描述如下图 String result = reqApi(UtilAndComs. Nacos UrlBase + "/instance/beat", params, bodyMap, HttpMethod.PUT);
   return JacksonUtils.toObj(result);
}

可以看到,这个过程其实也是个发送HTTP请求的过程,和官方API中提到的发送实例心跳是一样的,那么结合上述的客户端自动注册原理和上述逻辑的分析,我们知道,客户端心跳响应是BeatTask这个线程类实现的,客户端在发起服务注册的期间/时候,会通过buildBeatInfo创建心跳实例BeatInstance,然后通过addBeatInfo方法添加第一个心跳任务放到任务队列中,心跳任务的实质就是往Nacos服务发送一个心跳检测的HTTP请求,而每个心跳任务在结束之前都会将下一个心跳任务放到队列中,从而实现类似于循环的定时任务的效果,告诉Nacos我还活着

image-20250907213133103

结合上述的自动注册,整个流程就是如下

image-20250907214944135

/*
这里可以提前剧透一下,你在看1.14版本和高版本的registerService方法中,在if代码块中,两者的逻辑是不同的
*/

// 1.4.1版本中
if (instance.isEphemeral()) {
   // 构建BeatInfo对象
   BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
   // 添加BeatInfo对象
   beatReactor.addBeatInfo(groupedServiceName, beatInfo);
}

// 高版本中
if (instance.isEphemeral()) {
throw new UnsupportedOperationException("Do not support register ephemeral instances by HTTP, please use gRPC replaced.");
}

/*
你会发现上述两个if块中的代码确实是差距有点大,其实从Ephemeral的意思以及高版本中的抛出的异常信息来看,就可以知道原因
Nacos 2.0 及以上版本,不再支持通过 HTTP 协议来注册临时实例(ephemeral instance)了。

那么功能是如何实现的呢?
1. 架构演进:从 HTTP 到 gRPC
Nacos 2.0 引入了全新的双向通信架构,核心是基于 gRPC 的长连接。这个连接一旦建立,就实现了客户端和服务器之间的双向流(Bi-directional streaming)。

2. 功能替代:心跳被“连接状态”取代
在新的架构下:
注册请求:虽然你调用的 registerInstance 方法看起来没变,但在底层,客户端会选择使用 gRPC 协议(而不是 HTTP)将注册信息发送给服务器。
健康检查:不再需要客户端主动发送心跳。那个建立的 gRPC 长连接本身就是一个健康状态的象征。只要这个连接是健康的,服务器就认为客户端是健康的。如果连接断开(比如客户端宕机、网络故障),服务器会立刻感知到,并迅速(秒级)将对应的服务实例标记为不健康或删除。

这就是那部分“缺失”的心跳代码的真正去向——它被 gRPC 长连接的连接状态管理机制所取代。 这种方式的优势非常明显:
实时性更高:服务器能立刻感知客户端下线,而不需要等待 15 或 30 秒的心跳超时。
压力更小:减少了大量重复的 HTTP 心跳请求,降低了服务器和网络的负载。
功能更强大:为服务端主动推送(如配置变更、服务列表变化)提供了通道。

3. 为何还保留 HTTP 注册的代码?
会注意到高版本代码中仍然有 serverProxy.registerService 的逻辑。这是为了兼容持久化实例(persistent instance)。
临时实例(Ephemeral):默认类型,生命周期与客户端绑定,客户端下线则实例自动注销。必须使用 gRPC 协议。
持久化实例(Persistent):生命周期与客户端解耦,即使客户端下线,实例也会保留在服务器上,其健康检查由 Server 端主动发起(类似 Nacos 对 MySQL 等数据源的健康检查)。这种实例仍然可以使用 HTTP 协议注册。
*/

编程 Java 项目