背景

上回我們說到Nacos的註冊中心,我們講了註冊中心的一致性協議,訂閱和註冊的原理,有興趣的可以看一下上一篇文章: 你應該瞭解的Nacos註冊中心 。在Nacos中還有一個功能特別重要那就是配置中心,在這裏先不具體介紹配置中心是什麼,先來憶苦思甜一波。

在我們最開始做一些簡單的學習項目的時候,我們會遇到一些需要配置的東西,比如數據庫連接池大小,用戶的黑名單等等,我們都把這些東西寫死在代碼裏面,比如 if(userId == 123){do something} ,這種代碼在項目裏隨處可見。後來參加工作了,發現這種寫法並沒有將配置很好的統一管理起來,配置地方隨處可見,並且無法根據代碼環境去進行調整,比如線上和線下都只能使用同一個配置,雖然可以通過if,else的方式,但是這個非常麻煩,所以在工作就開始使用xml,yaml等方式在文件裏面進行配置,在不同的運行環境讀取不同的配置。這種方式基本滿足了大部分的需求,但是後面遇到了一個需要動態去修改這些配置的情況,如果通過文件的方式我們就只能修改文件然後重新上線服務,這樣是非常麻煩的,所以就誕生了配置中心。

我們在這裏可以想想,如果你要實現配置中心,應該具備哪些功能呢?我這裏列舉一些:

  • 可以動態的修改配置。

  • 配置中心掛了也不影響配置的使用。

  • 配置是可以多個服務共享的。

  • 支持權限管理,只有授予權限的人才能查看和修改配置

  • 配置可以回滾,當我們遇到配置出現問題的時候可以像回滾服務一樣回滾配置。

  • 灰度發佈,可以讓某幾臺機器先使用這個配置如果沒有問題,在進行全量。

  • 配置中心自身的QPS能保證足夠,如果是一個公司的基礎服務的話是需要保證這個的

其實在開源的項目中有挺多配置中心的開源的比如 spring cloud config , Apollo 等等,其中Apollo是攜程開源的配置中心,在業界也是非常出名,我們這邊文章主要還是介紹Nacos的配置中心,當然有興趣的同學可以下來自行查看其他註冊中心相關介紹。

基本概念

同樣的我們首先也先介紹一下和註冊中心相關的一些基本名詞概念:

  • 命名空間(namespace):和註冊中心一樣,命名空間屬於Nacos頂層的結構,用於進行租戶級別的隔離,我們最常用的就是不同環境比如測試環境,線上環境進行隔離。

  • 配置管理:系統配置的編輯、存儲、分發、變更管理、歷史版本管理、變更審計等所有與配置相關的活動。

  • 配置項:一個具體的可配置的參數與其值域,通常以 param-key=param-value 的形式存在。例如我們常配置系統的日誌輸出級別(logLevel=INFO|WARN|ERROR) 就是一個配置項。

  • 配置集: 一組相關或者不相關的配置項的集合稱爲配置集。在系統中,一個配置文件通常就是一個配置集,包含了系統各個方面的配置。例如,一個配置集可能包含了數據源、線程池、日誌級別等配置項。

  • 配置集 ID : Nacos 中的某個配置集的 ID。配置集 ID 是組織劃分配置的維度之一。

  • 配置分組:Nacos 中的一組配置集,是組織配置的維度之一。

  • 配置快照:Nacos 的客戶端 SDK 會在本地生成配置的快照。當客戶端無法連接到 Nacos Server 時,可以使用配置快照顯示系統的整體容災能力。配置快照類似於 Git 中的本地 commit,也類似於緩存,會在適當的時機更新,但是並沒有緩存過期(expiration)的概念。

配置中心的架構圖如下:

  • 用戶可以在後臺界面進行添加或者修改配置,也可以通過client-api進行修改配置

  • 所有修改的數據通過raft首先在Leader修改生效,然後同步至其他副本。

  • 如果用戶想訂閱該配置通過long polling的方式進行訂閱。

一致性存儲

配置中心最爲關鍵的就是如何去做好存儲,一般我們存儲就兩種方式, 要麼全內存存儲,能保證性能非常高,但是維護不同機器內存一致性複雜度比較高,還有一種就是使用數據庫,內存裏面不維護任何狀態,每一臺機器都可以進行寫入操作,這個複雜度比較低,不需要考慮一致性的問題,但是由於所有的讀寫都會走數據庫所以性能就不能保證。在Nacos中對這兩種存儲方式做了一些改進,實現了既保證了性能又保證了複雜度一致性。

在Nacos1.3之後提供了mysql 和 raft + derby兩種存儲方式,接下來介紹一一介紹一下這兩種存儲方式。

mysql + 異步全量通知

nacos最開始提供的就是mysql的方式,所有機器都可以進行讀寫,沒有主備之分,如下圖所示:

數據的讀寫都是直接走Mysql,具體的代碼在ExternalStoragePersistServiceImpl中,其中直接使用的JdbcTemplate,這裏就不詳細把代碼展現出來介紹了,有興趣的可以直接去這裏看。

如果只是使用mysql,有同學會提出問題,只使用mysql如何才能保證數據庫性能不會成爲瓶頸呢?最簡單的方法就是使用高配置的Mysql,用錢給我幹上去,很明顯這個不是很靠譜,只適用於土豪玩家。那麼怎麼去做這種優化呢?一般做業務的同學通常會在Mysql前面放一層緩存層,比如redis,memcached等等。

在Nacos中同樣的也使用了緩存這個概念幫助我們緩解數據庫壓力。但是和普通的緩存稍微有點不同:

在ConfigService中有一個HashMap緩存了所有Config的元數據(MD5,類型這些數據)

但是對於具體存儲的值我們不會直接放在內存,而是存儲到了本地磁盤,這麼做的好處是因爲我們的config所配置的值我們不能保證他的大小,如果每個config的值都很大,那麼我們的內存必然會不足,這個時候Nacos和Apollo 兩個開源中間件給出兩種解法:

  • Apollo的做法是使用一個guavaCache,使用淘汰策略將不經常使用的進行淘汰。

  • Nacos的做法是全量緩存元數據,具體的值存儲到磁盤空間,採用分離存儲的方式,nacos採用這種方法,如果只是訪問元數據那麼全量內存即可,不會像Apollo一樣可能會遇到淘汰的原因,訪問數據庫。

Dump

Nacos使用的是全量緩存元數據到內存,具體的值存儲到磁盤空間,但是會存在一個問題,那就是當一臺機器的數據發生變更,其他機器的內存怎麼變更呢?這就需要我們的全量異步通知,在每一次修改數據的時候都會發送一個ConfigDataChage事件,然後本機接受並進行處理,然後發送這個變更消息到其他的所有機器上。

其他機器收到這個變更通知之後,會進行一次dump操作:

會先查詢元數據中的MD5,MD5其實也是根據我們配置中的值算出來的,所以能進行快速判斷這一次時候發生了值的變更,如果發生變更,我們就將這個值存儲到磁盤上。

如果我們這個機器是新啓動的,這個時候其實就不會存在任何緩存以及dump文件,那麼DumpService會遍歷數據庫的所有數據,全量的都緩存到機器上,以便我們使用。

raft + derby

Nacos在1.3.0之後提供了一個新的存儲模式,那就是使用raft協議保證數據一致性,使用apache derby進行內嵌的數據存儲。提供這種方式的目的是減少用戶維護mysql數據庫集羣的成本,並且簡化了集羣部署的成本,部署Nacos的時候直接打包Nacos鏡像就好,不需要再單獨部署一套數據庫。

在Nacos中使用的是sofa-jraft,這個是螞蟻開源的一個java版本高性能的raft實現,不熟悉raft的同學可以閱讀以下raft的論文,瞭解過raft的同學應該都知道raft非常強化Leader的概念:

  • 系統中必須存在且同一時刻只能有一個 leader,只有 leader 可以接受 clients 發過來的請求

  • Leader 負責主動與所有 followers 通信,負責將’提案’發送給所有 followers,同時收集多數派的 followers 應答

  • Leader 還需向所有 followers 主動發送心跳維持領導地位

我們發現所有的事情都和leader相關,那麼我們的性能必定被限制在leader上面,所以在Nacos中選擇了對raft本身有大量優化的sofa-jraft,在sofa-jraft中做了如下的優化:

  • 批量化:批量化操作是很多系統的一個優化策略,在jraft中同樣的也採用了批量化操作,通過disruptor 的 MPSC 模型批量消費,實現了下面的一些批量操作,提升了很多的性能:

    • 批量提交 task

    • 批量網絡發送

    • 本地 IO batch 寫入

    • 批量應用到狀態機

  • pipeline複製: pipeline是一種管道技術,幫助我們不再和以前請求-響應模型一樣,他可以持續往管道中放入請求,過程中而不需要等待請求的回覆,在最後再一併讀取結果即可。在jraft中開啓pipeline性能會提升30%。

  • 並行化:leader持久化log和發送Log到follower是並行的,發送到不同的follower也是並行的。

  • 線性讀:在raft協議中,讀請求會按照 Log 處理,通過 Log 複製和狀態機執行來得到讀結果,然後再把結果返回給 Client。這種辦法的缺點是需要 Log 存儲、複製,這樣會帶來刷盤開銷、存儲開銷、網絡開銷,因此在讀操作很多的場景下對性能影響很大。在Sofajrat中進行了ReadIndex,Lease Read優化,讓所有的讀都可以在本地執行,這個對性能的提升特別大。

Apache Derby也是一個Java編寫的輕量級數據庫,Nacos通過這樣的設計其實是構建了一個輕量級的分佈式數據庫,在每一臺的機器上都會有一個保存數據的數據庫,然後通過raft協議保證所有機器數據的一致性。

內嵌數據庫的方式並不比Mysql的方式更好,在性能上Mysql那種方式因爲存了很多緩存,並且content也保存到磁盤上,讀取的時候基本不會走庫,所以Mysql的方式其實更好,但是內嵌數據庫的方式在運維部署的方式上是非常佔優的。這裏如何取捨需要用戶自己進行一個選擇

客戶端訂閱變更

我們在上一節說到Nacos註冊中心中的訂閱是通過udp廣播+定時輪訓來獲取到,而在配置中心中採用的是長輪訓的方式進行訂閱變更,爲什麼這兩個實現訂閱會採用不同的方式來實現呢?我們註冊中心中所保存的數據都是小數據比如節點的Ip,端口等信息,但是我們在配置中心中你不能控制配置的大小,比如一個服務訂閱了100個配置,每個配置的數據大小是1M,如果按照定時輪訓的做法每次會拉100M的數據,顯然是不靠譜的,所以這裏採用了長輪訓的方式,具體長輪訓的方式如下:

  • Step1: 客戶端定時發出長輪訓的請求,超時時間默認爲30s。發出的請求是自己所有訂閱配置內容的MD5,這裏我們不會把整個內容當成請求發出,不然又會出現上面所說的每次都會發出很多的數據。

  • Step2: 服務端收到這個請求後利用Servlet3.0的特性,開啓了異步AsyncContext。

  • Step3: 服務端存儲這個AsyncContext,等待配置的變更,數據的變更會通過DataChangeEvent事件中進行觸發,然後判斷之前請求中的md5和新更新的md5是否一樣,如果一致將變更信息寫入到AsyncContext的response中。

  • Ste4: 如果超時還沒有到,那麼代表本次沒有配置進行更新,又會回到Step1。

通過這樣的方式,我們每次請求量都會很少,只有在數據真正更新的時候纔會將真正的數據返回給我們。

配置灰度

我們可能有這樣的一個需求,我們需要驗證某個配置是否對業務上有影響,通常的配置中心都是直接修改,所有機器全量都會被更新,如果這個配置出現問題,那麼就會全量的出現故障。在Nacos中提供了一個灰度的功能,我們可以將某個配置只給某一些機器使用,這樣就可以完成一些小流量驗證。

在Nacos中灰度發佈也叫做beta發佈,如下圖所示:

只要我們修改了配置之後,勾選beta發佈,選中需要灰度的版本即可。

在nacos中的具體實現的是用一個單獨的表去保存beta相關的信息:

用beta_ips字段保存了我們需要灰度的機器,在客戶端訂閱進行長輪訓的時候,也會過濾是否是灰度的機器,如果是纔會進行更新,下面是LongPollingService的代碼:

歷史回滾

在Nacos中也提供了歷史版本,類似git的commit一樣,只要你有commitid你就能回滾到對應的版本,在Nacos用了一個history_config表來進行保存,我們可以通過這個表獲取我們某個配置的所有歷史,以此來進行回滾。

總結

在Nacos中還有很多其他的功能,比如權限管理等等,在這裏我就不一一介紹了。在Nacos的配置中心中設計得最爲巧妙的也就是存儲和訂閱了,存儲Nacos提供了兩種模式,一個是Mysql+緩存+本次磁盤的方式,還有一種是通過raft+derby的方式,都有自己的優劣點。訂閱的話Nacos採用的和註冊中心完全不一樣的方式,通過長輪訓很好的解決了更新的實時通知,並且不需要大量請求資源。如果大家對Nacos感興趣,建議還是可以閱讀下Nacos的代碼。

如果大家覺得這篇文章對你有幫助,你的關注和轉發是對我最大的支持,O(∩_∩)O:

相關文章