图解+源码讲解 Nacos 服务端处理心跳请求

1,010 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情

图解+源码讲解 Nacos 服务端处理心跳请求

人的才华就如海绵的水,没有外力的挤压,它是绝对流不出来的。流出来后,海绵才能吸收新的源泉 Nacos 源码分析系列相关文章

  1. 从零开始看 Nacos 源码环境搭建
  2. 图解+源码讲解 Nacos 客户端发起注册流程
  3. 图解+源码讲解 Nacos 服务端处理注册请求逻辑
  4. 图解+源码讲解 Nacos 客户端下线流程
  5. 图解+源码讲解 Nacos 服务端处理下线请求
  6. 图解+源码讲解 Nacos 客户端发起心跳请求
  7. 图解+源码讲解 Nacos 服务端处理心跳请求
  8. 图解+源码讲解 Nacos 服务端处理配置获取请求

从哪里开始说起

    在Nacos 客户端进行请求的地址可以找到服务端处理的地方,客户端的请求地址是什么呢?在Nacos 客户端发起心跳请求的文章中讲过,地址是:http://localhost:8848/nacos/v1/ns/instance/beat

Controller 处理入口

    在 nacos-naming这个项目的 controller 中的 InstanceController里面的 beat方法
image.png

@CanDistro
@PutMapping("/beat")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public ObjectNode beat(HttpServletRequest request) throws Exception {

    ObjectNode result = JacksonUtils.createEmptyJsonNode();
    // 5s 发送一次心跳 clientBeatInterval  TimeUnit.SECONDS.toMillis(5);
    result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval());
    // 解析的心跳信息
    String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);
    RsInfo clientBeat = null;
    if (StringUtils.isNotBlank(beat)) {
        // {"load":0.0,"cpu":0.0,"rt":0.0,"qps":0.0,"mem":0.0,"port":1020,
        //"ip":"192.168.60.1","serviceName":"DEFAULT_GROUP@@service-consumer",
        //"cluster":"DEFAULT","weight":1.0,"ephemeral":true,
        // "metadata":{"preserved.register.source":"SPRING_CLOUD"}}
        clientBeat = JacksonUtils.toObj(beat, RsInfo.class);
    }
    // 集群名称 default
    String clusterName = WebUtils.optional(request, CommonParams.CLUSTER_NAME,
                                           UtilsAndCommons.DEFAULT_CLUSTER_NAME);
    // ip地址192.168.60.1
    String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);
    // 端口号 1020
    int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));
    if (clientBeat != null) {
        if (StringUtils.isNotBlank(clientBeat.getCluster())) {
            clusterName = clientBeat.getCluster();
        } else {
            // fix #2533
            clientBeat.setCluster(clusterName);
        }
        ip = clientBeat.getIp();
        port = clientBeat.getPort();
    }
    // 命名空间 public
    String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID,
                                           Constants.DEFAULT_NAMESPACE_ID);
    // 服务名称 DEFAULT_GROUP@@service-consumer
    String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    NamingUtils.checkServiceNameFormat(serviceName);
    Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}",
                          clientBeat, serviceName);
    System.out.println("心跳信息: " + ip + "_" + port + "+ =====start=====:"
     + "命名空间:" + namespaceId + "服务名称:" + serviceName + "IP端口:" + ip + port +
    "集群名称: " + clusterName + "心跳信息: " + clientBeat + "=====end=====");
    // 处理心跳信息逻辑并返回结果
    int resultCode = getInstanceOperator().handleBeat(namespaceId, serviceName, ip, 
                                 port, clusterName, clientBeat);
    result.put(CommonParams.CODE, resultCode);
    result.put(SwitchEntry.CLIENT_BEAT_INTERVAL,
         // 获取指定的实例心跳间隔信息
        getInstanceOperator().getHeartBeatInterval(namespaceId, serviceName, 
                    ip, port, clusterName));
    result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());
    return result;
}

处理心跳请求

     走的是 InstanceOperatorClientImplhandleBeat方法

@Override
public int handleBeat(String namespaceId, String serviceName, String ip, int port,
                      String cluster,RsInfo clientBeat) throws NacosException {
    // 获取服务 Service{namespace='public', group='DEFAULT_GROUP', 
    // name='service-consumer', ephemeral=true, revision=0}
    Service service = getService(namespaceId, serviceName, true);
    // 获取客户端ID信息 192.168.60.1:1020#true
    String clientId = IpPortBasedClient.getClientId(ip + IPUtil.IP_PORT_SPLITER + port, 
                                                    true);
    // 根据客户端ID信息从 clientManager 中获取注册的实例
    IpPortBasedClient client = (IpPortBasedClient) clientManager.getClient(clientId);
    // 如果 client 为空或者发布者 publishers 信息集合里面没有该客户端实例那么就从新注册一个
    if (null == client || !client.getAllPublishedService().contains(service)) {
        // 如果心跳实体信息为空返回 20404,请求未找到
        if (null == clientBeat) {
            return NamingResponseCode.RESOURCE_NOT_FOUND;
        }
        // 构建实例信息
        Instance instance = new Instance();
        // 设置端口信息
        instance.setPort(clientBeat.getPort());
       // 设置IP信息
        instance.setIp(clientBeat.getIp());
        //设置权重
        instance.setWeight(clientBeat.getWeight());
        // 设置元数据
        instance.setMetadata(clientBeat.getMetadata());
        // 设置集群名称
        instance.setClusterName(clientBeat.getCluster());
        // 设置服务名称
        instance.setServiceName(serviceName);
        // 设置实例ID信息
        instance.setInstanceId(instance.getInstanceId());
        // 设置虚拟节点
        instance.setEphemeral(clientBeat.isEphemeral());
        // 发起注册
        registerInstance(namespaceId, serviceName, instance);
        // 注册完后再一次获取 client 从 clientManager 中,
        // 因为注册的时候就是把这个客户端放入到 clientManager 中去的
        client = (IpPortBasedClient) clientManager.getClient(clientId);
    }
    // 注册时候已经将当前的 service 放入到了 ServiceManager.getInstance()的
    // ConcurrentHashMap singletonRepository 里面
    // 所以在这里面获取了一次所以会有的
    if (!ServiceManager.getInstance().containSingleton(service)) {
        throw new NacosException(NacosException.SERVER_ERROR,
                    "service not found: " + serviceName + "@" + namespaceId);
    }
    // 如果心跳信息为空那么就构建一个心跳信息
    if (null == clientBeat) {
        clientBeat = new RsInfo();
        clientBeat.setIp(ip);
        clientBeat.setPort(port);
        clientBeat.setCluster(cluster);
        clientBeat.setServiceName(serviceName);
    }
    // 创建心跳处理器任务
    ClientBeatProcessorV2 beatProcessor = new ClientBeatProcessorV2(namespaceId,
                                                   clientBeat, client);
    HealthCheckReactor.scheduleNow(beatProcessor);
    client.setLastUpdatedTime();
    return NamingResponseCode.OK;
}

创建客户端心跳处理器并处理心跳

public class ClientBeatProcessorV2 implements BeatProcessor {
    private final String namespace;
    private final RsInfo rsInfo;
    private final IpPortBasedClient client;

    public ClientBeatProcessorV2(String namespace, RsInfo rsInfo,
                                 IpPortBasedClient ipPortBasedClient) {
        this.namespace = namespace;
        this.rsInfo = rsInfo;
        this.client = ipPortBasedClient;
    }

    // 处理心跳请求
    @Override
    public void run() {
        if (Loggers.EVT_LOG.isDebugEnabled()) {
            Loggers.EVT_LOG.debug("[CLIENT-BEAT] processing beat: {}", 
                                  rsInfo.toString());
        }
        // 获取ID
        String ip = rsInfo.getIp();
        // 端口号
        int port = rsInfo.getPort();
        // 服务名称
        String serviceName = NamingUtils.getServiceName(rsInfo.getServiceName());
        // 组名称
        String groupName = NamingUtils.getGroupName(rsInfo.getServiceName());
        // 获取服务
        Service service = Service.newService(namespace, groupName, 
                                             serviceName, rsInfo.isEphemeral());
        // 获取健康检测实例发布信息里面多了几个属性,
        // 比如 lastHeartBeatTime 和 healthCheckStatus
        HealthCheckInstancePublishInfo instance =
            (HealthCheckInstancePublishInfo) client.getInstancePublishInfo(service);
        System.out.println("健康检测 ====== start =====+" + ip + "_" + port + 
                           "+  HealthCheckInstancePublishInfo instance" +
                         instance.toString() + "date: " + new Date() + "====end====");
        if (instance.getIp().equals(ip) && instance.getPort() == port) {
            if (Loggers.EVT_LOG.isDebugEnabled()) {
                Loggers.EVT_LOG.debug("[CLIENT-BEAT] refresh beat: {}", 
                                      rsInfo.toString());
            }
            // 更新一下实例的心跳时间
            instance.setLastHeartBeatTime(System.currentTimeMillis());
            System.out.println("健康检测 ====== start =====+" + ip + "_" + port + 
                           "+  HealthCheckInstancePublishInfo instance" + 
                          instance.toString() + "date: " + new Date() + "====end====");
            // 如果实例是健康的那么就直接执行完任务,
            // 如果不是健康的那么就设置成健康的,之后发布服务改变事件和客户端改变事件
            if (!instance.isHealthy()) {
               instance.setHealthy(true);
               Loggers.EVT_LOG.info("service: {} {POS} {IP-ENABLED} valid: {}:{}@{},
                                     "region: {}, msg: client beat ok",
                          rsInfo.getServiceName(), ip, port, rsInfo.getCluster(),
                                     UtilsAndCommons.LOCALHOST_SITE);
               NotifyCenter.publishEvent(new ServiceEvent.ServiceChangedEvent(service));
               NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(client));
            }
        }
    }
}

    这里面其实就是 HealthCheckInstancePublishInfo 这个类,从 AbstractClient 的 发布者集合 ConcurrentHashMap<Service, InstancePublishInfo> publishers 中获取发布者实例之后更新一下这个实例的最新的心跳时间

心跳请求样例

image.png
image.png

小结

    其实就是根据客户端发过来的请求,去发布者集合中去获取当前实例信息之后更新一下心 跳时间

其他系列源码分析

feign 源码分析系列相关文章

  1. 图解+源码讲解 Feign 如何将客户端注入到容器中
  2. 图解+源码讲解动态代理获取 FeignClient 代理对象
  3. 图解+源码讲解代理对象 ReflectiveFeign 分析
  4. 图解+源码讲解 Feign 如何选取指定服务
  5. 图解+源码讲解 Feign 请求的流程
    ribbon 源码分析系列相关文章
  6. Ribbon 原理初探
  7. 图解+源码讲解 Ribbon 如何获取注册中心的实例
  8. 图解+源码讲解 Ribbon 服务列表更新
  9. 图解+源码讲解 Ribbon 服务选择原理
  10. 图解+源码讲解 Ribbon 如何发起网络请求 eureka 源码分析系列相关文章
  11. eureka-server 项目结构分析
  12. 图解+源码讲解 Eureka Server 启动流程分析
  13. 图解+源码讲解 Eureka Client 启动流程分析
  14. 图解+源码讲解 Eureka Server 注册表缓存逻辑
  15. 图解+源码讲解 Eureka Client 拉取注册表流程
  16. 图解+源码讲解 Eureka Client 服务注册流程
  17. 图解+源码讲解 Eureka Client 心跳机制流程
  18. 图解+源码讲解 Eureka Client 下线流程分析
  19. 图解+源码讲解 Eureka Server 服务剔除逻辑
  20. 图解+源码讲解 Eureka Server 集群注册表同步机制