深入解读Nacos源码

2,743 阅读4分钟

Nacos源码部分,我们主要阅读三部分:

  • 服务注册
  • 服务地址的获取
  • 服务地址变化的感知

下面我们基于这三个方面来分析Nacos是如何实现的。

Spring Cloud 什么时候完成服务注册

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

从源码层面分析Nacos服务注册的原理

Nacos提供了SDK及Open API的形式来实现服务注册。前面我们通过Open API 实现了一个服务地址的注册,其中serviceName表示服务名、ip/port表示该服务对应的地址。

对于服务注册,对外提供的服务接口请求地址nacos/v1/ns/instance,实现代码在nacos-naming模块下的InstanceController类中。

  • 从请求参数中获得serviceName(服务名)和namespaceId(命名空间id)。
  • 调用registerInstance注册实例

image.png `

/**
 * Register new instance.
 *
 * @param request http request
 * @return 'ok' if success
 * @throws Exception any error during register
 */
@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
    
    final String namespaceId = WebUtils
            .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
    final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    NamingUtils.checkServiceNameFormat(serviceName);
    
    final Instance instance = HttpRequestInstanceBuilder.newBuilder()
            .setDefaultInstanceEphemeral(switchDomain.isDefaultInstanceEphemeral()).setRequest(request).build();
    
    getInstanceOperator().registerInstance(namespaceId, serviceName, instance);
    return "ok";
}

上面这段代码,以本章中演示的项目为例,serviceName实际上就是spring-cloud-nacos-sample,namespaceId的值为public。接下来我们重点关注registerInstance方法,它的主要逻辑是:

  • 创建一个空服务(在Nacos控制台“服务列表”中展示的服务的信息),实际上是初始化一个serviceMap,它是一个ConcurrentHashMap集合。
  • getService,从serviceMap中根据namespaceId和serviceName得到一个服务对象。
  • 调用addInstance添加服务实例
/**
 * Register an instance to a service in AP mode.
 *
 * <p>This method creates service or cluster silently if they don't exist.
 *
 * @param namespaceId id of namespace
 * @param serviceName service name
 * @param instance    instance to register
 * @throws Exception any error occurred in the process
 */
public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
    
    createEmptyService(namespaceId, serviceName, instance.isEphemeral());
    
    Service service = getService(namespaceId, serviceName);
    
    checkServiceIsNull(service, namespaceId, serviceName);
    
    addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}

下面简单分析下createEmptyService创建空服务的代码,最终调用的方法是

  • 根据namespaceId、serviceName从缓存中获取service实例
  • 如果service实例为空,则创建并保存到缓存中
public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException {
    createServiceIfAbsent(namespaceId, serviceName, local, null);
}
/**
 * Create service if not exist.
 *
 * @param namespaceId namespace
 * @param serviceName service name
 * @param local       whether create service by local
 * @param cluster     cluster
 * @throws NacosException nacos exception
 */
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
        throws NacosException {
    Service service = getService(namespaceId, serviceName);
    if (service == null) {
        
        Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
        service = new Service();
        service.setName(serviceName);
        service.setNamespaceId(namespaceId);
        service.setGroupName(NamingUtils.getGroupName(serviceName));
        // now validate the service. if failed, exception will be thrown
        service.setLastModifiedMillis(System.currentTimeMillis());
        service.recalculateChecksum();
        if (cluster != null) {
            cluster.setService(service);
            service.getClusterMap().put(cluster.getName(), cluster);
        }
        service.validate();
        
        putServiceAndInit(service);
        if (!local) {
            addOrReplaceService(service);
        }
    }
}

也没有太复杂的逻辑,主要关注putServiceAndInit方法,它实现了以下功能:

  • 通过putService方法将服务缓存到内存。
  • service.init()建立心跳检测机制。
  • consistencyService.listent()实现数据一致性的监听。
private void putServiceAndInit(Service service) throws NacosException {
    putService(service);
    service = getService(service.getNamespaceId(), service.getName());
    service.init();
    consistencyService
            .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
    consistencyService
            .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
    Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
}

service.init()方法的代码就不看了,如图所示,它主要通过定时任务不断检测当前服务下所有实例最后发送心跳包的时间。如果超时,则设置healthy为false表示服务不健康,并且发送服务变更事件。在这里请大家思考一个问题,服务实例的最后心跳包更新时间是谁来触发的?Nacos客户端注册服务的同时也建立了心跳机制。

心跳检测机制图

下面看putService方法,它的功能是将service保存到serviceMap中。

/**
 * Put service into manager.
 *
 * @param service service
 */
public void putService(Service service) {
    if (!serviceMap.containsKey(service.getNamespaceId())) {
        serviceMap.putIfAbsent(service.getNamespaceId(), new ConcurrentSkipListMap<>());
    }
    serviceMap.get(service.getNamespaceId()).putIfAbsent(service.getName(), service);
}

上述步骤完成之后,继续调用addInstance方法把当前注册的服务实例保存到Service中。

addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);

至此,服务的注册基本上就完成了。最后,简单总结一下服务注册的完整过程:

  • Nacos客户端通过Open API的形式发送服务注册请求

Nacos服务端收到请求后,做以下三件事:

  1. 构建一个Service对象保存到ConcurrentHashMap集合中。
  2. 使用定时任务对当前服务下的所有实例建立心跳检测机制。
  3. 基于数据一致性协议将服务数据进行同步。

分析Nacos服务地址动态感知原理

服务消费者不仅需要获得服务提供者的地址列表,还需要在服务实例出现异常时监听服务地址的变化

nacos服务心跳与健康检查源码剖析

image.png

image.png