什么是Etcd?


    文章大部分引至:http://jolestar.com/etcd-architecture/

Etcd 按照官方介紹

Etcd is a distributed, consistent key-value store for shared configuration and service discovery

是一個分布式的,一致的 key-value 存儲,主要用途是共享配置和服務發現。Etcd 已經在很多分布式系統中得到廣泛的使用,本文的架構與實現部分主要解答以下問題:

  1. Etcd是如何實現一致性的?
  2. Etcd的存儲是如何實現的?
  3. Etcd的watch機制是如何實現的?
  4. Etcd的key過期機制是如何實現的?

為什么需要 Etcd ?

所有的分布式系統,都面臨的一個問題是多個節點之間的數據共享問題,這個和團隊協作的道理是一樣的,成員可以分頭干活,但總是需要共享一些必須的信息,比如誰是 leader, 都有哪些成員,依賴任務之間的順序協調等。所以分布式系統要么自己實現一個可靠的共享存儲來同步信息(比如 Elasticsearch ),要么依賴一個可靠的共享存儲服務,而 Etcd 就是這樣一個服務。

Etcd 提供什么能力?

Etcd 主要提供以下能力,已經熟悉 Etcd 的讀者可以略過本段。

  1. 提供存儲以及獲取數據的接口,它通過協議保證 Etcd 集群中的多個節點數據的強一致性。用於存儲元信息以及共享配置。
  2. 提供監聽機制,客戶端可以監聽某個key或者某些key的變更(v2和v3的機制不同,參看后面文章)。用於監聽和推送變更。
  3. 提供key的過期以及續約機制,客戶端通過定時刷新來實現續約(v2和v3的實現機制也不一樣)。用於集群監控以及服務注冊發現。
  4. 提供原子的CAS(Compare-and-Swap)和 CAD(Compare-and-Delete)支持(v2通過接口參數實現,v3通過批量事務實現)。用於分布式鎖以及leader選舉。

更詳細的使用場景不在這里描述,有興趣的可以參看文末infoq的一篇文章。

Etcd 如何實現一致性的?

說到這個就不得不說起raft協議。但這篇文章不是專門分析raft的,篇幅所限,不能詳細分析,有興趣的建議看raft協議的一個動畫。便於看后面的文章,我這里簡單做個總結:

  1. raft通過對不同的場景(選主,日志復制)設計不同的機制,雖然降低了通用性(相對paxos),但同時也降低了復雜度,便於理解和實現。
  2. raft內置的選主協議是給自己用的,用於選出主節點,理解raft的選主機制的關鍵在於理解raft的時鍾周期以及超時機制。
  3. 理解 Etcd 的數據同步的關鍵在於理解raft的日志同步機制。

Etcd 實現raft的時候,充分利用了go語言CSP並發模型和chan的魔法,想更進行一步了解的可以去看源碼,這里只簡單分析下它的wal日志。

etcdv3

wal日志是二進制的,解析出來后是以上數據結構LogEntry。其中第一個字段type,只有兩種,一種是0表示Normal,1表示ConfChange(ConfChange表示 Etcd 本身的配置變更同步,比如有新的節點加入等)。第二個字段是term,每個term代表一個主節點的任期,每次主節點變更term就會變化。第三個字段是index,這個序號是嚴格有序遞增的,代表變更序號。第四個字段是二進制的data,將raft request對象的pb結構整個保存下。Etcd 源碼下有個tools/etcd-dump-logs,可以將wal日志dump成文本查看,可以協助分析raft協議。

raft協議本身不關心應用數據,也就是data中的部分,一致性都通過同步wal日志來實現,每個節點將從主節點收到的data apply到本地的存儲,raft只關心日志的同步狀態,如果本地存儲實現的有bug,比如沒有正確的將data apply到本地,也可能會導致數據不一致。

Etcd v2 與 v3

Etcd v2 和 v3 本質上是共享同一套 raft 協議代碼的兩個獨立的應用,接口不一樣,存儲不一樣,數據互相隔離。也就是說如果從 Etcd v2 升級到 Etcd v3,原來v2 的數據還是只能用 v2 的接口訪問,v3 的接口創建的數據也只能訪問通過 v3 的接口訪問。所以我們按照 v2 和 v3 分別分析。

Etcd v2 存儲,Watch以及過期機制

etcdv2

Etcd v2 是個純內存的實現,並未實時將數據寫入到磁盤,持久化機制很簡單,就是將store整合序列化成json寫入文件。數據在內存中是一個簡單的樹結構。比如以下數據存儲到 Etcd 中的結構就如圖所示。

/nodes/1/name  node1  
/nodes/1/ip    192.168.1.1 

store中有一個全局的currentIndex,每次變更,index會加1.然后每個event都會關聯到currentIndex.

當客戶端調用watch接口(參數中增加 wait參數)時,如果請求參數中有waitIndex,並且waitIndex 小於 currentIndex,則從 EventHistroy 表中查詢index小於等於waitIndex,並且和watch key 匹配的 event,如果有數據,則直接返回。如果歷史表中沒有或者請求沒有帶 waitIndex,則放入WatchHub中,每個key會關聯一個watcher列表。 當有變更操作時,變更生成的event會放入EventHistroy表中,同時通知和該key相關的watcher。

這里有幾個影響使用的細節問題:

  1. EventHistroy 是有長度限制的,最長1000。也就是說,如果你的客戶端停了許久,然后重新watch的時候,可能和該waitIndex相關的event已經被淘汰了,這種情況下會丟失變更。
  2. 如果通知watch的時候,出現了阻塞(每個watch的channel有100個緩沖空間),Etcd 會直接把watcher刪除,也就是會導致wait請求的連接中斷,客戶端需要重新連接。
  3. Etcd store的每個node中都保存了過期時間,通過定時機制進行清理。

從而可以看出,Etcd v2 的一些限制:

  1. 過期時間只能設置到每個key上,如果多個key要保證生命周期一致則比較困難。
  2. watch只能watch某一個key以及其子節點(通過參數 recursive),不能進行多個watch。
  3. 很難通過watch機制來實現完整的數據同步(有丟失變更的風險),所以當前的大多數使用方式是通過watch得知變更,然后通過get重新獲取數據,並不完全依賴於watch的變更event。

Etcd v3 存儲,Watch以及過期機制

etcdv3

Etcd v3 將watch和store拆開實現,我們先分析下store的實現。

Etcd v3 store 分為兩部分,一部分是內存中的索引,kvindex,是基於google開源的一個golang的btree實現的,另外一部分是后端存儲。按照它的設計,backend可以對接多種存儲,當前使用的boltdb。boltdb是一個單機的支持事務的kv存儲,Etcd 的事務是基於boltdb的事務實現的。Etcd 在boltdb中存儲的key是reversion,value是 Etcd 自己的key-value組合,也就是說 Etcd 會在boltdb中把每個版本都保存下,從而實現了多版本機制。

舉個例子: 用etcdctl通過批量接口寫入兩條記錄:

etcdctl txn <<<' 
put key1 "v1" 
put key2 "v2" 

' 

再通過批量接口更新這兩條記錄:

etcdctl txn <<<' 
put key1 "v12" 
put key2 "v22" 

' 

boltdb中其實有了4條數據:

rev={3 0}, key=key1, value="v1" 
rev={3 1}, key=key2, value="v2" 
rev={4 0}, key=key1, value="v12" 
rev={4 1}, key=key2, value="v22" 

reversion主要由兩部分組成,第一部分main rev,每次事務進行加一,第二部分sub rev,同一個事務中的每次操作加一。如上示例,第一次操作的main rev是3,第二次是4。當然這種機制大家想到的第一個問題就是空間問題,所以 Etcd 提供了命令和設置選項來控制compact,同時支持put操作的參數來精確控制某個key的歷史版本數。

了解了 Etcd 的磁盤存儲,可以看出如果要從boltdb中查詢數據,必須通過reversion,但客戶端都是通過key來查詢value,所以 Etcd 的內存kvindex保存的就是key和reversion之前的映射關系,用來加速查詢。

然后我們再分析下watch機制的實現。Etcd v3 的watch機制支持watch某個固定的key,也支持watch一個范圍(可以用於模擬目錄的結構的watch),所以 watchGroup 包含兩種watcher,一種是 key watchers,數據結構是每個key對應一組watcher,另外一種是 range watchers, 數據結構是一個 IntervalTree(不熟悉的參看文文末鏈接),方便通過區間查找到對應的watcher。

同時,每個 WatchableStore 包含兩種 watcherGroup,一種是synced,一種是unsynced,前者表示該group的watcher數據都已經同步完畢,在等待新的變更,后者表示該group的watcher數據同步落后於當前最新變更,還在追趕。

當 Etcd 收到客戶端的watch請求,如果請求攜帶了revision參數,則比較請求的revision和store當前的revision,如果大於當前revision,則放入synced組中,否則放入unsynced組。同時 Etcd 會啟動一個后台的goroutine持續同步unsynced的watcher,然后將其遷移到synced組。也就是這種機制下,Etcd v3 支持從任意版本開始watch,沒有v2的1000條歷史event表限制的問題(當然這是指沒有compact的情況下)。

另外我們前面提到的,Etcd v2在通知客戶端時,如果網絡不好或者客戶端讀取比較慢,發生了阻塞,則會直接關閉當前連接,客戶端需要重新發起請求。Etcd v3為了解決這個問題,專門維護了一個推送時阻塞的watcher隊列,在另外的goroutine里進行重試。

Etcd v3 對過期機制也做了改進,過期時間設置在lease上,然后key和lease關聯。這樣可以實現多個key關聯同一個lease id,方便設置統一的過期時間,以及實現批量續約。

相比Etcd v2, Etcd v3的一些主要變化:

  1. 接口通過grpc提供rpc接口,放棄了v2的http接口。優勢是長連接效率提升明顯,缺點是使用不如以前方便,尤其對不方便維護長連接的場景。
  2. 廢棄了原來的目錄結構,變成了純粹的kv,用戶可以通過前綴匹配模式模擬目錄。
  3. 內存中不再保存value,同樣的內存可以支持存儲更多的key。
  4. watch機制更穩定,基本上可以通過watch機制實現數據的完全同步。
  5. 提供了批量操作以及事務機制,用戶可以通過批量事務請求來實現Etcd v2的CAS機制(批量事務支持if條件判斷)。

Etcd,Zookeeper,Consul 比較

這三個產品是經常被人拿來做選型比較的。 Etcd 和 Zookeeper 提供的能力非常相似,都是通用的一致性元信息存儲,都提供watch機制用於變更通知和分發,也都被分布式系統用來作為共享信息存儲,在軟件生態中所處的位置也幾乎是一樣的,可以互相替代的。二者除了實現細節,語言,一致性協議上的區別,最大的區別在周邊生態圈。Zookeeper 是apache下的,用java寫的,提供rpc接口,最早從hadoop項目中孵化出來,在分布式系統中得到廣泛使用(hadoop, solr, kafka, mesos 等)。Etcd 是coreos公司旗下的開源產品,比較新,以其簡單好用的rest接口以及活躍的社區俘獲了一批用戶,在新的一些集群中得到使用(比如kubernetes)。雖然v3為了性能也改成二進制rpc接口了,但其易用性上比 Zookeeper 還是好一些。 而 Consul 的目標則更為具體一些,Etcd 和 Zookeeper 提供的是分布式一致性存儲能力,具體的業務場景需要用戶自己實現,比如服務發現,比如配置變更。而Consul 則以服務發現和配置變更為主要目標,同時附帶了kv存儲。 在軟件生態中,越抽象的組件適用范圍越廣,但同時對具體業務場景需求的滿足上肯定有不足之處。

Etcd 的周邊工具

  1. Confd
    在分布式系統中,理想情況下是應用程序直接和 Etcd 這樣的服務發現/配置中心交互,通過監聽 Etcd 進行服務發現以及配置變更。但我們還有許多歷史遺留的程序,服務發現以及配置大多都是通過變更配置文件進行的。Etcd 自己的定位是通用的kv存儲,所以並沒有像 Consul 那樣提供實現配置變更的機制和工具,而 Confd 就是用來實現這個目標的工具。
    Confd 通過watch機制監聽 Etcd 的變更,然后將數據同步到自己的一個本地存儲。用戶可以通過配置定義自己關注那些key的變更,同時提供一個配置文件模板。Confd 一旦發現數據變更就使用最新數據渲染模板生成配置文件,如果新舊配置文件有變化,則進行替換,同時觸發用戶提供的reload腳本,讓應用程序重新加載配置。
    Confd 相當於實現了部分 Consul 的agent以及consul-template的功能,作者是kubernetes的Kelsey Hightower,但大神貌似很忙,沒太多時間關注這個項目了,很久沒有發布版本,我們着急用,所以fork了一份自己更新維護,主要增加了一些新的模板函數以及對metad后端的支持。confd
  2. Metad
    服務注冊的實現模式一般分為兩種,一種是調度系統代為注冊,一種是應用程序自己注冊。調度系統代為注冊的情況下,應用程序啟動后需要有一種機制讓應用程序知道『我是誰』,然后發現自己所在的集群以及自己的配置。Metad 提供這樣一種機制,客戶端請求 Metad 的一個固定的接口 /self,由 Metad 告知應用程序其所屬的元信息,簡化了客戶端的服務發現和配置變更邏輯。
    Metad 通過保存一個ip到元信息路徑的映射關系來做到這一點,當前后端支持Etcd v3,提供簡單好用的 http rest 接口。 它會把 Etcd 的數據通過watch機制同步到本地內存中,相當於 Etcd 的一個代理。所以也可以把它當做Etcd 的代理來使用,適用於不方便使用 Etcd v3的rpc接口或者想降低 Etcd 壓力的場景。 metad
  3. Etcd 集群一鍵搭建腳本
    Etcd 官方那個一鍵搭建腳本有bug,我自己整理了一個腳本,通過docker的network功能,一鍵搭建一個本地的 Etcd 集群便於測試和試驗。一鍵搭建腳本

Etcd 使用注意事項

    1. Etcd cluster 初始化的問題
      如果集群第一次初始化啟動的時候,有一台節點未啟動,通過v3的接口訪問的時候,會報告Error:  Etcdserver: not capable 錯誤。這是為兼容性考慮,集群啟動時默認的API版本是2.3,只有當集群中的所有節點都加入了,確認所有節點都支持v3接口時,才提升集群版本到v3。這個只有第一次初始化集群的時候會遇到,如果集群已經初始化完畢,再掛掉節點,或者集群關閉重啟(關閉重啟的時候會從持久化數據中加載集群API版本),都不會有影響。
    2. Etcd 讀請求的機制
      v2  quorum=true 的時候,讀取是通過raft進行的,通過cli請求,該參數默認為true。
      v3  –consistency=“l” 的時候(默認)通過raft讀取,否則讀取本地數據。sdk 代碼里則是通過是否打開:WithSerializable option 來控制。
      一致性讀取的情況下,每次讀取也需要走一次raft協議,能保證一致性,但性能有損失,如果出現網絡分區,集群的少數節點是不能提供一致性讀取的。但如果不設置該參數,則是直接從本地的store里讀取,這樣就損失了一致性。使用的時候需要注意根據應用場景設置這個參數,在一致性和可用性之間進行取舍。
    3. Etcd 的 compact 機制
      Etcd 默認不會自動 compact,需要設置啟動參數,或者通過命令進行compact,如果變更頻繁建議設置,否則會導致空間和內存的浪費以及錯誤。Etcd v3 的默認的 backend quota 2GB,如果不 compact,boltdb 文件大小超過這個限制后,就會報錯:”Error: etcdserver: mvcc: database space exceeded”,導致數據無法寫入。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM