前言
如上圖所示,Apollo portal 更新配置后,進行輪詢的客戶端獲取更新通知,然后再調用接口獲取最新配置。不僅僅只有輪詢,還有定時更新(默認 5 分鍾一次)。目的就是讓客戶端能夠穩定的獲取到最新的配置。
一起來看看他的設計。
核心代碼
具體的類是 RemoteConfigRepository
,每一個 Config —— 也就是 namespace 都有一個 RemoteConfigRepository 對象,表示這個 Config 的遠程配置倉庫,可以利用這個倉庫請求遠程服務,得到配置。
RemoteConfigRepository 的構造方法需要一個 namespace
字符串,表明這個 Repository 所屬的 Config 名稱。
下面是該類的構造方法。
public RemoteConfigRepository(String namespace) {
m_namespace = namespace;// Config 名稱
m_configCache = new AtomicReference<>(); // Config 引用
m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);// 單例的 config 配置,存放 application.properties
m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);// HTTP 工具
m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);// 遠程服務 URL 更新類
remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);// 長輪詢服務
m_longPollServiceDto = new AtomicReference<>();// 長輪詢發現的當前配置發生變化的服務
m_remoteMessages = new AtomicReference<>();
m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());// 限流器
m_configNeedForceRefresh = new AtomicBoolean(true);// 是否強制刷新
m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),//1
m_configUtil.getOnErrorRetryInterval() * 8);// 1 * 8;失敗定時重試策略: 最小一秒,最大 8 秒.
gson = new Gson();// json 序列化
this.trySync(); // 第一次同步
this.schedulePeriodicRefresh();// 定時刷新
this.scheduleLongPollingRefresh();// 長輪詢刷新
}
可以看到,在構造方法中,就執行了 3 個本地方法,其中就包括定時刷新和長輪詢刷新。這兩個功能在 apollo 的 github 文檔中也有介紹:
1.客戶端和服務端保持了一個長連接,從而能第一時間獲得配置更新的推送。
2.客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。
3.這是一個fallback機制,為了防止推送機制失效導致配置不更新。
4.客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified。
5.定時頻率默認為每5分鍾拉取一次,客戶端也可以通過在運行時指定System Property: apollo.refreshInterval來覆蓋,單位為分鍾。
所以,長連接是更新配置的主要手段,然后用定時任務輔助長連接,防止長連接失敗。
那就看看長連接和定時任務的具體代碼。
定時任務
定時任務主要由一個單 core 的線程池維護這定時任務。
static {
// 定時任務,單個 core. 后台線程
m_executorService = Executors.newScheduledThreadPool(1,
ApolloThreadFactory.create("RemoteConfigRepository", true));
}
private void schedulePeriodicRefresh() {
// 默認 5 分鍾同步一次.
m_executorService.scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
trySync();
}
}, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),// 5
m_configUtil.getRefreshIntervalTimeUnit());//單位:分鍾
}
具體就是每 5 分鍾執行 sync 方法。我簡化了一下 sync 方法,一起看看:
protected synchronized void sync() {
ApolloConfig previous = m_configCache.get();
// 加載遠程配置
ApolloConfig current = loadApolloConfig();
//reference equals means HTTP 304
if (previous != current) {
m_configCache.set(current);
// 觸發監聽器
this.fireRepositoryChange(m_namespace, this.getConfig());
}
}
首先,拿到上一個 config 對象的引用,然后,加載遠程配置,判斷是否相等,如果不相等,更新引用緩存,觸發監聽器。
可以看出,關鍵是加載遠程配置和觸發監聽器,這兩個操作。
loadApolloConfig 方法主要邏輯就是通過 HTTP 請求從 configService 服務里獲取到配置。大概步驟如下:
- 首先限流。獲取服務列表。然后根據是否有更新通知,決定此次重試幾次,如果有更新,重試2次,反之一次。
- 優先請求通知自己的 configService,如果失敗了,就要進行休息,休息策略要看是否得到更新通知了,如果是,就休息一秒,否則按照 SchedulePolicy 策略來。
- 拿到數據后,重置強制刷新狀態和失敗休息狀態,返回配置。
觸發監聽器步驟:
- 循環遠程倉庫的監聽器,調用他們的 onRepositoryChange 方法。其實就是 Config。
- 然后,更新 Config 內部的引用,循環向線程池提交任務—— 執行 Config 監聽器的 onChange 方法。
好,到這里,定時任務就算處理完了,總之就是調用 sync 方法,請求遠程 configServer 服務,得到結果后,更新 Config 對象里的配置,並通知監聽器。
再來說說長輪詢。
長連接 / 長輪詢
長輪詢實際上就是在一個類似死循環里,不停請求 ConfigServer 的配置變化通知接口 notifications/v2,如果配置有變更,就會返回變更信息,然后向定時任務線程池提交一個任務,任務內容是執行 sync 方法。
在請求 ConfigServer 的時候,ConfigServer 使用了 Servlet 3 的異步特性,將 hold 住連接 30 秒,等到有通知就立刻返回,這樣能夠實現一個基於 HTTP 的長連接。
關於為什么使用 HTTP 長連接,初次接觸 Apollo 的人都會疑惑,為什么使用這種方式,而不是"那種"方式?
下面是作者宋順的回復:
總結一下:
- 為什么不使用消息系統?太復雜,殺雞用牛刀。
- 為什么不用 TCP 長連接?對網絡環境要求高,容易推送失敗。且有雙寫問題。
- 為什么使用 HTTP 長輪詢?性能足夠,結合 Servlet3 的異步特性,能夠維持萬級連接(一個客戶端只有一個長連接)。直接使用 Servlet 的 HTTP 協議,比單獨用 TCP 連接方便。HTTP 請求/響應模式,保證了不會出現雙寫的情況。最主要還是簡單,性能暫時不是瓶頸。
總結
本文沒有貼很多的代碼。因為不是一篇源碼分析的文章。
總之,Apollo 的更新配置設計就是通過定時輪詢和長輪詢進行組合而來。
定時輪詢負責調用獲取配置接口,長輪詢負責調用配置更新通知接口,長輪詢得到結果后,將提交一個任務到定時輪詢線程池里,執行同步操作——也就是調用獲取配置接口。
為什么使用 HTTP 長輪詢? 簡單!簡單!簡單!