1.服务端注册实现

Nacos 源码分析02  注册_ide

这里有两个Set,一个是用来存储临时实例,一个是用来存储持久化实例,有个关键点,什么情况会存储在临时实例,什么情况下会存储持久化实例,这个是由客户端的配置来决定的,默认情况下客户端配置ephemeral=true,如果你想把实例用持久化的方式来存储,可以设置ephemeral=false,这样在客户端发起注册的时候会把这个参数带到Nacos Server,Nacos Server就会按照持久化的方式来存储。

注意:Nacos目前持久化存储的方式采用的是本地文件存储的方式。

Nacos的客户端是基于SpringBoot的自动装配实现的

Nacos提供了服务注册的API接口,客户端只需要向该接口发送请求,即可实现服务注册

NacosServiceRegistryAutoConfiguration 负责自动装配

Nacos 源码分析02  注册_封装_02

继续调用 NacosAutoServiceRegistration ,父类 AbstractAutoServiceRegistration初始化;可以看到它实现了ApplicationListener接口,监听Spring容器启动过程中的事件。在监听到WebServerInitializedEvent(web服务初始化完成)的事件后,执行了bind 方法。

bind方法调用start方法,继续调用注册核心方法register

核心实现如下

@Override
public void register(Registration registration) {
    // 判断serviceId是否为空,也就是spring.application.name不能为空
    if (StringUtils.isEmpty(registration.getServiceId())) {
        log.warn("No service to register for nacos client...");
        return;
    }
    // 获取Nacos的命名服务,其实就是注册中心服务
    NamingService namingService = namingService();
    // 获取 serviceId 和 Group
    String serviceId = registration.getServiceId();
    String group = nacosDiscoveryProperties.getGroup();
    // 封装服务实例的基本信息,如 cluster-name、是否为临时实例、权重、IP、端口等
    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) {
        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);
        }
    }
}

NacosNamingService 提供服务注册和订阅能力

@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    // 检查超时参数是否异常。心跳超时时间(默认15秒)必须大于心跳周期(默认5秒)
    NamingUtils.checkInstanceIsLegal(instance);
    // 拼接得到新的服务名,格式为:groupName@@serviceId
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    // 判断是否为临时实例,默认为 true。
    if (instance.isEphemeral()) {
        // 如果是临时实例,需要定时向 Nacos 服务发送心跳
        BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
        beatReactor.addBeatInfo(groupedServiceName, beatInfo);
    }
    // 发送注册服务实例的请求
    serverProxy.registerService(groupedServiceName, groupName, instance);
}

NacosProxy的注册服务registerService实现,完成服务注册

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()));
	// 通过POST请求将上述参数,发送到 /nacos/v1/ns/instance
    reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);

}
namespace_id:环境
service_name:服务名称
group_name:组名称
cluster_name:集群名称
ip: 当前实例的ip地址
port: 当前实例的端口

Nacos 源码分析02  注册_数据_03

先获取client,分成grpc和http两种实现

Nacos 源码分析02  注册_ide_04

然后注册服务,先看下grpc的实现

Nacos 源码分析02  注册_ide_05

1.先缓存到cache

2.执行注册流程,如下图

Nacos 源码分析02  注册_封装_06

2.1 可以看到是grpcClient发送请求到服务server端注册,如下图实现

Nacos 源码分析02  注册_数据_07

2.2 然后更新本地内存redoData数据,设置为已经注册

Nacos 源码分析02  注册_ide_08

再看下http协议的实现

Nacos 源码分析02  注册_ide_09

按照域名 调用server端,或者没有域名,就随机选择一个server,调用

callServer进行注册

Nacos 源码分析02  注册_ide_10

也是执行一个http调用,发送注册请求到server端

Nacos 源码分析02  注册_封装_11

++++++++++++++++++++++++++++++++++++++++++++

继续看下server端处理注册InstanceControllerV2的register方法

Nacos 源码分析02  注册_ide_12

1.先检查入参serviceName

2.构建instance实例,设置参数

3.调用registerInstance方法注册,如下

Nacos 源码分析02  注册_数据_133.1 先调用getService方法获取service服务,底层是创建service对象

3.2 执行客户端client注册分成两种实现,EphemeralClient(临时 AP模式)

和PersistentClient(持久化 CP模式)

先看下AP模式

Nacos 源码分析02  注册_数据_14

3.2.1 根据clientId获取client,也就是从本地内存clients中获取,校验合法性

3.2.2 发送注册通知事件

再看下CP模式

Nacos 源码分析02  注册_封装_15

(1) 校验参数,然后构建request请求,序列化处理,转成writeRequest

Nacos 源码分析02  注册_ide_16

(2) 异步线程写writeAsync

Nacos 源码分析02  注册_ide_17

(3) 基于raft协议,获取node,如果是leader,就执行写入数据,如下

Nacos 源码分析02  注册_封装_18

(4) 如果不是leader的节点,就异步调用leader节点写,并且执行回调函数

Nacos 源码分析02  注册_ide_19

服务端更新实例列表

Nacos 源码分析02  注册_封装_20

内部维护了一个阻塞队列,存放服务列表变更的事件

addTask时,将任务加入该阻塞队列:

// DistroConsistencyServiceImpl.Notifier类的 addTask 方法:
public void addTask(String datumKey, DataOperation action) {

    if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
        return;
    }
    if (action == DataOperation.CHANGE) {
        services.put(datumKey, StringUtils.EMPTY);
    }
    // 任务放入阻塞队列
    tasks.offer(Pair.with(datumKey, action));
}

notifier还是一个Runnable,通过一个单线程的线程池来不断从阻塞队列中获取任务,执行服务列表的更新。来看下其中的run方法:

// DistroConsistencyServiceImpl.Notifier类的run方法:
@Override
public void run() {
    Loggers.DISTRO.info("distro notifier started");
	// 死循环,不断执行任务。因为是阻塞队列,不会导致CPU负载过高
    for (; ; ) {
        try {
            // 从阻塞队列中获取任务
            Pair<String, DataOperation> pair = tasks.take();
            // 处理任务,更新服务列表
            handle(pair);
        } catch (Throwable e) {
            Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
        }
    }
}
// DistroConsistencyServiceImpl.Notifier类的 handle 方法:
private void handle(Pair<String, DataOperation> pair) {
    try {
        String datumKey = pair.getValue0();
        DataOperation action = pair.getValue1();

        services.remove(datumKey);

        int count = 0;

        if (!listeners.containsKey(datumKey)) {
            return;
        }
		// 遍历,找到变化的service,这里的 RecordListener就是 Service
        for (RecordListener listener : listeners.get(datumKey)) {

            count++;

            try {
                // 服务的实例列表CHANGE事件
                if (action == DataOperation.CHANGE) {
                    // 更新服务列表
                    listener.onChange(datumKey, dataStore.get(datumKey).value);
                    continue;
                }
				// 服务的实例列表 DELETE 事件
                if (action == DataOperation.DELETE) {
                    listener.onDelete(datumKey);
                    continue;
                }
            } catch (Throwable e) {
                Loggers.DISTRO.error("[NACOS-DISTRO] error while notifying listener of key: {}", datumKey, e);
            }
        }

        if (Loggers.DISTRO.isDebugEnabled()) {
            Loggers.DISTRO
                .debug("[NACOS-DISTRO] datum change notified, key: {}, listener count: {}, action: {}",
                       datumKey, count, action.name());
        }
    } catch (Throwable e) {
        Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
    }
}

更新onChange

@Override
public void onChange(String key, Instances value) throws Exception {

    Loggers.SRV_LOG.info("[NACOS-RAFT] datum is changed, key: {}, value: {}", key, value);

	// 更新实例列表
    updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key));

    recalculateChecksum();
}
public void updateIPs(Collection<Instance> instances, boolean ephemeral) {
    // 准备一个Map,key是cluster,值是集群下的Instance集合
    Map<String, List<Instance>> ipMap = new HashMap<>(clusterMap.size());
    // 获取服务的所有cluster名称
    for (String clusterName : clusterMap.keySet()) {
        ipMap.put(clusterName, new ArrayList<>());
    }
    // 遍历要更新的实例
    for (Instance instance : instances) {
        try {
            if (instance == null) {
                Loggers.SRV_LOG.error("[NACOS-DOM] received malformed ip: null");
                continue;
            }
			// 判断实例是否包含clusterName,没有的话用默认cluster
            if (StringUtils.isEmpty(instance.getClusterName())) {
                instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME);
            }
			// 判断cluster是否存在,不存在则创建新的cluster
            if (!clusterMap.containsKey(instance.getClusterName())) {
                Loggers.SRV_LOG
                    .warn("cluster: {} not found, ip: {}, will create new cluster with default configuration.",
                          instance.getClusterName(), instance.toJson());
                Cluster cluster = new Cluster(instance.getClusterName(), this);
                cluster.init();
                getClusterMap().put(instance.getClusterName(), cluster);
            }
			// 获取当前cluster实例的集合,不存在则创建新的
            List<Instance> clusterIPs = ipMap.get(instance.getClusterName());
            if (clusterIPs == null) {
                clusterIPs = new LinkedList<>();
                ipMap.put(instance.getClusterName(), clusterIPs);
            }
			// 添加新的实例到 Instance 集合
            clusterIPs.add(instance);
        } catch (Exception e) {
            Loggers.SRV_LOG.error("[NACOS-DOM] failed to process ip: " + instance, e);
        }
    }

    for (Map.Entry<String, List<Instance>> entry : ipMap.entrySet()) {
        //make every ip mine
        List<Instance> entryIPs = entry.getValue();
        // 将实例集合更新到 clusterMap(注册表)
        clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);
    }

    setLastModifiedMillis(System.currentTimeMillis());
    // 发布服务变更的通知消息
    getPushService().serviceChanged(this);
    StringBuilder stringBuilder = new StringBuilder();

    for (Instance instance : allIPs()) {
        stringBuilder.append(instance.toIpAddr()).append("_").append(instance.isHealthy()).append(",");
    }

    Loggers.EVT_LOG.info("[IP-UPDATED] namespace: {}, service: {}, ips: {}", getNamespaceId(), getName(),
                         stringBuilder.toString());

}

2.客户端注册实现

Nacos 源码分析02  注册_封装_21

InstanceController的register方法 负责注册实现

@CanDistro
@PostMapping
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public String register(HttpServletRequest request) throws Exception {
	// 尝试获取namespaceId
    final String namespaceId = WebUtils
        .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
    // 尝试获取serviceName,其格式为 group_name@@service_name
    final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    NamingUtils.checkServiceNameFormat(serviceName);
	// 解析出实例信息,封装为Instance对象
    final Instance instance = parseInstance(request);
	// 注册实例
    serviceManager.registerInstance(namespaceId, serviceName, instance);
    return "ok";
}

继续看注册实现

/**
     * 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 {
	// 创建一个空的service(如果是第一次来注册实例,要先创建一个空service出来,放入注册表)
    // 此时不包含实例信息
    createEmptyService(namespaceId, serviceName, instance.isEphemeral());
    // 拿到创建好的service
    Service service = getService(namespaceId, serviceName);
    // 拿不到则抛异常
    if (service == null) {
        throw new NacosException(NacosException.INVALID_PARAM,
                                 "service not found, namespace: " + namespaceId + ", service: " + serviceName);
    }
    // 添加要注册的实例到service中
    addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}

创建好服务,就添加实例到service中

/**
     * Add instance to service.
     *
     * @param namespaceId namespace
     * @param serviceName service name
     * @param ephemeral   whether instance is ephemeral
     * @param ips         instances
     * @throws NacosException nacos exception
     */
public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)
    throws NacosException {
	// 监听服务列表用到的key,服务唯一标识,例如:com.alibaba.nacos.naming.iplist.ephemeral.public##DEFAULT_GROUP@@order-service
    String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);
    // 获取服务
    Service service = getService(namespaceId, serviceName);
    // 同步锁,避免并发修改的安全问题
    synchronized (service) {
        // 1)获取要更新的实例列表
        List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);
		// 2)封装实例列表到Instances对象
        Instances instances = new Instances();
        instances.setInstanceList(instanceList);
		// 3)完成 注册表更新 以及 Nacos集群的数据同步
        consistencyService.put(key, instances);
    }
}
该方法中对修改服务列表的动作加锁处理,确保线程安全。而在同步代码块中,包含下面几步:

1)先获取要更新的实例列表,addIpAddresses(service, ephemeral, ips);
2)然后将更新后的数据封装到Instances对象中,后面更新注册表时使用
3)最后,调用consistencyService.put()方法完成Nacos集群的数据同步,
   保证集群一致性。
 在第1步的addIPAddress中,会拷贝旧的实例列表,添加新实例到列表中。
 在第3步中,完成对实例状态更新后,则会用新列表直接覆盖旧实例列表。
 而在更新过程中,旧实例列表不受影响,用户依然可以读取。
这样在更新列表状态过程中,无需阻塞用户的读操作,也不会导致用户读取到脏数据,
性能比较好。这种方案称为CopyOnWrite方案。


3.Nacos注册表结构

Nacos 源码分析02  注册_数据_22


答:Nacos是多级存储模型,最外层通过namespace来实现环境隔离,然后是group分组,分组下就是服务,一个服务有可以分为不同的集群,集群中包含多个实例。因此其注册表结构为一个Map,类型是:

Map<String, Map<String, Service>>

key:是namespace_id,起到环境隔离的作用。namespace下可以有多个group

value:又是一个Map<String, Service>,代表分组及组内的服务。一个组内可以有多个服务

key:代表group分组,不过作为key时格式是group_name:service_name

value:分组下的某一个服务,例如userservice,用户服务。类型为Service,内部也包含一个Map<String,Cluster>,一个服务下可以有多个集群

key:集群名称

value:Cluster类型,包含集群的具体信息。一个集群中可能包含多个实例,也就是具体的节点信息,其中包含一个Set<Instance>,就是该集群下的实例的集合

Instance:实例信息,包含实例的IP、Port、健康状态、权重等等信息

4.Nacos 并发处理

Nacos如何保证并发写的安全性?

答:首先,在注册实例时,会对service加锁,不同service之间本身就不存在并发写问题,互不影响。相同service时通过锁来互斥。并且,在更新实例列表时,是基于异步的线程池来完成,而线程池的线程数量为1.

Nacos如何避免并发读写的冲突?

答:Nacos在更新实例列表时,会采用CopyOnWrite技术,首先将Old实例列表拷贝一份,然后更新拷贝的实例列表,再用更新后的实例列表来覆盖旧的实例列表。

Nacos如何应对阿里内部数十万服务的并发写请求?

答:Nacos内部会将服务注册的任务放入阻塞队列,采用线程池异步来完成实例更新,从而提高并发写能力

Nacos的健康检测有两种模式:

  • 临时实例:
  • 采用客户端心跳检测模式,心跳周期5秒
  • 心跳间隔超过15秒则标记为不健康
  • 心跳间隔超过30秒则从服务列表删除
  • 永久实例:
  • 采用服务端主动健康检测方式
  • 周期为2000 + 5000毫秒内的随机数
  • 检测异常只会标记为不健康,不会删除

5.Nacos 临时实例和健康检测

那么为什么Nacos有临时和永久两种实例呢?

以淘宝为例,双十一大促期间,流量会比平常高出很多,此时服务肯定需要增加更多实例来应对高并发,而这些实例在双十一之后就无需继续使用了,采用临时实例比较合适。而对于服务的一些常备实例,则使用永久实例更合适。

与eureka相比,Nacos与Eureka在临时实例上都是基于心跳模式实现,差别不大,主要是心跳周期不同,eureka是30秒,Nacos是5秒。

另外,Nacos支持永久实例,而Eureka不支持,Eureka只提供了心跳模式的健康监测,而没有主动检测功能。

  • 临时实例:
  • 采用客户端心跳检测模式,心跳周期5秒
  • 心跳间隔超过15秒则标记为不健康
  • 心跳间隔超过30秒则从服务列表删除
  • 永久实例:
  • 采用服务端主动健康检测方式
  • 周期为2000 + 5000毫秒内的随机数
  • 检测异常只会标记为不健康,不会删除


6.Nacos服务发现模式

Nacos的服务发现分为两种模式:

模式一:主动拉取模式,消费者定期主动从Nacos拉取服务列表并缓存起来,再服务调用时优先读取本地缓存中的服务列表。

模式二:订阅模式,消费者订阅Nacos中的服务列表,并基于UDP协议来接收服务变更通知。当Nacos中的服务列表更新时,会发送UDP广播给所有订阅者。

与Eureka相比,Nacos的订阅模式服务状态更新更及时,消费者更容易及时发现服务列表的变化,剔除故障服务。

从源码中可以看出,这段代码相当于定时10s(这个时间是从/nacos/v1/ns/instance/list接口里回传回来的)拉取一次服务,这里有个Nacos Server比较巧妙的设计需要提一下,在updateServiceNow方法中可以看到调用服务端/nacos/v1/ns/instance/list接口的时候传入了一个Udp的端口,这个端口的作用是如果Nacos Server感知到Service的变化,就会把变化信息通知给订阅了这个Service信息的客户端。

7.异步多线程注册

之前主要分析了Spring Cloud集成Nacos client的服务注册和服务拉取的逻辑,现在接着分析一下Nacos Server注册中心的核心功能逻辑及源码,首先来分析Nacos怎么能支持高并发的Intance的注册的。

先直接给答案:采用内存队列的方式进行服务注册

也就是说客户端在把自己的信息注册到Nacos Server的时候,并不是同步把信息写入到注册表中的,而且采取了先写入内存队列中,然后用独立的线程池来消费队列进行注册的。 代码如下

Nacos 源码分析02  注册_封装_23

Nacos 源码分析02  注册_数据_24

在进行队列消费的时候其实最终也是采用的JDK的线程池,追踪到实例化线程线程池的代码为:

Nacos 源码分析02  注册_封装_25

Nacos Server在把Instance写入注册表的是时候怎么去解决读写并发冲突的呢?

答案就是:Copy on write 思想

Nacos 源码分析02  注册_数据_26

+++++++++++++++++++++++++++++++++++++++++