图解+源码讲解 Nacos 客户端发起注册流程

1,667 阅读7分钟

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

图解+源码讲解 Nacos 客户端发起注册流程

生命不可能有两次,但许多人连一次也不善于度过 —— 吕凯特

Nacos 源码分析系列相关文章

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

通用的服务注册接口

    ServiceRegistry 这个接口是 SpringCloud 通用的注册接口,Eureka 和 Nacos 这两个注册中心都实现了这个接口

/**
    就是抽象出来一个服务接口提供给其他的服务注册中心,用于进行服务的上线、下线等操作    
*/
public interface ServiceRegistry<R extends Registration> {
	// 服务注册
	void register(R registration);
    // 服务下线
	void deregister(R registration);
	// 服务关闭
	void close();
	// 设置服务状态
	void setStatus(R registration, String status);
	// 获取服务状态
	<T> T getStatus(R registration);
}

image.png
    因为我的这个项目是一个复合项目就是里面又Eureka 的注册中心,也Nacos 的注册中心所以就会出现上面图中的样子,显示了两个注册中心实现了这个类,这里我们主要是来看 NacosServiceRegistry

Nacos 服务注册中心

1. 服务注册方法

@Override
public void register(Registration registration) {
    // 如果注册实例信息中的服务ID 是空的那么就不是有效的服务实例,不允许注册
    // service-provider
    if (StringUtils.isEmpty(registration.getServiceId())) {
        log.warn("No service to register for nacos client...");
        return;
    }
    // 获取名称空间
    NamingService namingService = namingService();
    // 获取注册项目的服务 ID :service-provider
    String serviceId = registration.getServiceId();
    // 获取名称空间的组信息 DEFAULT_GROUP
    String group = nacosDiscoveryProperties.getGroup();
    // 构建实例信息 
    Instance instance = getNacosInstanceFromRegistration(registration);

    try {
        // 进行服务实例注册
        namingService.registerInstance(serviceId, group, instance);
        log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
                instance.getIp(), instance.getPort());
    }
    catch (Exception e) {
        log.error("nacos registry, {} register failed...{},", serviceId,
                registration.toString(), e);
        rethrowRuntimeException(e);
    }
}

2. 注册项目 Registration 信息

image.png

2. 获取命名空间 namingService

private NamingService namingService() {
    // 根据提供的nacos属性值进行命名空间信息获取
    return nacosServiceManager
            .getNamingService(nacosDiscoveryProperties.getNacosProperties());
}

NacosProperties 属性值

    这些属性值里面包括了命名空间、用户名、密码、服务地址、单例模式
image.png

3. 构建注册实例信息

    构建实例信息

private Instance getNacosInstanceFromRegistration(Registration registration) {
    Instance instance = new Instance();
    // ip设置
    instance.setIp(registration.getHost());
    // 端口设置
    instance.setPort(registration.getPort());
    // 权重设置
    instance.setWeight(nacosDiscoveryProperties.getWeight());
    // 集群名称
    instance.setClusterName(nacosDiscoveryProperties.getClusterName());
    // 是否开启
    instance.setEnabled(nacosDiscoveryProperties.isInstanceEnabled());
    // 元数据
    instance.setMetadata(registration.getMetadata());
    instance.setEphemeral(nacosDiscoveryProperties.isEphemeral());
    // 返回实例
    return instance;
}

    构建好的实例信息
image.png

4. 发起注册

    NacosNamingService#registerInstance 方法进行实例注册,先构建一个心跳的的信息实例,后台创建一个心跳的发送任务,好维持和服务端的连接,这样别的消费者实例在获取服务的时候能获取的你的服务实例,其实就是告诉服务端我还活着,我能用

@Override
public void registerInstance(String serviceName, String groupName, Instance instance) 
        throws NacosException {
    // 做一些检测
    NamingUtils.checkInstanceIsLegal(instance);
    // 组服务名称 DEFAULT_GROUP@@service-provider
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    if (instance.isEphemeral()) { // isEphemeral 默认是 true
        // 构建心跳信息
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        // 将心跳信息放入到dom2Beat的Map缓存中
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    // 发起注册请求
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

4.1 发起前的准备

1. 构建心跳信息
public BeatInfo buildBeatInfo(String groupedServiceName, Instance instance) {
    // 创建心跳实体
    BeatInfo beatInfo = new BeatInfo();
    // 设置服务名称
    beatInfo.setServiceName(groupedServiceName);
    // 设置IP地址
    beatInfo.setIp(instance.getIp());
    // 设置端口号
    beatInfo.setPort(instance.getPort());
    // 集群的名字是默认的其实就是单例模式母亲啊
    beatInfo.setCluster(instance.getClusterName());
    beatInfo.setWeight(instance.getWeight());
    beatInfo.setMetadata(instance.getMetadata());
    beatInfo.setScheduled(false);
    // 5s 发送一次心跳请求
    beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
    return beatInfo;
}

    新创建的心跳信息
image.png

2. 存储心跳信息到Map缓存中

    通过服务名称、IP地址、端口号创建 key

public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
    // DEFAULT_GROUP@@service-provider#192.168.60.1#1010
    String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
    BeatInfo existBeat = null;
    // 先移除心跳的 key
    if ((existBeat = dom2Beat.remove(key)) != null) {
        existBeat.setStopped(true);
    }
    // 心跳信息存储到 Map 中
    dom2Beat.put(key, beatInfo);
    // 创建一个心跳任务,定时发送心跳操作
    executorService.schedule(new BeatTask(beatInfo), 
                             beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
    // 注册监控器
    MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}

    dom2Beat 心跳信息缓存 Map 值样式
image.png

3. 创建定时发送心跳任务
class BeatTask implements Runnable {
    BeatInfo beatInfo;
    // 创建心跳
    public BeatTask(BeatInfo beatInfo) {
        this.beatInfo = beatInfo;
    }

    @Override
    public void run() {
        // 如果实例已经停了那么就不用发送心跳了
        if (beatInfo.isStopped()) {
            return;
        }
        long nextTime = beatInfo.getPeriod();
        try {
            // 发送心跳
            JsonNode result = serverProxy.sendBeat(beatInfo, 
                                     BeatReactor.this.lightBeatEnabled);
            long interval = result.get("clientBeatInterval").asLong();
            boolean lightBeatEnabled = false;
            if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) {
                lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED)
                    .asBoolean();
            }
            BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
            if (interval > 0) {
                nextTime = interval;
            }
            int code = NamingResponseCode.OK;
            if (result.has(CommonParams.CODE)) {
                code = result.get(CommonParams.CODE).asInt();
            }
            // 如果心跳实例没有找到的话那么就进行重新注册
            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);
                } catch (Exception ignore) {
                }
            }
        } catch (NacosException ex) {
            NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {},"
                                + "code: {}, msg: {}",
                    JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg());

        }
        // 在一次进行心跳任务执行
        executorService.schedule(new BeatTask(beatInfo), nextTime, 
                                 TimeUnit.MILLISECONDS);
    }
}

4.2 真正发起注册

public void registerService(String serviceName, String groupName, Instance instance) 
    throws NacosException {

    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance:{}", 
                       namespaceId, serviceName,instance);
    // 构建请求参数
    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());
    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()));
    // 发送请求 url地址是:/nacos/v1/ns/instance
    reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);

}

    构建好的参数值
image.png

public String reqApi(String api, Map<String, String> params, Map<String, String> body, 
                     List<String> servers, String method) throws NacosException {

    params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
    // 如果服务列表和 nacosDomain 都为空的话那么就是没有可利用的服务就抛出异常
    if (CollectionUtils.isEmpty(servers) && StringUtils.isBlank(nacosDomain)) {
        throw new NacosException(NacosException.INVALID_PARAM, "no server available");
    }	
    // 创建异常
    NacosException exception = new NacosException();
    // nacosDomain 是 localhost:8848
    if (StringUtils.isNotBlank(nacosDomain)) {
        // 最大重试测试书3次 maxRetry = 3
        for (int i = 0; i < maxRetry; i++) {
            try {
                // 请求服务
                return callServer(api, params, body, nacosDomain, method);
            } catch (NacosException e) {
                exception = e;
                if (NAMING_LOGGER.isDebugEnabled()) {
                    NAMING_LOGGER.debug("request {} failed.", nacosDomain, e);
                }
            }
        }
    } 
}
}
callServer 发起注册
public String callServer(String api, Map<String, String> params, 
                         Map<String, String> body, String curServer,
        String method) throws NacosException {
    // 开始时间
    long start = System.currentTimeMillis();
    long end = 0;
    // 参数检测
    injectSecurityInfo(params);
    // 构建请求头
    Header header = builderHeader();
    // 地址URL拼接
    String url;
    if (curServer.startsWith(UtilAndComs.HTTPS) || 
        curServer.startsWith(UtilAndComs.HTTP)) {
        url = curServer + api;
    } else {
        if (!IPUtil.containsPort(curServer)) {
            curServer = curServer + IPUtil.IP_PORT_SPLITER + serverPort;
        }
        // 走到这里
        // http://localhost:8848/nacos/v1/ns/instance
        url = NamingHttpClientManager.getInstance().getPrefix() + curServer + api;
    }

    try {
        // 发起请求
        HttpRestResult<String> restResult = nacosRestTemplate
                .exchangeForm(url, header, 
                 Query.newInstance().initParams(params), body, method, String.class);
        end = System.currentTimeMillis();
        // 监控
        MetricsMonitor.getNamingRequestMonitor(method, url, 
                 String.valueOf(restResult.getCode()))
                .observe(end - start);
        // 如果成功返回 OK
        if (restResult.ok()) {
            return restResult.getData();
        }
        if (HttpStatus.SC_NOT_MODIFIED == restResult.getCode()) {
            return StringUtils.EMPTY;
        }
        throw new NacosException(restResult.getCode(), restResult.getMessage());
    } catch (Exception e) {
        NAMING_LOGGER.error("[NA] failed to request", e);
        throw new NacosException(NacosException.SERVER_ERROR, e);
    }
}
exchangeForm 发起注册

    下面就是和 RestTemplate 类似发起真正的网络请求进行服务注册

public <T> HttpRestResult<T> exchangeForm(String url, Header header, Query query,
                                          Map<String, String> bodyValues,
        String httpMethod, Type responseType) throws Exception {
    RequestHttpEntity requestHttpEntity = new RequestHttpEntity(
            header.setContentType(MediaType.APPLICATION_FORM_URLENCODED), 
        query, bodyValues);
    // 服务注册执行
    return execute(url, httpMethod, requestHttpEntity, responseType);
}

private <T> HttpRestResult<T> execute(String url, String httpMethod, 
                                      RequestHttpEntity requestEntity,
        Type responseType) throws Exception {
    URI uri = HttpUtils.buildUri(url, requestEntity.getQuery());
    if (logger.isDebugEnabled()) {
        logger.debug("HTTP method: {}, url: {}, body: {}", 
                     httpMethod, uri, requestEntity.getBody());
    }

    ResponseHandler<T> responseHandler = super.selectResponseHandler(responseType);
    HttpClientResponse response = null;
    try {
        // 发起请求
        response = this.requestClient().execute(uri, httpMethod, requestEntity);
        // 返回响应
        return responseHandler.handle(response);
    } finally {
        if (response != null) {
            response.close();
        }
    }
}

5. 注册结果

image.png

6. 注册中心服务列表展示

image.png
    到这里我们算是将客户端发起注册流程成功的分析完了

小结

  1. 通过配置文件获取本地配置的实例注册信息
  2. 进行注册实例构建
  3. 构建心跳信息,并建立一个后台的心跳发送任务,定时发送
  4. 通过Http请求进行服务注册,服务的注册地址是:http://IP:端口/nacos/v1/ns/instance

其他系列源码分析

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 集群注册表同步机制