引言

記得我們那時候剛開始學習Java的時候都只是一個單體項目,項目里面的配置基本都是寫在項目里面的properties文件中,比如數據庫配置啥的,各種邏輯開關,一旦這些配置修改了,還需要重啟項目這修改才會生效。隨著各種微服務的誕生,服務的拆分也越來越細,可能涉及的服務成千上百,服務基本也是集群部署,這樣再去一個一個項目修改配置,然后重啟這顯然是行不通的。所以分布式配置中心就誕生了,現在開源的分布式配置中心也挺多的比如:開源分布式配置中心有很多,比如spring-cloud/spring-cloud-config、淘寶/diamond、百度/disconf、攜程/apollo、netflix/archaius、Qconf、XDiamond、nacos等等。我們是不是很好奇配置中心如何做到實時更新并且通知到客戶端的這也是一個面試中經常會問到的題目。下面我們就以apollo為例吧去分析分析它是如何實現的。為什么選擇Apollo來分析列?因為現在的公司就在使用它作為配置中心。雖然Apollo是攜程開源的,但是攜程內部也不用它。

Apoll簡介

要去了解一個玩意,就要先會去使用它。它的使用基本上很簡單。雖然使用簡單方便,但是它的設計還是挺復雜的,下面我們看一個它官網提供的架構圖,是不是挺復雜的。
通過上述架構圖我們可以看到ConfigService、AdminService、Client、Portal、 Meta Server、Eureka這幾個模塊,主要的還是前面四個模塊Meta Server、Eureka這兩個模塊只是Apollo本身內部所需要的輔助模塊,我們暫時可以不需要關注它。

ConfigService

  • 提供配置獲取接口
  • 提供配置推送接口
  • 服務于Apollo客戶端

    AdminService

  • 提供配置管理接口
  • 提供配置修改發布接口
  • 服務于管理界面Portal

    Client

  • 為應用獲取配置,支持實時更新
  • 通過MetaServer獲取ConfigService的服務列表/
  • 使用客戶端軟負載SLB方式調用ConfigService

    Portal

  • 配置管理界面
  • 通過MetaServer獲取AdminService的服務列表
  • 使用客戶端軟負載SLB方式調用AdminService

    Apoll更新配置

    介紹完了上面這些Apollo組成的模塊回到正題,配置中心如何做到實時更新并且到客戶端如何感知配置被更新了?看這個問題之前我們先回顧下每到周末我們去人氣比較旺的餐廳吃飯的時候流程是什么樣的?

  • 首先肯定是現場取號,或者手機端取號。
  • 然后就是排隊刷手機等著被叫號。中途你還會主動問一問還要等多久,服務員會告訴你等著吧,你前面還有幾桌。
    上面這個吃飯的例子怎么知道到號了列?兩種方式,一種使我們我每隔一段時間然后主動去問下服務員,是否到號,沒到號繼續刷手機,如果到號直接進去吃飯,還有一種的話就是干脆一直坐在那里刷手機我反正不趕時間,等著被通知到號。同樣的配置中心的更新是如何通知到客戶端列?是服務端主(configService)動通知到客戶端(client)告訴它某某你的應用的配置被修改了,原來的值是啥被修改后的值是啥?還是說客戶端(Client)每隔多久去問下服務端我的配置有沒有被修改呀?如果是你你會怎么選擇列?你也許會說我肯定兩種方式都要呀!小朋友才會做選擇?

    客戶端長鏈接獲取配置更新通知

    再回到我們使用apollo的時候我們應用里面引入的ApolloClient在我們應用啟動的時候會有一個線程每隔5s向服務短發起一個http請求,不過這個http請求是不會立即返回的。它是一個長鏈接如果配置沒有被更新,這個請求會被阻塞掛起,這個實現掛起的方式是通過SpringDeferredResult來實現的,如果對這個SpringDeferredResult不是很了解的推薦看下這個文章《5種SpringMvc的異步處理方式你都了解嗎?》
    掛起60s后會返回HTTP狀態碼為304給到客戶端,如果再阻塞的過程中服務端配置有更新,這個Http請求會立馬返回,并且把變化的nameSpace信息返回出去,并且返回的http的狀態碼是200??蛻舳说玫綘顟B碼是200并且會根據nameSpace立即去服務端拉取最新的配置。

    這里其實有一個問題,為什么不直接在長鏈接中返回變更后的結果,而是返回一個變更的通知,需要客戶端根據這個變更通知立即去拉取新的配置?

    感興趣的可以參考下這個issue :https://github.com/apolloconfig/apollo/issues/652
    這樣推送消息就是有狀態了,做不到冪等了,會帶來很多問題。目前推送是單連接走http的,所以問題可能不大,不過在設計上而言是有這個問題的,比如如果推送是走的tcp長連接的話。另外,長輪詢和推送之間也會有沖突,如果連續兩次配置變化,就可能造成雙寫。還有一點,就是保持邏輯的簡單,目前的做法推送只負責做簡單的通知,不需要去計算客戶端的配置應該是什么,因為計算邏輯挺復雜的,需要考慮集群,關聯,灰度等,總而言之,就是在滿足冪等性,實時性的基礎上保持設計的簡單。

客戶端定時任務全量拉取配置

這樣是不是就是很完美了,客戶端可以實時接收到配置的更新。但是客戶端如果接收服務端的更新內容處理失敗,比如服務異常或者空指針的時候。這時候我們的客戶端配置如果不重啟是不是永遠都不會被更新了。沒關系這種情況apollo也幫你想到啦,你既然告訴我更新失敗,那我就自己每隔一段時間主動去把我所有的配置都拉到客服端,拉回客服端之后和客戶端的緩存配置做比較,如果一致直接結束,不一致就更新客戶端的緩存,并且還會去異步更新本地文件。通過定時任務的補充,可以讓配置達到最終的一致性。

客戶端本地文件緩存配置

主動輪詢,和定時任務全量拉取配置是不是就萬無一失呢?只要涉及到分布式我們就要考慮到其他系統的宕機,比如哪一天挖機直接把部署Apollo的機房的光纖給挖斷了,這樣整個配置服務直接掛了,這時候主動輪詢以及定時任務都沒法起到作用了。是不是拉取不了配置,整個我們的客戶端應用也要跟著受影響列,我們的配置基本上是改動的頻率也是比較小的,即使我們的配置中心掛掉了,我們還有一份本地文件系統來兜底,這個文件目錄默認是/opt/data或C:/opt/data,

所以即使配置中心掛了,對應用的影響也比較小。因為它還會去讀取本地文件來兜底。

小結

到現在為止我們應該知道Apollo客戶端是如何感知服務端配置更新了的把?

  • 主要是通過客戶端應用發起一個長連接去Apollo ConfigServer端,如果Apollo ConfigServer端有配置更改會告訴應用端有配置修改,讓客戶端立馬去拉取全量的配置,并且把配置更新到本地緩存,并且還會異步去更新本地文件緩存。
  • 客戶端還有一個默認5min執行一次的定時任務,去拉取全量的配置。拉回配置之后也是對比本地緩存和遠程是否一致,如果不一致則更新本地進程緩存為遠程的,同時還去異步更新下本地文件。

    結束

  • 由于自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 如果你覺得文章還不錯,你的轉發、分享、贊賞、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎并感謝您的關注。
  • 站在巨人的肩膀
    https://www.apolloconfig.com/#/zh/design/apollo-design?id=%e4%b8%80%e3%80%81%e6%80%bb%e4%bd%93%e8%ae%be%e8%ae%a1
    https://www.iocoder.cn/Apollo/client-polling-config/