soul網(wǎng)關(guān)學(xué)習(xí)11-配置數(shù)據(jù)同步1-HttpLongPolling_2

在上篇中我們分析了配置數(shù)據(jù)同步中HttpLongPollingsoul-bootstrap端的源碼分析。在這一篇中,我們會分析soul-admin端的源碼。
進入正題。。。

找切入點

  • soul-bootstrap端在長輪詢中調(diào)用了soul-admin的兩個接口:
# 拉取特定類型的配置
/configs/fetch
# 配置變更的監(jiān)聽
/configs/listener
  • 全局搜/configs是怎么提供的服務(wù)
    search-cibfugs
  • 我們定位到org.dromara.soul.admin.controller.ConfigController
    ConfigController

拉取配置fetchConfigs

分析

  • org.dromara.soul.admin.listener.AbstractDataChangedListener
    public ConfigData<?> fetchConfig(final ConfigGroupEnum groupKey) {
        // 配置數(shù)據(jù)的緩存
        ConfigDataCache config = CACHE.get(groupKey.name());
        // 不同類型則傳入對應(yīng)類型,返回configData
        switch (groupKey) {
            case APP_AUTH:
                List<AppAuthData> appAuthList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<AppAuthData>>() {
                }.getType());
                // 對于每次的數(shù)據(jù)更新都有記錄cache的md5值,最后更新時間
                return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), appAuthList);
            case PLUGIN:
                List<PluginData> pluginList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<PluginData>>() {
                }.getType());
                return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), pluginList);
            case RULE:
                List<RuleData> ruleList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<RuleData>>() {
                }.getType());
                return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), ruleList);
            case SELECTOR:
                List<SelectorData> selectorList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<SelectorData>>() {
                }.getType());
                return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), selectorList);
            case META_DATA:
                List<MetaData> metaList = GsonUtils.getGson().fromJson(config.getJson(), new TypeToken<List<MetaData>>() {
                }.getType());
                return new ConfigData<>(config.getMd5(), config.getLastModifyTime(), metaList);
            default:
                throw new IllegalStateException("Unexpected groupKey: " + groupKey);
        }
    }
  • 這里注意到,對應(yīng)配置數(shù)據(jù)是直接從內(nèi)存cache中拿的,那什么時候?qū)⑴渲脭?shù)據(jù)放到內(nèi)存cache的?
  • 先來尋找cache的使用情況
    AbstractDataChangedListener.cache
  • 找到updateCache
    protected <T> void updateCache(final ConfigGroupEnum group, final List<T> data) {
        String json = GsonUtils.getInstance().toJson(data);
        ConfigDataCache newVal = new ConfigDataCache(group.name(), json, Md5Utils.md5(json), System.currentTimeMillis());
        ConfigDataCache oldVal = CACHE.put(newVal.getGroup(), newVal);
        log.info("update config cache[{}], old: {}, updated: {}", group, oldVal, newVal);
    }
  • 看起來這里沒啥東西,沒有找到出處;繼續(xù)找updateCache的使用之處
    updateCache.usage
  • 點進去看一個,到updateSelectorCache,再繼續(xù)往上找onSelectorChanged,再到org.dromara.soul.admin.listener.DataChangedEventDispatcher
    DataChangedEventDispatcher
  • DataChangedEventDispatcher使用了spring的內(nèi)存應(yīng)用事件機制,為事件消費端,再找下事件發(fā)布端
    DataChangedEventDispatcher.event
  • 查找關(guān)鍵字DataChangedEvent,看下事件發(fā)布的地方
  • 差不多可以了,找到了源頭的地方,下面總結(jié)一下

總結(jié)

  1. ConfigController提供接口配置獲取/configs/fetch,供soul-bootstrap調(diào)用
  2. http長輪詢數(shù)據(jù)變更監(jiān)聽器HttpLongPollingDataChangedListener,提供fetchConfig方法,其中,所有配置數(shù)據(jù)是存放在其成員變量cache中的;拉取特定類型的配置,只需要從cache中取出來就行了
  3. 關(guān)于配置數(shù)據(jù)的存放,則是用戶在soul-admin的web界面,對配置數(shù)據(jù)更新時,會通過spring的應(yīng)用事件機制,將變更的數(shù)據(jù)發(fā)布出來,事件為DataChangedEvent;而監(jiān)聽器端則監(jiān)聽DataChangedEvent事件,實現(xiàn)對應(yīng)數(shù)據(jù)變更的存放
  4. 上述是增量數(shù)據(jù)的處理;
  5. 全量數(shù)據(jù)是如何加載到cache中的?
  6. 仔細(xì)看HttpLongPollingDataChangedListener,發(fā)現(xiàn)在實例化的過程中,會創(chuàng)建一個定時任務(wù)線程池,其提供一個后臺守護線程,默認(rèn)情況下會每隔5min鐘會從數(shù)據(jù)庫中拉取配置數(shù)據(jù)加載到內(nèi)存。
   /**
    * Instantiates a new Http long polling data changed listener.
    * @param httpSyncProperties the HttpSyncProperties
    */
   public HttpLongPollingDataChangedListener(final HttpSyncProperties httpSyncProperties) {
       this.clients = new ArrayBlockingQueue<>(1024);
       // 后臺定期reload數(shù)據(jù)庫配置數(shù)據(jù)的線程池
       this.scheduler = new ScheduledThreadPoolExecutor(1,
               SoulThreadFactory.create("long-polling", true));
       this.httpSyncProperties = httpSyncProperties;
   }

   @Override
   protected void afterInitialize() {
       long syncInterval = httpSyncProperties.getRefreshInterval().toMillis();
       // Periodically check the data for changes and update the cache
       // 啟動這個定時任務(wù)線程池,用于reload數(shù)據(jù)庫配置到本地緩存
       scheduler.scheduleWithFixedDelay(() -> {
           log.info("http sync strategy refresh config start.");
           try {
               this.refreshLocalCache();
               log.info("http sync strategy refresh config success.");
           } catch (Exception e) {
               log.error("http sync strategy refresh config error!", e);
           }
       }, syncInterval, syncInterval, TimeUnit.MILLISECONDS);
       log.info("http sync strategy refresh interval: {}ms", syncInterval);
   }

   private void refreshLocalCache() {
       this.updateAppAuthCache();
       this.updatePluginCache();
       this.updateRuleCache();
       this.updateSelectorCache();
       this.updateMetaDataCache();
   }
  • 該操作只會reload,并不會生成update的事件,通知給soul-bootstrap
    現(xiàn)在就只剩下一個問題了,當(dāng)本地緩存數(shù)據(jù)有更新時,是如何通知到soul-bootstrap的呢?下面我們來分析這個問題。

配置變更的監(jiān)聽與響應(yīng)

分析

  • 我們知道soul-bootstrap是通過回調(diào)長輪詢的方式完成配置的監(jiān)聽,那實際上我們只要跟蹤監(jiān)聽的接口邏輯就行
  • 監(jiān)聽接口/config/listener中調(diào)用HttpLongPollingDataChangedListener.doLongPolling
public void doLongPolling(final HttpServletRequest request, final HttpServletResponse response) {

        // compare group md5
        // 根據(jù)監(jiān)聽傳入的md5與更新時間戳找到變化的配置數(shù)據(jù)
        List<ConfigGroupEnum> changedGroup = compareChangedGroup(request);
        String clientIp = getRemoteIp(request);

        // response immediately.
        // 如果此次存在變化的配置數(shù)據(jù),則直接響應(yīng)請求,將變化的配置類型返回給soul-bootstrap
        if (CollectionUtils.isNotEmpty(changedGroup)) {
            this.generateResponse(response, changedGroup);
            log.info("send response with the changed group, ip={}, group={}", clientIp, changedGroup);
            return;
        }

        // listen for configuration changed.
        // 否則將當(dāng)前請求異步化
        final AsyncContext asyncContext = request.startAsync();

        // AsyncContext.settimeout() does not timeout properly, so you have to control it yourself
        // 不設(shè)置超時
        asyncContext.setTimeout(0L);

        // block client's thread.
        // 通過調(diào)度線程池去執(zhí)行監(jiān)聽長輪詢?nèi)蝿?wù),這里的execute是立即執(zhí)行的
        scheduler.execute(new LongPollingClient(asyncContext, clientIp, HttpConstants.SERVER_MAX_HOLD_TIMEOUT));
    }
  • 接入的請求會開啟異步(servelet3.0支持),并將其封裝成長輪詢客戶端LongPollingClient后丟給調(diào)度線程池,并立即執(zhí)行
  • LongPollingClient中的run方法有點精巧,里邊的執(zhí)行邏輯并沒有立即執(zhí)行,而是先丟給調(diào)度線程池,并延遲60s執(zhí)行;同時LongPollingClient會添加到長輪詢隊列clients
       public void run() {
            // 這里并沒有立即執(zhí)行,會將其丟到調(diào)度線程池,延遲60s執(zhí)行
            this.asyncTimeoutFuture = scheduler.schedule(() -> {
                // 執(zhí)行時,先將當(dāng)前長輪詢的client從長輪詢隊列隊列中移除
                clients.remove(LongPollingClient.this);
                // 檢查是否存在變更的配置
                List<ConfigGroupEnum> changedGroups = compareChangedGroup((HttpServletRequest) asyncContext.getRequest());
                // 返回結(jié)果
                sendResponse(changedGroups);
            }, timeoutTime, TimeUnit.MILLISECONDS);
            // 將當(dāng)前長輪詢的client放入長輪詢隊列中
            clients.add(this);
        }
  • 上述做法的目的是,在這延遲的60s中,如果有配置變更產(chǎn)生,則會由配置變更的任務(wù)DataChangeTask,遍歷現(xiàn)有的長輪詢隊列clients,依次移除,并完成LongPollingClient的返回結(jié)果設(shè)置,將異步化的請求操作完結(jié)掉;
        public void run() {
            // 遍歷當(dāng)前所有正在長輪詢的client,將變更的數(shù)據(jù)作為此次輪詢響應(yīng)的結(jié)果返回給長輪詢的client
            //TODO question 這里是否會存在配置丟失的情況?
            // 如果兩次間隔很近的配置變更過來,第一次配置變更還在返回給client,此時的client并沒有重新輪詢進來,
            // 則會導(dǎo)致第二次配置變更沒有通知到第一次已通知的client,從而使得某些client節(jié)點丟失配置
            // 在admin是集群的情況下,該數(shù)據(jù)同步機制可能更不可靠
            for (Iterator<LongPollingClient> iter = clients.iterator(); iter.hasNext();) {
                // 從長輪詢隊列中移除client
                LongPollingClient client = iter.next();
                iter.remove();
                // 并將變更的數(shù)據(jù)返回給長輪詢client
                client.sendResponse(Collections.singletonList(groupKey));
                log.info("send response with the changed group,ip={}, group={}, changeTime={}", client.ip, groupKey, changeTime);
            }
        }
  • LongPollingClient 的返回結(jié)果設(shè)置
       void sendResponse(final List<ConfigGroupEnum> changedGroups) {
            // cancel scheduler
            // 如果在延遲60s的窗口中,存在配置變更的數(shù)據(jù),則會提前結(jié)束,把變更的數(shù)據(jù)給到長輪詢client;
            // 這里的asyncTimeoutFuture便會為空,從而可以取消當(dāng)前延遲執(zhí)行的任務(wù)
            if (null != asyncTimeoutFuture) {
                asyncTimeoutFuture.cancel(false);
            }
            generateResponse((HttpServletResponse) asyncContext.getResponse(), changedGroups);
            asyncContext.complete();
        }
  • 分析結(jié)束,做下總結(jié)

總結(jié)

  1. soul-bootstrapHttpLongPollingTask中采用請求回調(diào)輪詢的方式,去輪詢soul-admin中的配置監(jiān)聽接口/configs/listener,其中每次請求的超時時間為90s
  2. soul-admin中通過HttpLongPollingDataChangedListener.doLongPolling方法開啟請求的異步支持request.startAsync(),避免阻塞住soul-admin端的請求Acceptor線程;
  3. 將異步化請求AsyncContext封裝成長輪詢客戶端任務(wù)LongPollingClient,通過調(diào)度線程池scheduler執(zhí)行。
  4. 長輪詢客戶端任務(wù)LongPollingClientrun方法,將自身邏輯丟給調(diào)度線程池scheduler延遲60s執(zhí)行,這樣就實現(xiàn)請求/configs/listener至少會保持60s
  5. 長輪詢客戶端任務(wù)LongPollingClient還會將自身加入到長輪詢客戶端隊列clients
  6. 如果在請求保持的60s中,存在有配置變更產(chǎn)生(產(chǎn)生來源是用戶在配置web界面操作,包括對插件、選擇器、規(guī)則的增刪改,此類操作會自動觸發(fā)配置變更事件;還有web端提供的一些強制同步功能,如各個插件中的同步、插件管理的同步,也會產(chǎn)生配置變更事件),則會由數(shù)據(jù)變更任務(wù)DataChangeTask,遍歷現(xiàn)有的長輪詢隊列clients,依次移除,并完成長輪詢客戶端任務(wù)LongPollingClient的返回結(jié)果設(shè)置,將異步化的請求操作完結(jié)掉
  7. 上述就是soul-boostrapsoul-admin之間,http長輪詢HttpLongPolling同步方式的配置監(jiān)聽與響應(yīng)的大致流程。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容