• 分布式中间件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接口实现服务订阅并将服务实例信息缓存在本地,然后开启定时任务轮训查询并和本地数据进行比较,如果有更新那么就异步触发回调通知所有服务订阅者;

  • 相关阅读:
    http方法-get和post
    SVG
    canvas
    h5拖放
    jQuery表单事件
    html标签嵌套
    jquery.eq()
    jquery.index()
    Date对象
    json
  • 原文地址:https://www.cnblogs.com/jackion5/p/15715996.html
Copyright © 2020-2023  润新知