在上篇中我們分析了配置數(shù)據(jù)同步中HttpLongPolling
,soul-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é)
-
ConfigController
提供接口配置獲取/configs/fetch
,供soul-bootstrap
調(diào)用 - http長輪詢數(shù)據(jù)變更監(jiān)聽器
HttpLongPollingDataChangedListener
,提供fetchConfig
方法,其中,所有配置數(shù)據(jù)是存放在其成員變量cache
中的;拉取特定類型的配置,只需要從cache
中取出來就行了 - 關(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ù)變更的存放 - 上述是增量數(shù)據(jù)的處理;
- 全量數(shù)據(jù)是如何加載到
cache
中的? - 仔細(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é)
-
soul-bootstrap
在HttpLongPollingTask
中采用請求回調(diào)輪詢的方式,去輪詢soul-admin
中的配置監(jiān)聽接口/configs/listener
,其中每次請求的超時時間為90s
-
soul-admin
中通過HttpLongPollingDataChangedListener.doLongPolling
方法開啟請求的異步支持request.startAsync()
,避免阻塞住soul-admin
端的請求Acceptor
線程; - 將異步化請求
AsyncContext
封裝成長輪詢客戶端任務(wù)LongPollingClient
,通過調(diào)度線程池scheduler
執(zhí)行。 - 長輪詢客戶端任務(wù)
LongPollingClient
的run
方法,將自身邏輯丟給調(diào)度線程池scheduler
延遲60s
執(zhí)行,這樣就實現(xiàn)請求/configs/listener
至少會保持60s
- 長輪詢客戶端任務(wù)
LongPollingClient
還會將自身加入到長輪詢客戶端隊列clients
中 - 如果在請求保持的
60s
中,存在有配置變更產(chǎn)生(產(chǎn)生來源是用戶在配置web界面操作,包括對插件、選擇器、規(guī)則的增刪改,此類操作會自動觸發(fā)配置變更事件;還有web端提供的一些強制同步功能,如各個插件中的同步、插件管理的同步,也會產(chǎn)生配置變更事件),則會由數(shù)據(jù)變更任務(wù)DataChangeTask
,遍歷現(xiàn)有的長輪詢隊列clients
,依次移除,并完成長輪詢客戶端任務(wù)LongPollingClient
的返回結(jié)果設(shè)置,將異步化的請求操作完結(jié)掉 - 上述就是
soul-boostrap
與soul-admin
之間,http
長輪詢HttpLongPolling
同步方式的配置監(jiān)聽與響應(yīng)的大致流程。