流程分析

  • 服务实例在启动时注册到服务注册表,并在关闭时注销
  • 服务消费者查询服务注册表,获得可用实例
  • 服务注册中心需要调用服务实例的健康检查API来验证它是否能够处理请求

nacos做注册中心和zookeeper的区别_初始化

源码解读:

1、客户端注册:

在nacos-discovery的META-INF/spring.factories中包含自动装配的配置信息如下:

nacos做注册中心和zookeeper的区别_spring_02

很多个自动配置类被加载了,其中跟服务注册有关的就是NacosServiceRegistryAutoConfiguration这个类,在NacosServiceRegistryAutoConfiguration这个类中,包含一个跟自动注册有关的Bean:

nacos做注册中心和zookeeper的区别_spring_03

在初始化时,其父类AbstractAutoServiceRegistration也被初始化了。

nacos做注册中心和zookeeper的区别_后端_04

AbstractAutoServiceRegistration类中,实现了ApplicationListener接口,监听Spring容器启动过程中的事件,在监听到WebServerInitializedEvent(web服务初始化完成)的事件后,执行了bind 方法。

nacos做注册中心和zookeeper的区别_初始化_05

类的关系图如下

nacos做注册中心和zookeeper的区别_后端_06

bind方法

@Deprecated
public void bind(WebServerInitializedEvent event)
    // 获取 ApplicationContext
    WebServerApplicationContext context = event.getApplicationContext();
    // 判断服务的 namespace,一般都是null
    if(!(context instanceof ConfigurableWebServerApplicationContext) || !"management".equals(((ConfigurableWebServerApplicationContext)context).getServerNamespace())) {
        // 记录当前 web 服务的端口
        this.port.compareAndSet(0, event.getWebServer().getPort());
        // 启动当前服务注册流程
        this.start();
    }
}

start方法

if(!this.isEnabled()) {
    if(logger.isDebugEnabled()) {
        logger.debug("Discovery Lifecycle disabled. Not starting");
    }

} else {
    // 当前服务处于未运行状态时,才进行初始化
    if(!this.running.get()) {
        // 发布服务开始注册的事件
        this.context.publishEvent(new InstancePreRegisteredEvent(this, this.getRegistration()));
        // 开始注册
        this.register();
        if(this.shouldRegisterManagement()) {
            this.registerManagement();
        }
        // 发布注册完成事件
        this.context.publishEvent(new InstanceRegisteredEvent(this, this.getConfiguration()));
        // 服务状态设置为运行状态,基于AtomicBoolean
        this.running.compareAndSet(false, true);
    }

}

register 注册方法(重要)

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

        try {
            // 开始注册
            namingService.registerInstance(serviceId, group, instance);
            log.info("nacos registry, {} {} {}:{} register finished", new Object[]{group, serviceId, instance.getIp(), Integer.valueOf(instance.getPort())});
        } catch (Exception var7) {
            log.error("nacos registry, {} register failed...{},", new Object[]{serviceId, registration.toString(), var7});
            ReflectionUtils.rethrowRuntimeException(var7);
        }

    }
}

registerInstance方法,最终调用的默认实现是NacosNamingService,提供了服务注册、订阅等功能

public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    // 检查超时参数是否异常。心跳超时时间(默认15秒)必须大于心跳周期(默认5秒)
    NamingUtils.checkInstanceIsLegal(instance);
    this.clientProxy.registerService(serviceName, groupName, instance);
}
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", new Object[]{this.namespaceId, serviceName, instance});
    // 拼接得到新的服务名,格式为:groupName@@serviceId
    String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
    // 判断是否为临时实例,默认为 true。
    if(instance.isEphemeral()) {
        // 如果是临时实例,需要定时向 Nacos 服务发送心跳(后面会讲到)
        BeatInfo params = this.beatReactor.buildBeatInfo(groupedServiceName, instance);
        this.beatReactor.addBeatInfo(groupedServiceName, params);
    }
    // 完成服务注册
    HashMap params1 = new HashMap(32);
    // namespace_id:环境
    params1.put("namespaceId", this.namespaceId);
    // service_name:服务名称
    params1.put("serviceName", groupedServiceName);
    // group_name:组名称
    params1.put("groupName", groupName);
    // cluster_name:集群名称
    params1.put("clusterName", instance.getClusterName());
    // ip: 当前实例的ip地址
    params1.put("ip", instance.getIp());
    // port: 当前实例的端口
    params1.put("port", String.valueOf(instance.getPort()));
    params1.put("weight", String.valueOf(instance.getWeight()));
    params1.put("enable", String.valueOf(instance.isEnabled()));
    params1.put("healthy", String.valueOf(instance.isHealthy()));
    params1.put("ephemeral", String.valueOf(instance.isEphemeral()));
    params1.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
    // 通过POST请求将上述参数,发送到 /nacos/v1/ns/instance
    this.reqApi(UtilAndComs.nacosUrlInstance, params1, "POST");
}

至此,客户端注册示例结束,大致流程如下

nacos做注册中心和zookeeper的区别_封装_07

2、服务端

对于nacos服务端,对外提供的服务接口请求地址为nacos/v1/ns/instance,实现代码在nacos-naming模块下的InstanceController类中,进入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";
}

ServiceManager就是Nacos中管理服务、实例信息的核心API

/**
 * 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);
}

addInstance方法

/**
* Add instance to service.
*
* @paramnamespaceId namespace
* @paramserviceName service name
* @paramephemeral   whether instance is ephemeral
* @paramips         instances
* @throwsNacosException 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);
    }
}

该方法中对修改服务列表的动作加锁处理,确保线程安全。而在同步代码块中,包含下面几步:

  • 先获取要更新的实例列表,addIpAddresses(service, ephemeral, ips);
  • 然后将更新后的数据封装到Instances对象中,后面更新注册表时使用
  • 最后,调用consistencyService.put()方法完成Nacos集群的数据同步,保证集群一致性。

Nacos集群一致性

consistencyService.put(key, instances),这里的ConsistencyService接口,代表集群一致性的接口,有很多中不同实现:

nacos做注册中心和zookeeper的区别_java_08

临时实例的一致性实现:DistroConsistencyServiceImpl类的put方法

public void put(String key, Record value) throws NacosException {
    // 先将要更新的实例信息写入本地实例列表
    onPut(key, value);
    // 开始集群同步
    distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE,
                        globalConfig.getTaskDispatchPeriod() / 2);
}
  • onPut(key, value):其中value就是Instances,要更新的服务信息。这行主要是基于线程池方式,异步的将Service信息写入注册表中(就是那个多重Map)
  • distroProtocol.sync():就是通过Distro协议将数据同步给集群中的其它Nacos节点

onPut方法

public void onPut(String key, Record value) {
  // 判断是否是临时实例
    if (KeyBuilder.matchEphemeralInstanceListKey(key)) {
        // 封装 Instances 信息到 数据集:Datum
        Datum<Instances> datum = new Datum<>();
        datum.value = (Instances) value;
        datum.key = key;
        datum.timestamp.incrementAndGet();
        // 放入DataStore
        dataStore.put(key, datum);
    }
    if (!listeners.containsKey(key)) {
        return;
    }
  // 放入阻塞队列,这里的 notifier维护了一个阻塞队列,并且基于线程池异步执行队列中的任务
    notifier.addTask(key, DataOperation.CHANGE);
}
// 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));
}

distroProtocol.sync()集群同步方法

public void sync(DistroKey distroKey, DataOperation action, long delay) {
    // 遍历 Nacos 集群中除自己以外的其它节点
    for (Member each : memberManager.allMembersWithoutSelf()) {
        DistroKey distroKeyWithTarget = new DistroKey(distroKey.getResourceKey(), distroKey.getResourceType(),
                                                      each.getAddress());
        // 定义一个Distro的同步任务
        DistroDelayTask distroDelayTask = new DistroDelayTask(distroKeyWithTarget, action, delay);
        // 交给线程池去执行
        distroTaskEngineHolder.getDelayTaskExecuteEngine().addTask(distroKeyWithTarget, distroDelayTask);
        if (Loggers.DISTRO.isDebugEnabled()) {
            Loggers.DISTRO.debug("[DISTRO-SCHEDULE] {} to {}", distroKey, each.getAddress());
        }
    }
}

NacosDelayTaskExecuteEngine,这个类维护了一个线程池,并且接收任务,执行任务,执行任务的方法为processTasks()方法:

protected void processTasks() {
    Collection<Object> keys = getAllTaskKeys();
    for (Object taskKey : keys) {
        AbstractDelayTask task = removeTask(taskKey);
        if (null == task) {
            continue;
        }
        NacosTaskProcessor processor = getProcessor(taskKey);
        if (null == processor) {
            getEngineLog().error("processor not found for task, so discarded. " + task);
            continue;
        }
        try {
            // 尝试执行同步任务,如果失败会重试
            if (!processor.process(task)) {
                retryFailedTask(taskKey, task);
            }
        } catch (Throwable e) {
            getEngineLog().error("Nacos task execute error : " + e.toString(), e);
            retryFailedTask(taskKey, task);
        }
    }
}

基于Distro模式的同步是异步进行的,并且失败时会将任务重新入队并充实,因此不保证同步结果的强一致性,属于AP模式的一致性策略。

服务端流程图:

nacos做注册中心和zookeeper的区别_java_09

3、总结

客户端启动时会将当前服务的信息包含ip、端口号、服务名、集群名、实例类型等信息封装为一个Instance对象,然后创建一个定时任务,每隔一段时间向Nacos服务器发送PUT请求并携带相关信息。

nacos服务器端在接收到心跳请求后,会去检查当前服务列表中有没有该实例,如果没有的话将当前服务实例重新注册,注册完成后立即开启一个异步任务,更新客户端实例的最后心跳时间,如果当前实例是非健康状态则将其改为健康状态。

nacos服务器端接收到注册实例请求后,会将携带的数据封装为Instance对象,然后为这个服务实例创建一个服务Service,一个Service下可能有多个服务实例,服务在Nacos保存到ConcurrentHashMap中Map(namespace,Map(group::serviceName, Service));

Nacos存储模型,最外层通过namespace来实现环境隔离,然后是group分组,分组下就是服务,一个服务有可以分为不同的集群,集群中包含多个实例。因此其注册表结构为一个Map,类型是:
Map<String, Map<String, Service>>,
外层key是namespace_id,内层key是group+serviceName.
Service内部维护一个Map,结构是:Map<String,Cluster>,key是clusterName,值是集群信息
Cluster内部维护一个Set集合,元素是Instance类型,代表集群中的多个实例。

nacos将实例添加到对应服务列表中会根据AP和CP不同的模式,采用不同协议。

  • CP模式就是基于Raft协议(通过leader节点将实例数据更新到内存和磁盘文件中,并且通过CountDownLatch实现了一个简单的raf写入数据的逻辑,必须集群半数以上节点写入成功才会给客户端返回成功)
  • AP模式基于Distro协议(向任务阻塞队列添加一个本地服务实例改变任务,去更新本地服务列表,然后在遍历集群中所有节点,分别创建数据同步任务放进阻塞队异步进行集群数据同步,不保证集群节点数据同步完成即可返回)
  • nacos在将服务实例更新到服务注册表中时,为了防止并发读写冲突,采用的是写时复制的思想,将原注册表数据拷贝一份,添加完成之后再替换回真正的注册。

nacos在更新完成之后,通过发布服务变化事件,将服务变动通知给客户端,采用的是UDP通信,客户端接收到UDP消息后会返回一个ACK信号,如果一定时间内服务端没有收到ACK信号,还会尝试重发,当超出重发时间后就不在重发。

客户端通过定时任务定时从服务端拉取服务数据保存在本地缓存。

服务端在发生心跳检测、服务列表变更或者健康状态改变时会触发推送事件,在推送事件中会基于UDP通信将服务列表推送到客户端,虽然通过UDP通信不能保证消息的可靠抵达,但是由于Nacos客户端会开启定时任务,每隔一段时间更新客户端缓存的服务列表,通过定时轮询更新服务列表做兜底,所以不用担心数据不会更新的情况,这样既保证了实时性,又保证了数据更新的可靠性。

4、心跳机制

服务的健康检查分为两种模式:

临时实例:

  • 采用客户端心跳检测模式,心跳周期5秒
  • 心跳间隔超过15秒则标记为不健康
  • 心跳间隔超过30秒则从服务列表删除

永久实例:

  • 采用服务端主动健康检测方式
  • 周期为2000 + 5000毫秒内的随机数
  • 检测异常只会标记为不健康,不会删除

nacos 目前的instance有一个ephemeral字段属性,该字段表示实例是否是临时实例还是持久化实例。如果是临时实例则不会在nacos中持久化,需要通过心跳上报,如果一段时间没有上报心跳,则会被nacos服务端删除。删除后如果又重新开始上报,则会重新实例注册。而持久化实例会被nacos服务端持久化,此时即使注册实例的进程不存在,这个实例也不会删除,只会将健康状态设置成不健康。

这里就涉及到了nacos的AP和CP模式 ,默认是AP,即nacos的client的节点注册时ephemeral=true,那么nacos集群中这个client节点就是AP,采用的是distro 协议,而ephemeral=false时就是CP采用的是raft协议实现。所以nacos可以很好的解决了业务常见的不同需求。

#false为永久实例,true表⽰临时实例开启,注册为临时实例,默认是true
 spring.cloud.nacos.discovery.ephemeral=true

为什么nacos有两种心跳机制?

对于临时实例,健康检查失败,则直接删除。这种特性适合于需要应对流量突增的场景,服务可以弹性扩容,当流量过去后,服务停掉即可自动注销。

对于持久化实例,健康检查失败,会设置为不健康状态。它的优点就是可以实时的监控到实例的健康状态,便于后续的告警和扩容等一系列处理。