Nacos源码—5.Nacos配置中心实现分析二

大纲

1.关于Nacos配置中心的几个问题

2.Nacos如何整合SpringBoot读取远程配置

3.Nacos加载读取远程配置数据的源码分析

4.客户端如何感知远程配置数据的变更

5.集群架构下节点间如何同步配置数据

4.客户端如何感知远程配置数据的变更

(1)ConfigService对象使用介绍

(2)客户端注册监听器的源码

(3)回调监听器的方法的源码

(1)ConfigService对象使用介绍

ConfigService是一个接口,定义了获取配置、发布配置、移除配置等方法。ConfigService只有一个实现类NacosConfigService,Nacos配置中心源码的核心其实就是这个NacosConfigService对象。

步骤一:手动创建ConfigService对象

首先定义好基本的Nacos信息,然后利用NacosFactory工厂类来创建ConfigService对象。

复制代码
public class Demo {
    public static void main(String[] args) throws Exception {
        //步骤一:配置信息
        String serverAddr = "124.223.102.236:8848";
        String dataId = "stock-service-test.yaml";
        String group = "DEFAULT_GROUP";

        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);

        //步骤一:获取配置中心服务
        ConfigService configService = NacosFactory.createConfigService(properties);
    }
}

步骤二:获取配置、发布配置

创建好ConfigService对象后,就可以使用ConfigService对象的getConfig()方法来获取配置信息,还可以使用ConfigService对象的publishConfig()方法来发布配置信息。

如下Demo先获取一次配置数据,然后发布新配置,紧接着重新获取数据。发现第二次获取的配置数据已发生变化,从而也说明发布配置成功了。

复制代码
public class Demo {
    public static void main(String[] args) throws Exception {
        //步骤一:配置信息
        String serverAddr = "124.223.102.236:8848";
        String dataId = "stock-service-test.yaml";
        String group = "DEFAULT_GROUP";

        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);

        //步骤一:获取配置中心服务
        ConfigService configService = NacosFactory.createConfigService(properties);
     
        //步骤二:从配置中心获取配置
        String content = configService.getConfig(dataId, group, 5000);
        System.out.println("发布配置前" + content);
      
        //步骤二:发布配置
        configService.publishConfig(dataId, group, "userName: userName被修改了", ConfigType.PROPERTIES.getType());

        Thread.sleep(300L);
        //步骤二:从配置中心获取配置
        content = configService.getConfig(dataId, group, 5000);
        System.out.println("发布配置后" + content);
    }
}

步骤三:添加监听器

可以使用ConfigService对象的addListener()方法来添加监听器。通过dataId + group这两个参数,就可以注册一个监听器。当dataId + group对应的配置在服务端发生改变时,客户端的监听器就可以马上感知并对配置数据进行刷新。

复制代码
public class Demo {
    public static void main(String[] args) throws Exception {
        //步骤一:配置信息
        String serverAddr = "124.223.102.236:8848";
        String dataId = "stock-service-test.yaml";
        String group = "DEFAULT_GROUP";

        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);

        //步骤一:获取配置中心服务
        ConfigService configService = NacosFactory.createConfigService(properties);
     
        //步骤二:从配置中心获取配置
        String content = configService.getConfig(dataId, group, 5000);
        System.out.println("发布配置前" + content);
      
        //步骤二:发布配置
        configService.publishConfig(dataId, group, "userName: userName被修改了", ConfigType.PROPERTIES.getType());

        Thread.sleep(300L);
        //步骤二:从配置中心获取配置
        content = configService.getConfig(dataId, group, 5000);
        System.out.println("发布配置后" + content);
     
        //步骤三:注册监听器
        configService.addListener(dataId, group, new Listener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                System.out.println("感知配置变化:" + configInfo);
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        });

        //阻断进程关闭
        Thread.sleep(Integer.MAX_VALUE);
    }
}

(2)客户端注册监听器的源码

Nacos客户端是什么时候为dataId + group注册监听器的?

在nacos-config下的spring.factories文件中,有一个自动装配的配置类NacosConfigAutoConfiguration,在该配置类中定义了一个NacosContextRefresher对象,而NacosContextRefresher对象会监听ApplicationReadyEvent事件。

在NacosContextRefresher的onApplicationEvent()方法中,会执行registerNacosListenersForApplications()方法,这个方法中会遍历每一个dataId + group注册Nacos监听器。

对于每一个dataId + group,则通过调用registerNacosListener()方法来进行Nacos监听器的注册,也就是最终调用ConfigService对象的addListener()方法来注册监听器。

复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigAutoConfiguration {
    ...
    @Bean
    public NacosContextRefresher nacosContextRefresher(NacosConfigManager nacosConfigManager, NacosRefreshHistory nacosRefreshHistory) {
        return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
    }
    ...
}

public class NacosContextRefresher implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
    private final ConfigService configService;
    ...
    
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        //many Spring context
        if (this.ready.compareAndSet(false, true)) {
            this.registerNacosListenersForApplications();
        }
    }
    
    //register Nacos Listeners.
    private void registerNacosListenersForApplications() {
        if (isRefreshEnabled()) {
            //获取全部的配置
            for (NacosPropertySource propertySource : NacosPropertySourceRepository.getAll()) {
                //判断当前配置是否需要刷新
                if (!propertySource.isRefreshable()) {
                    continue;
                }
                String dataId = propertySource.getDataId();
                //注册监听器
                registerNacosListener(propertySource.getGroup(), dataId);
            }
        }
    }
    
    private void registerNacosListener(final String groupKey, final String dataKey) {
        String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
        Listener listener = listenerMap.computeIfAbsent(key, lst -> new AbstractSharedListener() {
            @Override
            public void innerReceive(String dataId, String group, String configInfo) {
                //监听器的回调方法处理逻辑
                refreshCountIncrement();
                //记录刷新历史
                nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
                //发布RefreshEvent刷新事件
                applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));
                if (log.isDebugEnabled()) {
                    log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo));
                }
            }
        });
        try {
            //注册监听器
            configService.addListener(dataKey, groupKey, listener);
        } catch (NacosException e) {
            log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), e);
        }
    }
    ...
}

(3)回调监听器的方法的源码

给每一个dataId + group注册Nacos监听器后,当Nacos服务端的配置文件发生变更时,就会回调监听器的方法,也就是会触发调用AbstractSharedListener的innerReceive()方法。然后调用applicationContext.publishEvent()发布RefreshEvent刷新事件,而发布的RefreshEvent刷新事件会被RefreshEventListener类来处理。

RefreshEventListener类不是Nacos中的类了,而是SpringCloud的类。它在处理刷新事件时,会销毁被@RefreshScope注解修饰的类的Bean,也就是会调用添加了@RefreshScope注解的类的destroy()方法。把Bean实例销毁后,后面需要用到这个Bean时才重新进行创建。重新进行创建的时候,就会获取最新的配置文件,从而完成刷新效果。

(4)总结

客户端注册Nacos监听器,服务端修改配置后,客户端刷新配置的流程:

5.集群架构下节点间如何同步配置数据

(1)Nacos控制台的配置管理模块

(2)变更配置数据时的源码

(3)集群节点间的配置数据变更同步

(4)服务端通知客户端配置数据已变更

(5)总结

(1)Nacos控制台的配置管理模块

在这个模块中,可以通过配置列表维护我们的配置文件,可以通过历史版本找到配置的发布记录,并且支持回滚操作。当编辑配置文件时,客户端可以及时感知变化并刷新其配置文件。当服务端通知客户端配置变更时,也会通知集群节点进行数据同步。

当用户在Nacos控制台点击确认发布按钮时,Nacos会大概进行如下处理:

一.修改配置文件数据

二.保存配置发布历史

三.通知并触发客户端监听事件进行配置文件变更

四.通知集群对配置文件进行变更

点击确认发布按钮时,会发起HTTP请求,地址为"/nacos/v1/cs/configs"。通过请求地址可知处理入口是ConfigController的publishConfig()方法。

(2)变更配置数据时的源码

ConfigController的publishConfig()方法中的两行核心代码是:一.新增或修改配置数据的PersistService的insertOrUpdate()方法,二.发布配置变更事件的ConfigChangePublisher的notifyConfigChange()方法。

一.新增或者修改配置数据

其中PersistService有两个实现类:一是EmbeddedStoragePersistServiceImpl,它是Nacos内置的Derby数据库。二是ExternalStoragePersistServiceImpl,它是Nacos外置数据库如MySQL。

在ExternalStoragePersistServiceImpl的insertOrUpdate()方法中,如果执行ExternalStoragePersistServiceImpl的updateConfigInfo()方法,那么会先查询对应的配置,然后更新配置,最后保存配置历史。

复制代码
@RestController
@RequestMapping(Constants.CONFIG_CONTROLLER_PATH)
public class ConfigController {
    private final PersistService persistService;
    ...
    
    @PostMapping
    @Secured(action = ActionTypes.WRITE, parser = ConfigResourceParser.class)
    public Boolean publishConfig(HttpServletRequest request, HttpServletResponse response,
            @RequestParam(value = "dataId") String dataId, @RequestParam(value = "group") String group,
            @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
            @RequestParam(value = "content") String content, @RequestParam(value = "tag", required = false) String tag,
            @RequestParam(value = "appName", required = false) String appName,
            @RequestParam(value = "src_user", required = false) String srcUser,
            @RequestParam(value = "config_tags", required = false) String configTags,
            @RequestParam(value = "desc", required = false) String desc,
            @RequestParam(value = "use", required = false) String use,
            @RequestParam(value = "effect", required = false) String effect,
            @RequestParam(value = "type", required = false) String type,
            @RequestParam(value = "schema", required = false) String schema) throws NacosException {
    
        final String srcIp = RequestUtil.getRemoteIp(request);
        final String requestIpApp = RequestUtil.getAppName(request);
        srcUser = RequestUtil.getSrcUserName(request);
        //check type
        if (!ConfigType.isValidType(type)) {
            type = ConfigType.getDefaultType().getType();
        }
        //check tenant
        ParamUtils.checkTenant(tenant);
        ParamUtils.checkParam(dataId, group, "datumId", content);
        ParamUtils.checkParam(tag);
        Map<String, Object> configAdvanceInfo = new HashMap<String, Object>(10);
        MapUtils.putIfValNoNull(configAdvanceInfo, "config_tags", configTags);
        MapUtils.putIfValNoNull(configAdvanceInfo, "desc", desc);
        MapUtils.putIfValNoNull(configAdvanceInfo, "use", use);
        MapUtils.putIfValNoNull(configAdvanceInfo, "effect", effect);
        MapUtils.putIfValNoNull(configAdvanceInfo, "type", type);
        MapUtils.putIfValNoNull(configAdvanceInfo, "schema", schema);
        ParamUtils.checkParam(configAdvanceInfo);
    
        if (AggrWhitelist.isAggrDataId(dataId)) {
            LOGGER.warn("[aggr-conflict] {} attemp to publish single data, {}, {}", RequestUtil.getRemoteIp(request), dataId, group);
            throw new NacosException(NacosException.NO_RIGHT, "dataId:" + dataId + " is aggr");
        }
    
        final Timestamp time = TimeUtils.getCurrentTime();
        String betaIps = request.getHeader("betaIps");
        ConfigInfo configInfo = new ConfigInfo(dataId, group, tenant, appName, content);
        configInfo.setType(type);
        if (StringUtils.isBlank(betaIps)) {
            if (StringUtils.isBlank(tag)) {
                //新增配置或者修改配置
                persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, true);
                //发布配置改变事件
                ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));
            } else {
                persistService.insertOrUpdateTag(configInfo, tag, srcIp, srcUser, time, true);
                //发布配置改变事件
                ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(false, dataId, group, tenant, tag, time.getTime()));
            }
        } else {
            //beta publish
            persistService.insertOrUpdateBeta(configInfo, betaIps, srcIp, srcUser, time, true);
            //发布配置改变事件
            ConfigChangePublisher.notifyConfigChange(new ConfigDataChangeEvent(true, dataId, group, tenant, time.getTime()));
        }
        ConfigTraceService.logPersistenceEvent(dataId, group, tenant, requestIpApp, time.getTime(), InetUtils.getSelfIP(), ConfigTraceService.PERSISTENCE_EVENT_PUB, content);
        return true;
    }
    ...
}

//External Storage Persist Service.
@SuppressWarnings(value = {"PMD.MethodReturnWrapperTypeRule", "checkstyle:linelength"})
@Conditional(value = ConditionOnExternalStorage.class)
@Component
public class ExternalStoragePersistServiceImpl implements PersistService {
    private DataSourceService dataSourceService;
    ...
    
    @Override
    public void insertOrUpdate(String srcIp, String srcUser, ConfigInfo configInfo, Timestamp time, Map<String, Object> configAdvanceInfo, boolean notify) {
        try {
            addConfigInfo(srcIp, srcUser, configInfo, time, configAdvanceInfo, notify);
        } catch (DataIntegrityViolationException ive) { // Unique constraint conflict
            updateConfigInfo(configInfo, srcIp, srcUser, time, configAdvanceInfo, notify);
        }
    }
    
    @Override
    public void updateConfigInfo(final ConfigInfo configInfo, final String srcIp, final String srcUser, final Timestamp time, final Map<String, Object> configAdvanceInfo, final boolean notify) {
        boolean result = tjt.execute(status -> {
            try {
                //查询已存在的配置数据
                ConfigInfo oldConfigInfo = findConfigInfo(configInfo.getDataId(), configInfo.getGroup(), configInfo.getTenant());
                String appNameTmp = oldConfigInfo.getAppName();
                if (configInfo.getAppName() == null) {
                    configInfo.setAppName(appNameTmp);
                }
                //更新配置数据
                updateConfigInfoAtomic(configInfo, srcIp, srcUser, time, configAdvanceInfo);
                String configTags = configAdvanceInfo == null ? null : (String) configAdvanceInfo.get("config_tags");
                if (configTags != null) {
                    // delete all tags and then recreate
                    removeTagByIdAtomic(oldConfigInfo.getId());
                    addConfigTagsRelation(oldConfigInfo.getId(), configTags, configInfo.getDataId(), configInfo.getGroup(), configInfo.getTenant());
                }
                //保存到发布配置历史表
                insertConfigHistoryAtomic(oldConfigInfo.getId(), oldConfigInfo, srcIp, srcUser, time, "U");
            } catch (CannotGetJdbcConnectionException e) {
                LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e);
                throw e;
            }
            return Boolean.TRUE;
        });
    }
    
    @Override
    public ConfigInfo findConfigInfo(final String dataId, final String group, final String tenant) {
        final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant;
        try {
            return this.jt.queryForObject("SELECT ID,data_id,group_id,tenant_id,app_name,content,md5,type FROM config_info WHERE data_id=? AND group_id=? AND tenant_id=?", new Object[] {dataId, group, tenantTmp}, CONFIG_INFO_ROW_MAPPER);
        } catch (EmptyResultDataAccessException e) { // Indicates that the data does not exist, returns null.
            return null;
        } catch (CannotGetJdbcConnectionException e) {
            LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e);
            throw e;
        }
    }
    ...
}

二.发布配置变更事件

执行ConfigChangePublisher的notifyConfigChange()方法发布配置变更事件时,最终会把事件添加到DefaultPublisher.queue阻塞队列中,完成事件发布。

NotifyCenter在其静态方法中,会创建DefaultPublisher并进行初始化。在执行DefaultPublisher的init()方法时,就会开启一个异步任务。该异步任务便会不断从阻塞队列DefaultPublisher.queue中获取事件,然后调用DefaultPublisher的receiveEvent()方法处理配置变更事件。

在DefaultPublisher的receiveEvent()方法中,会循环遍历事件订阅者。其中就会包括来自客户端,以及来自集群节点的两个订阅者。前者会通知客户端发生了配置变更事件,后者会通知各集群节点发生了配置变更事件。而且进行事件通知时,都会调用DefaultPublisher的notifySubscriber()方法。该方法会异步执行订阅者的监听逻辑,也就是subscriber.onEvent()方法。

具体的subscriber订阅者有:用来通知集群节点进行数据同步的订阅者AsyncNotifyService,用来通知客户端处理配置文件变更的订阅者LongPollingService。

**事件发布机制的实现简单总结:**发布者需要一个Set存放注册的订阅者,发布者发布事件时,需要遍历调用订阅者处理事件的方法。

复制代码
public class ConfigChangePublisher {
    //Notify ConfigChange.
    public static void notifyConfigChange(ConfigDataChangeEvent event) {
        if (PropertyUtil.isEmbeddedStorage() && !EnvUtil.getStandaloneMode()) {
            return;
        }
        NotifyCenter.publishEvent(event);
    }
}

//Unified Event Notify Center.
public class NotifyCenter {
    static {
        ...
        try {
            // Create and init DefaultSharePublisher instance.
            INSTANCE.sharePublisher = new DefaultSharePublisher();
            INSTANCE.sharePublisher.init(SlowEvent.class, shareBufferSize);        
        } catch (Throwable ex) {
            LOGGER.error("Service class newInstance has error : {}", ex);
        }
        ThreadUtils.addShutdownHook(new Runnable() {
            @Override
            public void run() {
                shutdown();
            }
        });
    }
    
    //注册订阅者
    public static <T> void registerSubscriber(final Subscriber consumer) {
        ...
        addSubscriber(consumer, subscribeType);
    }
    
    private static void addSubscriber(final Subscriber consumer, Class<? extends Event> subscribeType) {
        ...
        EventPublisher publisher = INSTANCE.publisherMap.get(topic);
        //执行DefaultPublisher.addSubscriber()方法
        publisher.addSubscriber(consumer);
    }
    ...
    //Request publisher publish event Publishers load lazily, calling publisher. Start () only when the event is actually published.
    public static boolean publishEvent(final Event event) {
        try {
            return publishEvent(event.getClass(), event);
        } catch (Throwable ex) {
            LOGGER.error("There was an exception to the message publishing : {}", ex);
            return false;
        }
    }
    
    //Request publisher publish event Publishers load lazily, calling publisher.
    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) {
            //执行DefaultPublisher.publish()方法
            return publisher.publish(event);
        }
        LOGGER.warn("There are no [{}] publishers for this event, please register", topic);
        return false;
    }
    ...
}

//The default event publisher implementation.
public class DefaultPublisher extends Thread implements EventPublisher {
    protected final ConcurrentHashSet<Subscriber> subscribers = new ConcurrentHashSet<Subscriber>();
    private BlockingQueue<Event> queue;
    ...
    
    @Override
    public void addSubscriber(Subscriber subscriber) {
        //注册事件订阅者
        subscribers.add(subscriber);
    }
    
    @Override
    public boolean publish(Event event) {
        checkIsStart();
        //将事件添加到阻塞队列,则表示已完成事件发布
        boolean success = this.queue.offer(event);
        if (!success) {
            LOGGER.warn("Unable to plug in due to interruption, synchronize sending time, event : {}", event);
            receiveEvent(event);
            return true;
        }
        return true;
    }
    
    @Override
    public void init(Class<? extends Event> type, int bufferSize) {
        setDaemon(true);
        setName("nacos.publisher-" + type.getName());
        this.eventType = type;
        this.queueMaxSize = bufferSize;
        this.queue = new ArrayBlockingQueue<Event>(bufferSize);
        start();
    }
    
    @Override
    public synchronized void start() {
        if (!initialized) {
            //执行线程的run()方法,start just called once
            super.start();
            if (queueMaxSize == -1) {
                queueMaxSize = ringBufferSize;
            }
            initialized = true;
        }
    }
    
    @Override
    public void run() {
        openEventHandler();
    }
    
    void openEventHandler() {
        try {
            //This variable is defined to resolve the problem which message overstock in the queue.
            int waitTimes = 60;
            //To ensure that messages are not lost, enable EventHandler when waiting for the first Subscriber to register
            for (; ;) {
                if (shutdown || hasSubscriber() || waitTimes <= 0) {
                    break;
                }
                ThreadUtils.sleep(1000L);
                waitTimes--;
            }
            for (; ;) {
                if (shutdown) {
                    break;
                }
                final Event event = queue.take();
                receiveEvent(event);
                UPDATER.compareAndSet(this, lastEventSequence, Math.max(lastEventSequence, event.sequence()));
            }
        } catch (Throwable ex) {
            LOGGER.error("Event listener exception : {}", ex);
        }
    }
    
    //Receive and notifySubscriber to process the event.
    void receiveEvent(Event event) {
        final long currentEventSequence = event.sequence();
        //循环遍历事件的订阅者
        for (Subscriber subscriber : subscribers) {
            // Whether to ignore expiration events
            if (subscriber.ignoreExpireEvent() && lastEventSequence > currentEventSequence) {
                LOGGER.debug("[NotifyCenter] the {} is unacceptable to this subscriber, because had expire", event.getClass());
                continue;
            }
            //通知事件订阅者
            notifySubscriber(subscriber, event);
        }
    }
    
    @Override
    public void notifySubscriber(final Subscriber subscriber, final Event event) {
        LOGGER.debug("[NotifyCenter] the {} will received by {}", event, subscriber);
        final Runnable job = new Runnable() {
            @Override
            public void run() {
                //异步执行订阅者的监听逻辑
                subscriber.onEvent(event);
            }
        };
        final Executor executor = subscriber.executor();
        if (executor != null) {
            executor.execute(job);
        } else {
            try {
                job.run();
            } catch (Throwable e) {
                LOGGER.error("Event callback exception : {}", e);
            }
        }
    }
    ...
}

(3)集群节点间的配置数据变更同步

核心处理方法便是AsyncNotifyService的onEvent()方法。该方法首先会获取集群节点列表,然后遍历集群列表构造通知任务NotifySingleTask,接着把通知任务NotifySingleTask添加到队列queue当中,最后根据通知任务队列queue封装一个异步任务提交到线程池去处理,也就是异步任务AsyncTask的run()方法会处理通知任务NotifySingleTask。

在异步任务AsyncTask的run()方法中,会一直从queue中获取通知任务,以便将配置数据同步到对应的集群节点。具体就是在while循环中,首先获得通知任务中对应的集群节点的IP地址。然后判断该集群节点的IP是否在当前节点的配置中,并且是否是健康状态。如果该集群节点不健康,则放入队列并将队列提交给异步任务来延迟处理。如果该集群节点是健康状态,则通过HTTP方式发起配置数据的同步,地址是"/v1/cs/communication/dataChange"。

复制代码
@Service
public class AsyncNotifyService {
    ...
    @Autowired
    public AsyncNotifyService(ServerMemberManager memberManager) {
        this.memberManager = memberManager;
        //Register ConfigDataChangeEvent to NotifyCenter.
        NotifyCenter.registerToPublisher(ConfigDataChangeEvent.class, NotifyCenter.ringBufferSize);
        //Register A Subscriber to subscribe ConfigDataChangeEvent.
        NotifyCenter.registerSubscriber(new Subscriber() {
            @Override
            public void onEvent(Event event) {
                //配置中心数据变更,同步其他集群节点数据
                if (event instanceof ConfigDataChangeEvent) {
                    ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;
                    long dumpTs = evt.lastModifiedTs;
                    String dataId = evt.dataId;
                    String group = evt.group;
                    String tenant = evt.tenant;
                    String tag = evt.tag;
                    //获取集群节点列表
                    Collection<Member> ipList = memberManager.allMembers();
                  
                    Queue<NotifySingleTask> queue = new LinkedList<NotifySingleTask>();
                    //遍历集群列表构造通知任务NotifySingleTask去同步数据
                    for (Member member : ipList) {
                        //把通知任务NotifySingleTask添加到队列queue当中
                        queue.add(new NotifySingleTask(dataId, group, tenant, tag, dumpTs, member.getAddress(), evt.isBeta));
                    }
                    //根据通知任务队列Queue<NotifySingleTask>,封装一个异步任务AsyncTask,提交到线程池执行
                    ConfigExecutor.executeAsyncNotify(new AsyncTask(nacosAsyncRestTemplate, queue));
                }
            }
            
            @Override
            public Class<? extends Event> subscribeType() {
                return ConfigDataChangeEvent.class;
            }
        });
    }
    ...
    class AsyncTask implements Runnable {
        private Queue<NotifySingleTask> queue;
        private NacosAsyncRestTemplate restTemplate;
        
        public AsyncTask(NacosAsyncRestTemplate restTemplate, Queue<NotifySingleTask> queue) {
            this.restTemplate = restTemplate;
            this.queue = queue;
        }
    
        @Override
        public void run() {
            executeAsyncInvoke();
        }
    
        private void executeAsyncInvoke() {
            while (!queue.isEmpty()) {
                //一直从queue队列中获取通知任务,以便将配置数据同步到对应的集群节点
                NotifySingleTask task = queue.poll();
                //获取通知任务中对应的集群节点的IP地址
                String targetIp = task.getTargetIP();
                if (memberManager.hasMember(targetIp)) {
                    //start the health check and there are ips that are not monitored, put them directly in the notification queue, otherwise notify
                    //判断该集群节点的ip是否在当前节点的配置中,并且是否是健康状态
                    boolean unHealthNeedDelay = memberManager.isUnHealth(targetIp);
                    if (unHealthNeedDelay) {
                        //target ip is unhealthy, then put it in the notification list
                        //如果该集群节点不健康,则放入另外一个队列,同样会将队列提交给异步任务,然后延迟处理
                        ConfigTraceService.logNotifyEvent(task.getDataId(), task.getGroup(), task.getTenant(), null,
                            task.getLastModified(), InetUtils.getSelfIP(), ConfigTraceService.NOTIFY_EVENT_UNHEALTH,
                            0, task.target);
                        //get delay time and set fail count to the task
                        asyncTaskExecute(task);
                    } else {
                        //如果该集群节点是健康状态,则通过HTTP方式发起配置数据的同步
                        Header header = Header.newInstance();
                        header.addParam(NotifyService.NOTIFY_HEADER_LAST_MODIFIED, String.valueOf(task.getLastModified()));
                        header.addParam(NotifyService.NOTIFY_HEADER_OP_HANDLE_IP, InetUtils.getSelfIP());
                        if (task.isBeta) {
                            header.addParam("isBeta", "true");
                        }
                        AuthHeaderUtil.addIdentityToHeader(header);
                        //通过HTTP方式发起配置数据的同步,请求的HTTP地址:/v1/cs/communication/dataChange
                        restTemplate.get(task.url, header, Query.EMPTY, String.class, new AsyncNotifyCallBack(task));
                    }
                }
            }
        }
    }
    
    private void asyncTaskExecute(NotifySingleTask task) {
        int delay = getDelayTime(task);
        Queue<NotifySingleTask> queue = new LinkedList<NotifySingleTask>();
        queue.add(task);
        AsyncTask asyncTask = new AsyncTask(nacosAsyncRestTemplate, queue);
        //提交异步任务给线程池延迟执行
        ConfigExecutor.scheduleAsyncNotify(asyncTask, delay, TimeUnit.MILLISECONDS);
    }
}

当集群节点处理"/v1/cs/communication/dataChange"这个HTTP请求时,会调用CommunicationController的notifyConfigInfo()方法,接着调用DumpService的dump()方法将请求包装成DumpTask同步数据任务,然后调用TaskManager的addTask()方法将DumpTask同步数据任务放入map。

TaskManager的父类NacosDelayTaskExecuteEngine在初始化时,会开启一个异步任务执行ProcessRunnable的run()方法,也就是会不断从map中取出DumpTask同步数据任务,然后调用DumpProcessor的process()方法处理具体的配置数据同步逻辑。也就是查询数据库最新的配置,然后持久化配置数据到磁盘上,从而完成集群之间配置数据的同步。

复制代码
@RestController
@RequestMapping(Constants.COMMUNICATION_CONTROLLER_PATH)
public class CommunicationController {
    private final DumpService dumpService;
    ...
    @GetMapping("/dataChange")
    public Boolean notifyConfigInfo(HttpServletRequest request, @RequestParam("dataId") String dataId,
            @RequestParam("group") String group,
            @RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
            @RequestParam(value = "tag", required = false) String tag) {
        dataId = dataId.trim();
        group = group.trim();
        String lastModified = request.getHeader(NotifyService.NOTIFY_HEADER_LAST_MODIFIED);
        long lastModifiedTs = StringUtils.isEmpty(lastModified) ? -1 : Long.parseLong(lastModified);
        String handleIp = request.getHeader(NotifyService.NOTIFY_HEADER_OP_HANDLE_IP);
        String isBetaStr = request.getHeader("isBeta");
        if (StringUtils.isNotBlank(isBetaStr) && trueStr.equals(isBetaStr)) {
            dumpService.dump(dataId, group, tenant, lastModifiedTs, handleIp, true);
        } else {
            dumpService.dump(dataId, group, tenant, tag, lastModifiedTs, handleIp);
        }
        return true;
    }
    ...
}

public abstract class DumpService {
    private TaskManager dumpTaskMgr;
    public DumpService(PersistService persistService, ServerMemberManager memberManager) {
        ...
        this.processor = new DumpProcessor(this);
        this.dumpTaskMgr = new TaskManager("com.alibaba.nacos.server.DumpTaskManager");
        this.dumpTaskMgr.setDefaultTaskProcessor(processor);
        ...
    }
    ...
    public void dump(String dataId, String group, String tenant, long lastModified, String handleIp, boolean isBeta) {
        String groupKey = GroupKey2.getKey(dataId, group, tenant);
        dumpTaskMgr.addTask(groupKey, new DumpTask(groupKey, lastModified, handleIp, isBeta));
    }
    ...
}

public final class TaskManager extends NacosDelayTaskExecuteEngine implements TaskManagerMBean {
    ...
    @Override
    public void addTask(Object key, AbstractDelayTask newTask) {
        super.addTask(key, newTask);
        MetricsMonitor.getDumpTaskMonitor().set(tasks.size());
    }
    ...
}

public class NacosDelayTaskExecuteEngine extends AbstractNacosTaskExecuteEngine<AbstractDelayTask> {
    protected final ConcurrentHashMap<Object, AbstractDelayTask> tasks;//任务池
    public NacosDelayTaskExecuteEngine(String name, int initCapacity, Logger logger, long processInterval) {
        super(logger);
        tasks = new ConcurrentHashMap<Object, AbstractDelayTask>(initCapacity);
        processingExecutor = ExecutorFactory.newSingleScheduledExecutorService(new NameThreadFactory(name));
        //开启延时任务
        processingExecutor.scheduleWithFixedDelay(new ProcessRunnable(), processInterval, processInterval, TimeUnit.MILLISECONDS);
    }
    ...
    
    @Override
    public void addTask(Object key, AbstractDelayTask newTask) {
        lock.lock();
        try {
            AbstractDelayTask existTask = tasks.get(key);
            if (null != existTask) {
                newTask.merge(existTask);
            }
            //最后放入到ConcurrentHashMap中
            tasks.put(key, newTask);
        } finally {
            lock.unlock();
        }
    }
    ...
    private class ProcessRunnable implements Runnable {
        @Override
        public void run() {
            try {
                processTasks();
            } catch (Throwable e) {
                getEngineLog().error(e.toString(), e);
            }
        }
    }
    
    @Override
    public Collection<Object> getAllTaskKeys() {
        Collection<Object> keys = new HashSet<Object>();
        lock.lock();
        try {
            keys.addAll(tasks.keySet());
        } finally {
            lock.unlock();
        }
        return keys;
    }
    
    protected void processTasks() {
        //获取tasks中所有的任务,然后进行遍历
        Collection<Object> keys = getAllTaskKeys();
        for (Object taskKey : keys) {
            //通过任务key,获取具体的任务,并且从任务池中移除掉
            AbstractDelayTask task = removeTask(taskKey);
            if (null == task) {
                continue;
            }
            //DumpService在初始化时会设置TaskManager的默认processor是DumpProcessor
            //根据taskKey获取NacosTaskProcessor延迟任务处理器:DumpProcessor
            NacosTaskProcessor processor = getProcessor(taskKey);
            if (null == processor) {
                getEngineLog().error("processor not found for task, so discarded. " + task);
                continue;
            }
            try {
                //ReAdd task if process failed
                //调用DumpProcessor.process()方法
                if (!processor.process(task)) {
                    //如果失败了,会重试添加task回tasks这个map中
                    retryFailedTask(taskKey, task);
                }
            } catch (Throwable e) {
                getEngineLog().error("Nacos task execute error : " + e.toString(), e);
                retryFailedTask(taskKey, task);
            }
        }
    }
}

(4)服务端通知客户端配置数据已变更

服务端通知客户端配置文件变更的方法是LongPollingService.onEvent()。

由前面客户端如何感知远程配置数据的变更可知,Nacos客户端启动时:会调用ConfigService的addListener()方法为每个dataId + group添加一个监听器。而NacosConfigService初始化时会创建ClientWorker对象,此时会开启多个长连接任务即执行LongPollingRunnable的run()方法。

执行LongPollingRunnable的run()方法时,会触发执行ClientWorker的checkUpdateDataIds()方法,该方法最后会调用服务端的"/v1/cs/configs/listener"接口,将当前客户端添加到LongPollingService的allSubs属性中。

这样当以后dataId + group的配置发生变更时,服务端会触发执行LongPollingService的onEvent()方法,然后遍历LongPollingService.allSubs属性通知客户端配置已变更。

客户端收到变更事件通知后,会将最新的配置刷新到容器中,同时将@RefreshScope注解修饰的Bean从缓存中删除。这样再次访问这些Bean,就会重新创建Bean,从而读取到最新的配置。

复制代码
public class NacosConfigService implements ConfigService {
    //long polling.
    private final ClientWorker worker;
    ...
    public NacosConfigService(Properties properties) throws NacosException {
        ...
        this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
    }
    ...
}

//Long polling.
public class ClientWorker implements Closeable {
    public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
        ...
        this.executor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    checkConfigInfo();
                } catch (Throwable e) {
                    LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
                }
            }
        }, 1L, 10L, TimeUnit.MILLISECONDS);
    }
    
    //Check config info.
    public void checkConfigInfo() {
        //Dispatch taskes.
        int listenerSize = cacheMap.size();
        //Round up the longingTaskCount.
        int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
        if (longingTaskCount > currentLongingTaskCount) {
            for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
                //The task list is no order.So it maybe has issues when changing.
                //执行长连接任务:LongPollingRunnable.run()
                executorService.execute(new LongPollingRunnable(i));
            }
            currentLongingTaskCount = longingTaskCount;
        }
    }
    ...
    class LongPollingRunnable implements Runnable {
        ...
        @Override
        public void run() {
            ...
            //check server config
            List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
            ...
        }
    }
    
    //Fetch the dataId list from server.
    List<String> checkUpdateDataIds(List<CacheData> cacheDatas, List<String> inInitializingCacheList) throws Exception {
        ...
        return checkUpdateConfigStr(sb.toString(), isInitializingCacheList);
    }
    
    //Fetch the updated dataId list from server.
    List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws Exception {
        ...
        try {
            //In order to prevent the server from handling the delay of the client's long task, increase the client's read timeout to avoid this problem.
            long readTimeoutMs = timeout + (long) Math.round(timeout >> 1);
            //发起HTTP请求:/v1/cs/configs/listener,将客户端添加到LongPollingService.allSubs属性中
            HttpRestResult<String> result = agent.httpPost(Constants.CONFIG_CONTROLLER_PATH + "/listener", headers, params, agent.getEncode(), readTimeoutMs);
            ...
        } catch (Exception e) {
            ...
        }
        return Collections.emptyList();
    }
}

@RestController
@RequestMapping(Constants.CONFIG_CONTROLLER_PATH)
public class ConfigController {
    private final ConfigServletInner inner;
    ...
    @PostMapping("/listener")
    @Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
    public void listener(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ...
        //do long-polling
        inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
    }
    ...
}

@Service
public class ConfigServletInner {
    ...
    //轮询接口.
    public String doPollingConfig(HttpServletRequest request, HttpServletResponse response, Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {
        //Long polling.
        if (LongPollingService.isSupportLongPolling(request)) {
            longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
            return HttpServletResponse.SC_OK + "";
        }
        ...
    }
    ...
}

@Service
public class LongPollingService {
    //客户端长轮询订阅者
    final Queue<ClientLongPolling> allSubs;
    ...
    public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map, int probeRequestSize) {
        ...
        //添加订阅者
        ConfigExecutor.executeLongPolling(new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
    }
    
    class ClientLongPolling implements Runnable {
        @Override
        public void run() {
            ...
            allSubs.add(this);
        }
       ...
    }
    ...
    public LongPollingService() {
        allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();
        ...
        NotifyCenter.registerSubscriber(new Subscriber() {
            @Override
            public void onEvent(Event event) {
                if (isFixedPolling()) {
                    // Ignore.
                } else {
                    if (event instanceof LocalDataChangeEvent) {
                        LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
                        //触发执行DataChangeTask.run()方法
                        ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
                    }
                }
            }
            ...
        });
    }
    
    class DataChangeTask implements Runnable {
        @Override
        public void run() {
            try {
                ConfigCacheService.getContentBetaMd5(groupKey);
                //遍历订阅了配置变更事件的客户端
                for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
                    ClientLongPolling clientSub = iter.next();
                    if (clientSub.clientMd5Map.containsKey(groupKey)) {
                        ...
                        getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                        iter.remove(); // Delete subscribers' relationships.
                        //发送服务端数据变更的响应给客户端
                        clientSub.sendResponse(Arrays.asList(groupKey));
                    }
                }
            } catch (Throwable t) {
                LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
            }
        }
    }
    ...
}

(5)总结

一.配置中心数据变更同步集群节点的整体逻辑

当在Nacos后台变更配置数据后:首先自身节点会把最新的配置数据更新到数据库中,并且添加变更历史。然后利用事件发布订阅机制来通知订阅者,其中订阅者AsyncNotifyService会通过HTTP方式来通知其他集群节点。当其他集群节点收到通知后,会重新查询数据库最新的配置数据。然后持久化到磁盘上,因为获取配置数据的接口是直接读磁盘文件的。集群节点的配置数据同步完成后,还要通知客户端配置数据已变更。

二.服务端通知客户端配置数据已变更

在客户端给dataId + group添加监听器后,会和服务端建立一个长轮询,所以另外一个订阅者LongPollingService会通过长轮询通知客户端。也就是会遍历每一个客户端,通过长轮询向客户端进行响应。最终会调用到客户端监听器的回调方法,从而去刷新客户端的配置Bean。

相关推荐
东阳马生架构30 分钟前
Nacos源码—6.Nacos升级gRPC分析一
nacos
gxh19922 小时前
springboot微服务连接nacos超时
linux·后端·微服务·nacos
东阳马生架构13 小时前
Nacos源码—5.Nacos配置中心实现分析一
nacos·注册中心·配置中心
东阳马生架构1 天前
Nacos源码—5.Nacos配置中心实现分析
nacos
东阳马生架构2 天前
Nacos源码—4.Nacos集群高可用分析三
nacos·注册中心·配置中心
东阳马生架构2 天前
Nacos源码—4.Nacos集群高可用分析二
nacos
东阳马生架构3 天前
Nacos源码—3.Nacos集群高可用分析
nacos
冷环渊5 天前
Finish技术生态计划: FinishRpc
java·后端·nacos·rpc·netty
forestsea6 天前
Nacos-3.0.0适配PostgreSQL数据库
数据库·postgresql·nacos