分布式中间件--nacos入门解析

一、Nacos简介

1、Nacos是什么?

Nacos是阿里巴巴开源的一个为微服务提供服务发现、服务配置和服务管理的微服务基础设施,简单说就是Nacos为微服务架构提供了分布式配置和服务注册中心的工作。

2、Nacos有什么功能?

Nacos主要有两大功能:注册中心和配置中心

2.1、注册中心

a.服务发布:服务提供者发布服务到nacos,nacos存储服务和提供者关系;

b.服务订阅:服务消费者从nacos订阅服务,拉去服务提供者信息列表;

c.变更推送:当服务提供者信息变更时,实时通知服务消费者;

d.路由策略:根据不同路由规则,推送不同服务提供者信息给消费者;

e.健康检测:和服务提供者和服务消费者保持心跳,检测服务的健康状态;

2.2、配置中心

a.管理配置:配置的增删改查管理;

b.监听配置:客户端实时监听配置的更新情况;

c.灰度更新:允许针对部分客户端进行配置更新;

d.配置快照:客户端需要缓存配置快照,当nacos服务器不可用时可以使用本地配置,提高整体容灾能力。

3、Nacos有哪些概念?

3.1、命名空间(namespace)

命名空间是用于配置和服务的空间隔离,不同命名空间下的数据相互独立,不同命名空间下可以存在相同配置和相同服务,通常命名空间可用于不同环境。如开发环境、测试环境和生产环境可以通过命名空间来进行区分隔离。

nacos默认有一个保留的命名空间为public,每一个命名空间都有一个唯一的ID,如果没有手动配置则会自动生产一个。服务管理和配置管理都是在命名空间区域内进行管理,每一个服务和配置都会绑定一个命名空间。

3.2、配置分组(Group)

同一个命名空间下可以有多个应用的配置,每个应用都可能有相同的配置,所以需要有一个分组来将属于同一个应用的配置进行区分。配置分组不需要单独管理,在管理配置集时添加配置分组即可。

3.3、配置集(Data)

配置集是一组配置的集合,通常一个配置文件就是一个配置集,每一个配置集都有一个配置集ID叫做Data ID,如和缓存相关配置都可以放在配置集cache.properties中,数据库配置放在db.properties中。

配置集ID可以重复,但是同一个命名空间下同一个配置分组下的配置集ID不可重复,也就是说命名空间+配置分组+配置集ID可以唯一定位一个配置文件。

3.4、服务

通过预定义接口网络访问的提供给客户端的软件功能。每个服务都有一个服务名是服务提供的标识,通过该标识可以唯一确定其指代的服务。

3.5、服务注册

服务提供者将自己提供的服务注册到nacos,nacos存储服务和服务提供者关系。

3.6、服务订阅

服务消费者从nacos上获取对应服务的服务提供者信息列表

3.7、元数据

Nacos数据(如配置和服务)描述信息,如服务版本、权重、容灾策略、负载均衡策略、鉴权配置、各种自定义标签 (label),从作用范围来看,分为服务级别的元信息、集群的元信息及实例的元信息。

3.8、权重

实例级别的配置。权重为浮点数。权重越大,分配给该实例的流量越大。

3.9、健康检查

以指定方式检查服务下挂载的实例 (Instance) 的健康度,从而确认该实例 (Instance) 是否能提供服务。根据检查结果,实例 (Instance) 会被判断为健康或不健康。对服务发起解析请求时,不健康的实例 (Instance) 不会返回给客户端。

3.10、健康保护阈值

为了防止因过多实例 (Instance) 不健康导致流量全部流向健康实例 (Instance) ,继而造成流量压力把健康实例 (Instance) 压垮并形成雪崩效应,应将健康保护阈值定义为一个 0 到 1 之间的浮点数。当域名健康实例数 (Instance) 占总服务实例数 (Instance) 的比例小于

该值时,无论实例 (Instance) 是否健康,都会将这个实例 (Instance) 返回给客户端。这样做虽然损失了一部分流量,但是保证了集群中剩余健康实例 (Instance) 能正常工作。

 

二、Nacos使用

2.1、Nacos的Open API

Nacos提供了大量的HTTP API,其中包括配置管理、服务管理和命名空间管理等,核心API如下

配置管理 获取配置 GET /nacos/v1/cs/configs
  监听配置 POST /nacos/v1/cs/configs/listener
  发布配置 POST /nacos/v1/cs/configs
  删除配置 DELETE /nacos/v1/cs/configs
  查询历史版本配置 GET /nacos/v1/cs/history?search=accurate
  查询上一个版本配置 GET /nacos/v1/cs/history/previous
服务发现 注册实例 POST /nacos/v1/ns/instance
  注销实例 DELETE /nacos/v1/ns/instance
  修改实例 PUT /nacos/v1/ns/instance
  查询实例列表   GET /nacos/v1/ns/instance/list
  查询实例详情 GET /nacos/v1/ns/instance
  发送实例心跳 PUT /nacos/v1/ns/instance/beat
  创建服务 POST /nacos/v1/ns/service
  删除服务 DELETE /nacos/v1/ns/service
  修改服务 PUT /nacos/v1/ns/service
  查询服务详情 GET /nacos/v1/ns/service
  查询服务列表 GET /nacos/v1/ns/service/list
  查询系统数据指标 GET /nacos/v1/ns/operator/metrics
  查询集群服务器列表 GET /nacos/v1/ns/operator/servers
  查询集群当前Leader GET /nacos/v1/ns/raft/leader
  更新实例健康状态 PUT /nacos/v1/ns/health/instance
  批量更新实例元数据 PUT /nacos/v1/ns/instance/metadata/batch
命名空间 查询命名空间列表 GET /nacos/v1/console/namespaces
  创建命名空间 POST /nacos/v1/console/namespaces
  修改命名空间 PUT /nacos/v1/console/namespaces
  删除命名空间 DELETE /nacos/v1/console/namespaces


2.2、JAVA集成Nacos的SDK

Maven依赖

<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>${version}</version>
</dependency>

2.2.1、配置管理

和配置相关功能都定义在ConfigService接口中,根据NacosFactory可以创建ConfigService对象,调用ConfigService相关方法就可对配置文件进行增删改查或监听配置更新,ConfigService相关方法定义如下:

 

public interface ConfigService {

        /**
         * 获取配置
         */
        String getConfig(String dataId, String group, long timeoutMs) throws NacosException;

        /**
         * 获取配置并添加监听器监听配置变更
         */
        String getConfigAndSignListener(String dataId, String group, long timeoutMs, Listener listener)
                throws NacosException;

        /**
         * 添加监听器监听配置变更
         */
        void addListener(String dataId, String group, Listener listener) throws NacosException;

        /**
         * 发布配置
         */
        boolean publishConfig(String dataId, String group, String content) throws NacosException;

        /**
         * 发布指定类型的配置,如yml、xml、properties、json等
         */
        boolean publishConfig(String dataId, String group, String content, String type) throws NacosException;

        /**
         * 删除配置
         */
        boolean removeConfig(String dataId, String group) throws NacosException;

        /**
         * 删除监听器
         */
        void removeListener(String dataId, String group, Listener listener);

        /**
         * 获取服务器状态
         */
        String getServerStatus();

        /**
         * 关闭服务
         */
        void shutDown() throws NacosException;
    }

 

ConfigService测试案例代码如下:

public static void main(String[] args) throws NacosException {
        /** 配置管理服务*/
        String nacosServer = "localhost:8848";
        ConfigService configService = NacosFactory.createConfigService(nacosServer);

        String dataId = "db.config";
        String group = "lucky";
        /** 1.发布配置*/
        String configContent = "";
        configService.publishConfig(dataId, group, configContent);
        /** 2.获取配置*/
        String config = configService.getConfig(dataId, group, 5000);
        /** 3.添加配置更新监听器*/
        configService.addListener(dataId, group, new Listener() {
            @Override
            public Executor getExecutor() {
                return null;
            }

            @Override
            public void receiveConfigInfo(String configInfo) {
                System.out.println("监听配置更新:" + configInfo);
                //TODO 处理配置更新
            }
        });
        while (true){

        }
    }

2.2.2、服务管理

服务管理相关功能都由NamingService接口定义,根据NacosFactory可以获取NamingService实例,NamingService包含服务注册、订阅等相关方法,定义如下:

public interface NamingService {

        /**
         *  注册服务实例
         */
        void registerInstance(String serviceName, String ip, int port) throws NacosException;

        void registerInstance(String serviceName, String groupName, String ip, int port) throws NacosException;

        void registerInstance(String serviceName, String ip, int port, String clusterName) throws NacosException;

        void registerInstance(String serviceName, String groupName, String ip, int port, String clusterName) throws NacosException;

        void registerInstance(String serviceName, Instance instance) throws NacosException;

        void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException;

        /**
         * 注销服务实例
         */
        void deregisterInstance(String serviceName, String ip, int port) throws NacosException;

        void deregisterInstance(String serviceName, String groupName, String ip, int port) throws NacosException;

        void deregisterInstance(String serviceName, String ip, int port, String clusterName) throws NacosException;

        void deregisterInstance(String serviceName, String groupName, String ip, int port, String clusterName) throws NacosException;

        void deregisterInstance(String serviceName, Instance instance) throws NacosException;

        void deregisterInstance(String serviceName, String groupName, Instance instance) throws NacosException;

        /**
         * 根据条件获取服务实例列表
         */
        List<Instance> getAllInstances(String serviceName) throws NacosException;

        List<Instance> getAllInstances(String serviceName, String groupName) throws NacosException;

        List<Instance> getAllInstances(String serviceName, boolean subscribe) throws NacosException;

        List<Instance> getAllInstances(String serviceName, String groupName, boolean subscribe) throws NacosException;

        List<Instance> getAllInstances(String serviceName, List<String> clusters) throws NacosException;

        List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters) throws NacosException;

        List<Instance> getAllInstances(String serviceName, List<String> clusters, boolean subscribe) throws NacosException;

        List<Instance> getAllInstances(String serviceName, String groupName, List<String> clusters, boolean subscribe) throws NacosException;

        /**
         * 根据条件选择服务实例列表
         */
        List<Instance> selectInstances(String serviceName, boolean healthy) throws NacosException;

        List<Instance> selectInstances(String serviceName, String groupName, boolean healthy) throws NacosException;

        List<Instance> selectInstances(String serviceName, boolean healthy, boolean subscribe) throws NacosException;

        List<Instance> selectInstances(String serviceName, String groupName, boolean healthy, boolean subscribe) throws NacosException;

        List<Instance> selectInstances(String serviceName, List<String> clusters, boolean healthy) throws NacosException;

        List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy) throws NacosException;

        List<Instance> selectInstances(String serviceName, List<String> clusters, boolean healthy, boolean subscribe) throws NacosException;

        List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy, boolean subscribe) throws NacosException;

        /**
         * 根据条件以及负载均衡策略选择一个健康的服务实例
         */
        Instance selectOneHealthyInstance(String serviceName) throws NacosException;

        Instance selectOneHealthyInstance(String serviceName, String groupName) throws NacosException;

        Instance selectOneHealthyInstance(String serviceName, boolean subscribe) throws NacosException;

        Instance selectOneHealthyInstance(String serviceName, String groupName, boolean subscribe) throws NacosException;

        Instance selectOneHealthyInstance(String serviceName, List<String> clusters) throws NacosException;

        Instance selectOneHealthyInstance(String serviceName, String groupName, List<String> clusters) throws NacosException;

        Instance selectOneHealthyInstance(String serviceName, List<String> clusters, boolean subscribe) throws NacosException;

        Instance selectOneHealthyInstance(String serviceName, String groupName, List<String> clusters, boolean subscribe) throws NacosException;

        /**
         * 订阅服务,并开启Listener监听服务变更事件
         */
        void subscribe(String serviceName, EventListener listener) throws NacosException;

        void subscribe(String serviceName, String groupName, EventListener listener) throws NacosException;

        void subscribe(String serviceName, List<String> clusters, EventListener listener) throws NacosException;

        void subscribe(String serviceName, String groupName, List<String> clusters, EventListener listener)
                throws NacosException;

        /**
         * 取消订阅服务,并关闭Listener监听服务变更事件
         */
        void unsubscribe(String serviceName, EventListener listener) throws NacosException;

        void unsubscribe(String serviceName, String groupName, EventListener listener) throws NacosException;

        void unsubscribe(String serviceName, List<String> clusters, EventListener listener) throws NacosException;

        void unsubscribe(String serviceName, String groupName, List<String> clusters, EventListener listener)
                throws NacosException;

        /**
         * 根据条件获取所有服务名称列表
         */
        ListView<String> getServicesOfServer(int pageNo, int pageSize) throws NacosException;

        ListView<String> getServicesOfServer(int pageNo, int pageSize, String groupName) throws NacosException;

        ListView<String> getServicesOfServer(int pageNo, int pageSize, AbstractSelector selector) throws NacosException;

        ListView<String> getServicesOfServer(int pageNo, int pageSize, String groupName, AbstractSelector selector) throws NacosException;

        /**
         * 获取当前客户端订阅的服务列表
         */
        List<ServiceInfo> getSubscribeServices() throws NacosException;

        /**
         * 获取服务器状态
         */
        String getServerStatus();

        /**
         * 关闭服务器
         */
        void shutDown() throws NacosException;
    }

 NamingService测试案例代码如下:

public static void main(String[] args) throws NacosException {
        String serverAddr = "42.192.94.208:8858";
        /** 1.创建NamingService实例 */
        NamingService namingService = NacosFactory.createNamingService(serverAddr);
        /** 2.注册实例*/
        namingService.registerInstance("testService", "localhost", 8080);
        /** 3.注销实例*/
        namingService.deregisterInstance("testService", "localhost", 8080);
        /** 4.获取所有健康实例*/
        List<Instance> instances = namingService.selectInstances("testService", true);
        /** 5.监听服务变化*/
        namingService.subscribe("testService", new EventListener() {
            @Override
            public void onEvent(Event event) {
                System.out.println("处理服务变更事件");
                if(event instanceof NamingEvent){
                    //TODO
                }
            }
        });
        while (true){

        }
    }

2.3、dubbo集成Nacos注册中心

dubbo采用Nacos作为注册中心,只需要在配置注册中心时将地址改成nacos地址即可,如下:

XML配置

<!-- nacos地址 -->
<dubbo:registry address="nacos://127.0.0.1:8848" />

 外部配置

## dubbo注册中心地址
dubbo.registry.address = zookeeper://10.20.153.10:2181

 2.4、SpringBoot集成Nacos配置中心

添加nacos依赖

<dependency>
         <groupId>com.alibaba.boot</groupId>
         <artifactId>nacos-config-spring-boot-starter</artifactId>
         <version>0.2.1</version>
</dependency>

 

版本号0.2.x.RELEASE对应的是 Spring Boot 2.x 版本,版本0.1.x.RELEASE对应的是 Spring Boot 1.x 版本

在application.properties配置文件中添加nacos地址配置

nacos.config.server-addr=127.0.0.1:8848

 在SpringBoot启动类添加@NacosProperySource注解添加Nacos配置来源,autoRefreshed表示是否自动更新

@NacosPropertySource(dataId = "db.config", autoRefreshed = true)

 通过nacos的@NacosValue注解给变量赋值配置的值,autoRefreshed表示是否自动更新,如:

    @NacosValue(value = "${db.username:tempUser}", autoRefreshed = true)
    private String dbUser;

    @NacosValue(value = "${db.password:tempPassword}")
    private String dbPassword;

 

三、Nacos实现原理

3.1、配置中心实现原理

Nacos提供了大量的配置管理相关API供客户端调用,客户端可以很方便的调用API来进行配置管理。所以Nacos Client启动的时候只需要调用Nacos server的接口就可以获取到所有的配置。

所以客户端获取配置的重点是如何进行热更新,也就是当服务端配置更新后,客户端是如何根据监听器进行实时更新的,监听器又是如何实现的呢?首先就需要从ConfigService的addListener方法入手。

ConfigService接口的实现类是NacosConfigService,addListener方法源码如下:

 1 private final ClientWorker worker;
 2 
 3     /** NacosConfigService类 添加配置更新监听器方法
 4      * @param dataId : 配置集
 5      * @param group : 配置分组
 6      * @param listener : 配置更新监听器
 7      *  */
 8     public void addListener(String dataId, String group, Listener listener) throws NacosException {
 9         //调用ClientWorker对象方法
10         worker.addTenantListeners(dataId, group, Arrays.asList(listener));
11     }
12 
13     //Http客户端
14     private final HttpAgent agent;
15 
16     /** ClientWorker类 添加监听器方法 */
17     public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners) throws NacosException {
18         group = null2defaultGroup(group);
19         String tenant = agent.getTenant();
20         CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
21         for (Listener listener : listeners) {
22             /** 调用CacheData对象的addListener方法*/
23             cache.addListener(listener);
24         }
25     }

 

/** CacheData类 监听器列表*/
    private final CopyOnWriteArrayList<ManagerListenerWrap> listeners;

    /**
     * CacheData类 添加监听器
     * */
    public void addListener(Listener listener) {
        if (null == listener) {
            throw new IllegalArgumentException("listener is null");
        }
        /** 包装Listener*/
        ManagerListenerWrap wrap = (listener instanceof AbstractConfigChangeListener) ? new ManagerListenerWrap(listener, md5, content)
                        : new ManagerListenerWrap(listener, md5);

        /** 将监听器添加到列表中*/
        if (listeners.addIfAbsent(wrap)) {
            LOGGER.info("[{}] [add-listener] ok, tenant={}, dataId={}, group={}, cnt={}", name, tenant, dataId, group,
                    listeners.size());
        }
    }

 

 逻辑并不复杂,最终是将Listener对象进行封装并添加到了CacheData对象的listeners列表中存储起来。既然有地方存了,那么就需要有地方去读,而开启监听是通过ClientWorker实例来实现。

NacosConfigService初始化时,会初始化ClientWorker对象,ClientWorker构造函数如下:

/** ClientWorker构造函数 */
    public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
                        final Properties properties) {
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;

        /** 1.初始化配置*/
        init(properties);

        /** 2.创建定时任务线程池*/
        this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });

        /** 3.创建定时任务线程池*/
        this.executorService = Executors
                .newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(r);
                        t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
                        t.setDaemon(true);
                        return t;
                    }
                });

        /** 4.开启定时任务,10毫秒执行一次*/
        this.executor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    /** 5.检测配置信息*/
                    checkConfigInfo();
                } catch (Throwable e) {
                    LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
                }
            }
        }, 1L, 10L, TimeUnit.MILLISECONDS);
    }

 

ClientWorker初始化时会创建两个定时任务线程池,一个只有一个线程每10毫秒执行一次checkConfigInfo方法,而另一个线程池就是专门用来处理checkConfigInfo方法内部的检查配置的逻辑,源码如下:

/** ClientWorker检查配置信息方法*/
    public void checkConfigInfo() {
        /** 1.获取CacheData对象,key是dataId*/
        int listenerSize = cacheMap.size();
        int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
        if (longingTaskCount > currentLongingTaskCount) {
            for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
                /** 2.线程池执行LongPollingRunnable任务*/
                executorService.execute(new LongPollingRunnable(i));
            }
            currentLongingTaskCount = longingTaskCount;
        }
    }

 

checkConfigInfo方法实际就是向定时任务线程池中提交一个长轮训任务LongPollingRunnable,该任务执行逻辑如下:

/** LongPollingRunnable线程执行逻辑 */
    public void run() {
        List<CacheData> cacheDatas = new ArrayList<CacheData>();
        List<String> inInitializingCacheList = new ArrayList<String>();
        try {
            //遍历所有CacheData
            for (CacheData cacheData : cacheMap.values()) {
                if (cacheData.getTaskId() == taskId) {
                    cacheDatas.add(cacheData);
                    try {
                        /** 检查CacheData的本地配置*/
                        checkLocalConfig(cacheData);
                        if (cacheData.isUseLocalConfigInfo()) {
                            cacheData.checkListenerMd5();
                        }
                    } catch (Exception e) {
                        LOGGER.error("get local config info error", e);
                    }
                }
            }

            // 校验服务器配置,检查需要更新的DataId
            List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
            if (!CollectionUtils.isEmpty(changedGroupKeys)) {
                LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
            }

            /** 遍历所有更新的配置分组key*/
            for (String groupKey : changedGroupKeys) {
                String[] key = GroupKey.parseKey(groupKey);
                String dataId = key[0];
                String group = key[1];
                String tenant = null;
                if (key.length == 3) {
                    tenant = key[2];
                }
                try {
                    /** 获取服务器配置 */
                    String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                    CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
                    /** 更新服务器配置*/
                    cache.setContent(ct[0]);
                    if (null != ct[1]) {
                        cache.setType(ct[1]);
                    }
                    LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}", agent.getName(), dataId, group, tenant, cache.getMd5(), ContentUtils.truncateContent(ct[0]), ct[1]);
                } catch (NacosException ioe) {
                    String message = String.format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                                    agent.getName(), dataId, group, tenant);
                    LOGGER.error(message, ioe);
                }
            }
            for (CacheData cacheData : cacheDatas) {
                if (!cacheData.isInitializing() || inInitializingCacheList
                        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                    /** 校验配置的MD5*/
                    cacheData.checkListenerMd5();
                    cacheData.setInitializing(false);
                }
            }
            inInitializingCacheList.clear();
            executorService.execute(this);

        } catch (Throwable e) {
            LOGGER.error("longPolling error : ", e);
            executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
        }
    }
}

 

首先是检查本地配置,所以及时服务器崩溃了,nacos客户端也可以保证可以使用本地配置,本地配置存储在~nacos/config/目录下,检查完本地配置之后,再查询服务器配置,然后和本地配置进行比较的到需要更新的配置,将最新的配置写入本地。

最后执行CacheData的checkListenerMd5()方法,该方法作用是比较配置文件的MD5加密数据是否一致,如果不一致则表示更新过,那么就需要触发监听器的回调,源码如下:

 1 /** CacheData类*/
 2     void checkListenerMd5() {
 3         for (ManagerListenerWrap wrap : listeners) {
 4             //比较MD5加密数据是否一致
 5             if (!md5.equals(wrap.lastCallMd5)) {
 6                 /** 回调Listener*/
 7                 safeNotifyListener(dataId, group, content, type, md5, wrap);
 8             }
 9         }
10     }
11 
12     private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
13                                     final String md5, final ManagerListenerWrap listenerWrap) {
14         final Listener listener = listenerWrap.listener;
15 
16         Runnable job = new Runnable() {
17             @Override
18             public void run() {
19                 ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
20                 ClassLoader appClassLoader = listener.getClass().getClassLoader();
21                 try {
22                     if (listener instanceof AbstractSharedListener) {
23                         AbstractSharedListener adapter = (AbstractSharedListener) listener;
24                         adapter.fillContext(dataId, group);
25                         LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
26                     }
27                     // 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
28                     Thread.currentThread().setContextClassLoader(appClassLoader);
29 
30                     ConfigResponse cr = new ConfigResponse();
31                     cr.setDataId(dataId);
32                     cr.setGroup(group);
33                     cr.setContent(content);
34                     configFilterChainManager.doFilter(null, cr);
35                     String contentTmp = cr.getContent();
36                     /** 回调执行Listener的receiveConfigInfo方法 */
37                     listener.receiveConfigInfo(contentTmp);
38 
39                     // compare lastContent and content
40                     if (listener instanceof AbstractConfigChangeListener) {
41                         Map data = ConfigChangeHandler.getInstance()
42                                 .parseChangeData(listenerWrap.lastContent, content, type);
43                         ConfigChangeEvent event = new ConfigChangeEvent(data);
44                         ((AbstractConfigChangeListener) listener).receiveConfigChange(event);
45                         listenerWrap.lastContent = content;
46                     }
47 
48                     listenerWrap.lastCallMd5 = md5;
49                     LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
50                             listener);
51                 } catch (NacosException ex) {
52                     LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}",
53                             name, dataId, group, md5, listener, ex.getErrCode(), ex.getErrMsg());
54                 } catch (Throwable t) {
55                     LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId,
56                             group, md5, listener, t.getCause());
57                 } finally {
58                     Thread.currentThread().setContextClassLoader(myClassLoader);
59                 }
60             }
61         };
62 
63         final long startNotify = System.currentTimeMillis();
64         try {
65             if (null != listener.getExecutor()) {
66                 listener.getExecutor().execute(job);
67             } else {
68                 job.run();
69             }
70         } catch (Throwable t) {
71             LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} throwable={}", name, dataId,
72                     group, md5, listener, t.getCause());
73         }
74         final long finishNotify = System.currentTimeMillis();
75         LOGGER.info("[{}] [notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
76                 name, (finishNotify - startNotify), dataId, group, md5, listener);
77     }

 

当比较更新完的配置和之前的配置不一样时,就会触发监听器Listener的回调,执行Listener的receiveConfigInfo方法

 

总结:

Nacos配置中心采用的是客户端pull的方式从nacos服务器获取配置数据,并且没有和nacos服务器保持长连接,而是以定时任务执行HTTP请求的方式从Nacos服务器获取最新配置,然后再刷新到本地存储,最后再触发监听器Listener的回调方法。

所以Nacos客户端的监听器的通知并不是nacos服务器主动推送过来的,而是nacos客户端本地轮训查询发现了配置变更之后才触发的回调。另外nacos客户端本地采用了线程池方式拉取配置,所以不会影响核心业务线程。

 

3.2、服务管理实现原理

nacos提供了大量关于服务发布和订阅的API,作为Nacos客户端,无论是服务提供者还是服务消费者,只需要在启动时调用nacos的API即可完成服务发布和服务订阅功能。但是作为注册中心,还需要有服务实例健康检查功能,服务消费者实时监听服务提供者变化的

通知功能。而服务订阅的监听逻辑和nacos配置的变更监听流程基本上相同,订阅功能主要由subscribe方法实现,NamingService实现类是NacosNamingService,初始化时会执行init方法,初始化服务器代理serverProxy,心跳处理器beatReactor,host处理器

hostReactor等对象,服务订阅方法subscribe方法逻辑如下:

    private HostReactor hostReactor;

    private BeatReactor beatReactor;

    private NamingProxy serverProxy;

    /** NacosNamingService初始化方法 */
    private void init(Properties properties) throws NacosException {
        ValidatorUtils.checkInitParam(properties);
        this.namespace = InitUtils.initNamespaceForNaming(properties);
        InitUtils.initSerialization();
        initServerAddr(properties);
        InitUtils.initWebRootContext(properties);
        initCacheDir();
        initLogName(properties);

        this.serverProxy = new NamingProxy(this.namespace, this.endpoint, this.serverList, properties);
        this.beatReactor = new BeatReactor(this.serverProxy, initClientBeatThreadCount(properties));
        this.hostReactor = new HostReactor(this.serverProxy, beatReactor, this.cacheDir, isLoadCacheAtStart(properties),
                isPushEmptyProtect(properties), initPollingThreadCount(properties));
    }

    /** NacosNamingService服务订阅方法 */
    public void subscribe(String serviceName, String groupName, List<String> clusters, EventListener listener)
            throws NacosException {
        hostReactor.subscribe(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, ","),
                listener);
    }

    /** HostReactor的服务订阅方法,并开启监听器*/
    public void subscribe(String serviceName, String clusters, EventListener eventListener) {
        /** 1.注册监听器,存入InstanceChangeNotifier对象的Map中,key是服务名称和集群,value是监听器集合 */
        notifier.registerListener(serviceName, clusters, eventListener);
        /** 2.根据服务名称获取服务器信息 */
        getServiceInfo(serviceName, clusters);
    }

 方法执行到HostReactor对象的subscribe方法,首先是将监听器存入InstanceChangeNotifier对象的Map中,根据服务名称和集群名称作为key存储,value是监听器的集合,存储起来之后调用getServiceInfo方法从nacos服务器获取服务实例信息,逻辑如下:

 1 /** HostReactor类 获取服务实例信息方法 */
 2     public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
 3         String key = ServiceInfo.getKey(serviceName, clusters);
 4         if (failoverReactor.isFailoverSwitch()) {
 5             return failoverReactor.getService(key);
 6         }
 7         /** 从本地缓存中获取ServiceInfo对象 */
 8         ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
 9 
10         if (null == serviceObj) {// 如果本地缓存中没有服务实例
11             serviceObj = new ServiceInfo(serviceName, clusters);
12             serviceInfoMap.put(serviceObj.getKey(), serviceObj);
13             updatingMap.put(serviceName, new Object());
14             /** 立即更新服务实例*/
15             updateServiceNow(serviceName, clusters);
16             updatingMap.remove(serviceName);
17 
18         } else if (updatingMap.containsKey(serviceName)) {//判断当前服务实例是否正在更新
19             if (UPDATE_HOLD_INTERVAL > 0) {
20                 synchronized (serviceObj) {
21                     try {
22                         serviceObj.wait(UPDATE_HOLD_INTERVAL);
23                     } catch (InterruptedException e) {
24                         NAMING_LOGGER
25                                 .error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
26                     }
27                 }
28             }
29         }
30 
31         /** 定时更新服务实例信息 */
32         scheduleUpdateIfAbsent(serviceName, clusters);
33         return serviceInfoMap.get(serviceObj.getKey());
34     }

 

 核心逻辑是先从本地获取服务实例信息,如果不存在那么立即执行updateServiceNow方法进行更新;如果已经存在那么先执行scheuleUpdateIfAbsent方法定时更新。updateServiceNow方法也就是当前线程立即更新服务实例,执行了updateService方法,

而定时更新逻辑是先构建一个UpdateTask,然后提交给线程池来执行,定时每1秒执行一次,逻辑如下:

/** HostReactor类 */
    public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
        if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
            return;
        }

        synchronized (futureMap) {
            if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
                return;
            }

            /** 创建UpdateTask,并添加定时任务 */
            ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters));
            futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
        }
    }

    /** HostReactor类添加任务*/
    public synchronized ScheduledFuture<?> addTask(UpdateTask task) {
        /** 线程池执行,每1秒执行一次*/
        return executor.schedule(task, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
    }

 

所以更新的逻辑主要在UpdateTask执行体类,且逻辑肯定包含了updateService方法的逻辑,源码核心逻辑如下:

/** HostReactor 更新服务实例方法 */
    public void updateService(String serviceName, String clusters) throws NacosException {
        /** 1.从本地获取旧的服务实例 */
        ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
        try {
            /** 2.从服务器查询最新服务实例列表 */
            String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false);

            if (StringUtils.isNotEmpty(result)) {
                /** 3.刷新本地缓存 */
                processServiceJson(result);
            }
        } finally {
            if (oldService != null) {
                synchronized (oldService) {
                    oldService.notifyAll();
                }
            }
        }
    }

    /** UpdateTask 执行体*/
    public void run() {
        long delayTime = DEFAULT_DELAY;
        try {
            /** 1.从缓存中获取服务实例*/
            ServiceInfo serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
            if (serviceObj == null) {
                /** 2.如果缓存中没有,则执行updateService方法查询*/
                updateService(serviceName, clusters);
                return;
            }
            /** 2.如果本地服务实例更新时间延迟,那么就执行updateService方法刷新*/
            if (serviceObj.getLastRefTime() <= lastRefTime) {
                updateService(serviceName, clusters);
                serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
            } else {
                refreshOnly(serviceName, clusters);
            }

            lastRefTime = serviceObj.getLastRefTime();

            if (!notifier.isSubscribed(serviceName, clusters) && !futureMap
                    .containsKey(ServiceInfo.getKey(serviceName, clusters))) {
                // abort the update task
                NAMING_LOGGER.info("update task is stopped, service:" + serviceName + ", clusters:" + clusters);
                return;
            }
            if (CollectionUtils.isEmpty(serviceObj.getHosts())) {
                /** 3.如果查询失败,那么失败次数自增*/
                incFailCount();
                return;
            }
            delayTime = serviceObj.getCacheMillis();
            /** 4.如果查询成功,那么重置失败次数*/
            resetFailCount();
        } catch (Throwable e) {
            incFailCount();
            NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName, e);
        } finally {
            /** 5.提交下一次延迟任务*/
            executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60), TimeUnit.MILLISECONDS);
        }
    }

 

 可以发现更新逻辑就是执行updateService方法,首先从服务器查询最新的服务实例列表,然后将查询结果刷新到本地缓存中,然后开启下一次定时任务继续执行。默认是1秒钟执行一次,如果查询不到任何记录(服务器异常或无可用实例),那么就增加失败次数,每

增加一次失败次数延迟执行时间就翻倍,最长会1分钟执行一次。

另外当执行updateService方法刷新服务实例时,如果触发了服务更新,就需要更新本地缓存并且写入磁盘的持久化文件中保持,并且还会调用NotifyCenter的publishEvent方法发布服务实例变更事件,逻辑如下:

/** HostReactor 处理查询服务实例结果方法*/
    public ServiceInfo processServiceJson(String json) {
        //......
        boolean changed = false;
        if (oldService != null) {
            //......
        } else {
            changed = true;
            /** 刷新内存中缓存*/
            serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
            /** 发布服务实例变更事件*/
            NotifyCenter.publishEvent(new InstancesChangeEvent(serviceInfo.getName(), serviceInfo.getGroupName(), serviceInfo.getClusters(), serviceInfo.getHosts()));
            serviceInfo.setJsonFromServer(json);
            /** 写入磁盘本地数据*/
            DiskCache.write(serviceInfo, cacheDir);
        }
        //......
        return serviceInfo;
    }

    /** NotifyCenter 发布事件方法*/
    public static boolean publishEvent(Event event) {
        try {
            return publishEvent(event.getClass(), event);
        } catch (Throwable var2) {
            LOGGER.error("There was an exception to the message publishing : {}", var2);
            return false;
        }
    }
    private static boolean publishEvent(final Class<? extends Event> eventType, final Event event) {
        if (ClassUtils.isAssignableFrom(SlowEvent.class, eventType)) {
            return INSTANCE.sharePublisher.publish(event);
        }

        final String topic = ClassUtils.getCanonicalName(eventType);
        EventPublisher publisher = INSTANCE.publisherMap.get(topic);
        if (publisher != null) {
            /** 执行EventPublisher对象publish方法*/
            return publisher.publish(event);
        }
        LOGGER.warn("There are no [{}] publishers for this event, please register", topic);
        return false;
    }

 

实际是调用了EventPublisher对象的publish方法,默认实现是DefaultPublisher类,DefaultPublisher会先将通知事件存入本地队列,然后采用线程异步通知,逻辑如下:

 1  /** DefaultPublisher类 发布事件方法*/
 2     public boolean publish(Event event) {
 3         /** 1.检查并开启线程 */
 4         checkIsStart();
 5         /** 2.将事件存入队列*/
 6         boolean success = this.queue.offer(event);
 7         if (!success) {
 8             LOGGER.warn("Unable to plug in due to interruption, synchronize sending time, event : {}", event);
 9             /** 3.如果存入队列失败,那么立即通知*/
10             receiveEvent(event);
11             return true;
12         }
13         return true;
14     }
15 
16     public void run() {
17         openEventHandler();
18     }
19 
20     void openEventHandler() {
21         try {
22 
23             // This variable is defined to resolve the problem which message overstock in the queue.
24             int waitTimes = 60;
25             // To ensure that messages are not lost, enable EventHandler when
26             // waiting for the first Subscriber to register
27             for (; ; ) {
28                 if (shutdown || hasSubscriber() || waitTimes <= 0) {
29                     break;
30                 }
31                 ThreadUtils.sleep(1000L);
32                 waitTimes--;
33             }
34 
35             for (; ; ) {
36                 if (shutdown) {
37                     break;
38                 }
39                 final Event event = queue.take();
40                 receiveEvent(event);
41                 UPDATER.compareAndSet(this, lastEventSequence, Math.max(lastEventSequence, event.sequence()));
42             }
43         } catch (Throwable ex) {
44             LOGGER.error("Event listener exception : {}", ex);
45         }
46     }
47 
48     void receiveEvent(Event event) {
49         final long currentEventSequence = event.sequence();
50         /** 遍历所有订阅者,*/
51         for (Subscriber subscriber : subscribers) {
52             // Whether to ignore expiration events
53             if (subscriber.ignoreExpireEvent() && lastEventSequence > currentEventSequence) {
54                 LOGGER.debug("[NotifyCenter] the {} is unacceptable to this subscriber, because had expire",
55                         event.getClass());
56                 continue;
57             }
58             /** 通知订阅者,执行订阅者的onEvent方法 */
59             notifySubscriber(subscriber, event);
60         }
61     }

 

DefaultPublisher先将事件存入队列,然后通过异步线程从队列中取任务,遍历事件所有订阅者,依次遍历执行订阅者的onEvent方法实现事件回调通知。

 

总结:

服务管理的实现和配置管理实现原理基本一致,启动时首先会调用Nacos服务器的HTTP接口初始化一次,并且在本地内存中缓存一份,磁盘中持久化一份。然后开启定时任务轮训查询服务器最新数据,如果数据发生变化,那么就更新内存中缓存,重新写入磁盘,

然后再由线程池异步遍历所有订阅者,回调执行订阅者的回调函数实现变更通知的逻辑。

 

3.3、心跳检测

作为服务提供者,需要和nacos服务器保持心跳,服务提供者在注册实例时会创建心跳任务,逻辑如下:

 1 /** 服务提供者 注册实例*/
 2     public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
 3         NamingUtils.checkInstanceIsLegal(instance);
 4         String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
 5         /** 如果实例是临时节点*/
 6         if (instance.isEphemeral()) {
 7             /** 构建心跳任务交给BeatReactor处理 */
 8             BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);
 9             beatReactor.addBeatInfo(groupedServiceName, beatInfo);
10         }
11         serverProxy.registerService(groupedServiceName, groupName, instance);
12     }

 调用BeatReactor的addBeatInfo方法提交心跳任务

public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        //fix #1733
        if ((existBeat = dom2Beat.remove(key)) != null) {
            existBeat.setStopped(true);
        }
        dom2Beat.put(key, beatInfo);
        /** 创建并提交心跳定时任务,默认是5秒执行一次*/
        executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
    }

    /** 心跳定时任务执行体 */
    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 {
                /** 发送心跳给Nacos服务器
                 *  调用Nacos服务器的 /instance/beat 接口 */
                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 {
                        /** 如果返回404,那么就重新注册实例*/
                        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);
        }
    }

 

 核心逻辑就是构建心跳定时任务交给NacosNamingService的线程池,默认每5秒发送一次心跳,实际就是调用nacos服务器的 /instance/beat接口发送心跳,心跳发送完成再开启下一次的定时任务,整体逻辑比较简单。

 

总结:

虽然nacos实现了配置中心和服务发现、服务订阅、健康检测等功能,但是nacos客户端实际上并没有和nacos服务器保持长连接,而是采用HTTP请求的方式来实现。

配置中心就是调用查询配置HTTP接口查询并缓存在本地,然后开启定时任务轮训查询,如果发送变更就刷新本地缓存,并触发回调通知监听器;

服务发布就是调用注册服务HTTP接口实现注册,然后开启定时任务每5秒向nacos调用一次HTTP接口发送心跳数据,nacos根据心跳来管理服务提供者的健康状态;

服务订阅就是调用查询服务HTTP接口实现服务订阅并将服务实例信息缓存在本地,然后开启定时任务轮训查询并和本地数据进行比较,如果有更新那么就异步触发回调通知所有服务订阅者;

posted @ 2021-12-23 09:32  Lucky帅小武  阅读(1314)  评论(0编辑  收藏  举报