分佈式鍵值存儲系統 Etcd 介紹

言歸正傳。前文介紹了分佈式服務框架 Zookeeper 的一些概念,本文繼續介紹 ETCD 相關概念。ETCD 作爲分佈式鍵值存儲系統,使用場景上和 Zookeeper 有很多相似之處,但在設計理念以及架構穩定性和性能上有了提升,本文將對其中的架構流程、一致性 Raft 算法以及存儲模型作簡要介紹。

1、ETCD 基本概念

ETCD 是一個分佈式鍵值對存儲,通常用於分佈式系統中的配置管理、服務發現和分佈式協調工作。

提示:"etcd"這個名字源於兩個想法,即 unix “/etc” 文件夾和分佈式系統"d"istibuted。
“/etc” 文件夾爲單個系統存儲配置數據的地方,而 etcd 存儲大規模分佈式系統的配置信息。
因此,"d"istibuted 的 “/etc” ,是爲 “etcd”。
1.1 ETCD 的特點

ETC 目標是構建一個高可用的分佈式鍵值 key-value 數據庫,內部採用 Raft 一致性協議算法實現,具有以下特點:

1.2 ETCD 相關概念

ETCD 中存在許多概念,如下所示:

1.3 ETCD 和 Zookeeper 對比

ETCD 和 Zookeeper 都可以用於分佈式協調框架和元數據存儲,相比於 Zookeeper 複雜的部署和維護以及開發使用上的複雜性,ETCD 在設計理念上更爲先進:

  1. 動態的集羣節點關係重配置

  2. 高負載條件下的穩定讀寫

  3. 多版本併發控制的數據模型

  4. 持久穩定的 watch 而不是簡單的單次觸發式 watch。Zookeeper 的單次觸發式 watch 是指監聽到一次事件之後,需要客戶端重新發起監聽,這樣 zookeeper 服務器在接收到客戶端的監聽請求之前的事件是獲取不到的,而且在兩次監昕請求的時間間隔內發生的 事件,客戶端也是沒法感知的。etcd 的持久監聽是每當有事件發生時,就會連續觸發,不需要客戶端重新發起監聽

  5. 租約 (lease) 原語實現了連接和會話的解耦

  6. 安全的分佈式共享鎖 API

  7. Etcd 客戶端協議是 gRPC,廣泛支持各種各樣的語言和框架,但 zookeeper 只有它自己的客戶端協議:Jute RPC 協議,只在特定的語言庫(Java 和 C)中綁定。

ETCD 和 Zookeeper 的主要區別如下表所示:

2、ETCD 原理

2.1 ETCD 基礎架構

  1. client 層:包含 client v2 和 v3 兩個⼤版本 API 客戶端

  2. API ⽹絡層

  3. 主要包含 clent 訪問 server 和 server 節點之間的通信協議

  4. clent 訪問 server 分爲兩個版本:v2 API 採⽤ HTTP/1.x 協議,v3 API 採用 gRPC 協議

  5. server 之間的通信:是指節點間通過 Raft 算法實現數據複製和 Leader 選舉等功能時使⽤的 HTTP 協議

  6. Raft 算法層

  7. 實現了 Leader 選舉、⽇志複製、ReadIndex 等核心算法特性

  8. ⽤於保障 etcd 多節點間的數據⼀致性、提升服務可⽤性等,是 etcd 的基⽯和亮點

  9. 功能邏輯層

  10. etcd 核⼼特性實現層

  11. 如典型的 KVServer 模塊、MVCC 模塊、Auth 鑑權模塊、Lease 租約模塊、Compactor 壓縮模塊等

  12. 其中 MVCC 模塊主要有 treeIndex 模塊和 boltdb 模塊組成

  13. 存儲層

  14. 包含預寫⽇志 WAL 模塊、快照 Snapshot 模塊、boltdb 模塊

  15. 其中 WAL 可保障 etcd crash 後數據不丟失,boltdb 則保存了集羣元數據和⽤戶寫⼊的數據

2.1.1 讀請求過程

  1. 客戶端向 ETCD 服務端發起讀請求

  2. KVServer 模塊收到線性讀請求後,向 Raft 模塊發起 ReadIndex 請求

  3. Raft 模塊將 Leader 最新的已提交日誌索引封裝在 ReadState 結構體中

  4. 通過 channel 層層返回給線性讀模塊

  5. 線性讀模塊等待本節點狀態機追趕上 Leader 進度

  6. 追趕完成後,就通知 KVServer 模塊,與狀態機中的 MVCC 模塊進⾏交互

2.1.2 寫請求過程

  1. 客戶端發送更新請求,PUT 指令到 KVServer

  2. ETCD Server 在接收到寫請求時,首先會檢查當前 ETCD DB 的大小和寫入數據的大小是否超出 quota,若超過會產生告警,拒絕寫入

  3. KVServer 將 PUT 寫請求打包成 Proposal 發送到 Raft 模塊

  4. Raft 模塊爲 Proposal 生成一個日誌條目,並將日誌寫入 WAL 模塊持久化,之後將日誌寫入 Raft 穩定日誌中,此時標記爲已提交狀態

  5. Apply 模塊接收到 apply 的消息後,利用 consistent index 字段記錄已執行的日誌

  6. 日誌應用到 MVCC 模塊,實現真正的存儲。MVCC 模塊包含 treeIndex 和 boltdb,treeIndex 在內存中,維護版本號和用戶 key 的映射關係,真正的數據存儲在 boltdb 中

2.2 一致性 Raft 協議

ETCD 使用 Raft 協議來維護集羣內各個節點狀態的一致性。簡單說,ETCD 集羣是一個分佈式系統,由多個節點相互通信構成整體對外服務,每個節點都存儲了完整的數據,並且通過 Raft 協議保證每個節點維護的數據是一致的。

每個 ETCD 節點都維護了一個狀態機,並且任意時刻至多存在一個有效的主節點。主節點處理所有來自客戶端寫操作,通過 Raft 協議保證寫操作對狀態機的改動會可靠的同步到其他節點。

  1. Client 客戶端向 ETCD Server 發送請求

  2. ETCD server 的 KVServer 模塊接收到請求後,向 RAFT 模塊提交 Proposal

  3. Leader 的 RAFT 模塊獲取到提案 Proposal 後,會爲 Proposal 生成日誌條目,並追加到本地日誌

  4. Leader 會向 Follower 廣播消息,爲每個 Follower 生成追加的 RPC 消息,包括複製給 Follower 的日誌條目

  5. Follower 會持久化消息到 WAL 日誌中,並追加到日誌存儲

  6. Follower 向 Leader 回覆一個應答日誌條目的消息,告知 Leader 當前已複製日誌的最大索引

  7. Leader 在收到 Follower 的應答後,將已複製日誌的最大索引信息更新到跟蹤 Follower 進展的 Match Index 字段

  8. Leader 根據 Follower 的 MatchIndex 信息,計算出一個位置。如果該位置已經被一半以上的節點持久化,那麼這個日誌之前的日誌條目都可以標記爲已提交

  9. Leader 發送心跳消息到 Follower 節點時,告知目前已經提交的索引位置

  10. 各個節點的 etcdserver 模塊,根據已提交的日誌條目,將內容 apply 到存儲狀態機,並返回結果給 client 端

2.3 Watcher 特性

Watch 機制指的是訂閱 / 通知,當一個值發生變化的時候,通過訂閱過的節點能夠觀察到這種變化。在 ETCD 中是 key/value 鍵值對的改變,在 Zookeeper 中則是 znode 的變化,但是在 Zookeeper 中只能 watch 子節點不能遞歸到孫節點,另外只能 watch 節點的創建和刪除,不能 watch 節點值的變化。ETCD 則做了改進,並且支持三種類型的 watch:

  1. 當客戶端或者 API 發起 watch key 請求時,etcd 的 gRPCWatchServer 收到 watch 請求時,會創建一個 serverWatchStream

  2. serverWatchStream 收到 create watch 請求後,會調用 MVCC 模塊的 WatchStream 子模塊分配一個 watcher ID,並將 watcher 註冊到 MVCC 的 WatchableKV 模塊

  3. ETCD 啓動的時候,WatchableKV 模塊會運行 syncWatchLoop 和 syncVictimsLoop

2.4 數據版本機制

ETCD 中有個 term 的概念,代表的是整個集羣 Leader 的任期。當集羣發生 Leader 切換,term 的值就會 + 1。在節點故障,或者 Leader 節點網絡出現問題,再或者是將整個集羣停止後再次拉起,都會發生 Leader 的切換。

在 ETCD 中版本號叫做 revision,revision 代表的是全局數據的版本。當數據發生變更,包括創建、修改、刪除,其 revision 對應的都會 + 1。特別的,在集羣中跨 Leader 任期之間,revision 都會保持全局單調遞增。正是 revision 的這一特性,使得集羣中任意一次的修改都對應着一個唯一的 revision,因此可以通過 revision 來支持數據的 MVCC,也可以支持數據的 Watch。

對於每一個 KeyValue 數據節點,etcd 中都記錄了三個版本:

2.5 ETCD 數據存儲

ETCD 和其它存儲系統不同,存儲的一般是重要的元數據信息,通常變動較少,屬於讀多寫少的場景。在 ETCD V2 版本中,整個 ETCD 是一個純內存數據庫,整個數據庫在內存中是一個簡單的樹結構。寫操作先通過 Raft 複製日誌文件,複製成功後將數據寫人內存。在 ETCD V3 版本中實現了 MVCC,每個 Key 值都會保留多個歷史版本,數據量會很大,因此需要保存到磁盤,整個數據庫都會存儲在磁盤上,默認的存儲引擎是 BlotDB。使用 MVCC 的好處是可以減輕用戶設計分佈式系統的難度,通過對多版本的控制,用戶可以獲得一個一致的鍵值空間的快照。用戶可以在無鎖的狀態下查詢快照上的鍵值,減少了鎖衝突,提高了併發。

如上圖所示,ETCD 的存儲分爲兩部分:KVstore 和 backend,其中 KVstore 是存儲在內存中的,用於對數據構建索引便於快速查找;backend 是真正落盤的數據庫,默認使用 BlotDB 存儲引擎實現。

2.5.1 數據持久化 BlotDB

底層的存儲引擎一般包含三大類的選擇:SQL 類數據庫、LevelDB 和 RocksDB、LMDB 和 BoltDB

BoltDB 中的 B + 樹存儲如下所示,B + 樹的非葉子節點存儲的是 revision,對於 revision 可以理解爲唯一併且遞增的序列,包含兩個字段,main 和 sub,main 表示每個事務的一個 id,是全局自增的,sub 表示每個事務中對於每個 key 的更新操作,sub 在每個事務中都從 0 開始。

2.5.2 內存存儲結構

ETCD 在內存中維護了一個基於 B 樹的二級索引來通過業務的 Key 映射到底層存儲中的 B + 樹,其中 B + 樹中,key 爲業務的 key,value 爲 keyIndex

type keyIndex struct {
   key         []byte
   modified    revision // the main rev of the last modification
   generations []generation
}
  1. key 字段就是用戶的原始 key

  2. modified 字段記錄了這個 key 的最後一次修改對應的 revision 信息

  3. generations 保存 key 的多版本信息(歷史修改記錄)

type generation struct {
  ver     int64    //key修改的次數
  created revision // when the generation is created (put in first revision).
  revs    []revision //每次更新key時,append對應的revision
}

generation 結構定義如上,當一個 key 從無到有的時候 ,就會創建一個 generation,其 created 字段記錄了引起本次 key 創建的 revision 信息。當用戶繼續更新這個 key 的時候,generation.revs 數組會不斷追加記錄本次 revision 信息(main,sub)。如果一個 key 被刪除後,又被再次創建,則新建一個 generation。

2.5.3 WAL 日誌和快照

1)WAL 日誌

ETCD 中使用 WAL 機制,即所有的數據提交前都會事先記錄日誌。WAL 機制使得 ETCD 擁有兩個終於的功能:

在 etcd 的數據目錄中,WAL 文件以 $seq-$index.wal 的格式存儲。最初始的 WAL 文件是 0000000000000000-0000000000000000.wal,表示是所有 WAL 文件中的第 0 個,初始的 Raft 狀態編號爲 0。運行一段時間後可能需要進行日誌切分,把新的條目放到一個新的 WAL 文件中。假設,當集羣運行到 Raft 狀態爲 20 時,需要進行 WAL 文件的切分時,下一份 WAL 文件就會變爲 0000000000000001-0000000000000021.wal。如果在 10 次操作後又進行了一次日誌切分,那麼後一次的 WAL 文件名會變爲 0000000000000002-0000000000000031.wal。可以看到 - 符號前面的數字是每次切分後自增 1,而 - 符號後面的數字則是根據實際存儲的 Raft 起始狀態來定。

2)WAL 記錄類型

3)WAL 持久化 Raft 日誌過程

4)Snapshot 快照

既然有了 WAL 實時存儲了所有的變更,爲什麼還需要 snapshot 呢?隨着使用量的增加,WAL 存儲的數據會暴增,爲了防止磁盤很快就爆滿,etcd 默認每 10000 條記錄做一次 snapshot,經過 snapshot 以後的 WAL 文件就可以刪除。因此,snapshot 是爲了防止數據過多而進行的狀態快照,在 Etcd V3 的快照機制是從 BoldDB 中讀取數據庫的當前版本數據,然後序列化到磁盤中。通過引入 WAL 和快照機制,ETCD 實現了故障的快速恢復。

2.6 租約 Lease

lease 是分佈式系統中一個常見的概念,用於代表一個分佈式租約。典型情況下,在分佈式系統中需要去檢測一個節點是否存活時,就需要租約機制。

上圖示例中的代碼示例首先創建了一個 10s 的租約,如果創建租約後不做任何的操作,那麼 10s 之後,這個租約就會自動過期。接着將 key1 和 key2 兩個 key value 綁定到這個租約之上,這樣當租約過期時 etcd 就會自動清理掉 key1 和 key2,使得節點 key1 和 key2 具備了超時自動刪除的能力。

如果希望這個租約永不過期,需要週期性的調用 KeepAlive 方法刷新租約。比如說需要檢測分佈式系統中一個進程是否存活,可以在進程中去創建一個租約,並在該進程中週期性的調用 KeepAlive 的方法。如果一切正常,該節點的租約會一致保持,如果這個進程掛掉了,最終這個租約就會自動過期。

在 etcd 中,允許將多個 key 關聯在同一個 lease 之上,這個設計是非常巧妙的,可以大幅減少 lease 對象刷新帶來的開銷。試想一下,如果有大量的 key 都需要支持類似的租約機制,每一個 key 都需要獨立的去刷新租約,這會給 etcd 帶來非常大的壓力。通過多個 key 綁定在同一個 lease 的模式,我們可以將超時間相似的 key 聚合在一起,從而大幅減小租約刷新的開銷,在不失靈活性同時能夠大幅提高 etcd 支持的使用規模。

3、ETCD 使用場景

3.1 服務註冊與發現

**服務發現(Service Discovery)**要解決的是分佈式系統中最常見的問題之一,即在同一個分佈式集羣中的進程或服務如何才能找到對方並建立連接。從本質上說,服務發現就是要了解集羣中是否有進程在監聽 UDP 或者 TCP 端口,並且通過名字就可以進行查找和鏈接。而要解決服務發現的問題,需要具備如下三個條件:

  1. 一個強一致性、高可用的服務存儲目錄。基於 Raft 算法的 etcd 天生就是這樣一個強一致性和高可用的服務存儲目錄;

  2. 一種註冊服務和監控服務健康狀態的檢測機制。用戶可以在 etcd 中註冊服務,並且對註冊的服務配置 key TTL,定義保持服務的心跳以達到監控健康狀態的效果;

  3. 具備查找和連接服務的機制。在 etcd 指定的主題下注冊的服務也能在對應的主題下找到;爲了確保連接,可以在各個服務機器上都部署一個代理模式的 etcd, 這樣就可以確保訪問 etcd 集羣的服務都可以互相連接

3.2 消息發佈與訂閱

在分佈式系統中,消息的發佈和訂閱機制適用於組件之間的通信機制。具體而言就是,設置一個配置共享中心,消息提供者在這個配置中心發佈消息,而消息使用者則訂閱它們關心的主題,一旦所關心的主題有消息發佈,就會實時通知訂閱者。通過這種方式,可以實現分佈式系統配置的集中式管理和實時動態更新。

1) etcd 集中管理應用配置信息更新

應用在啓動的時候主動從 etcd 獲取一次配置信息,同時在 etcd 節點上註冊 Watcher 並等待。每當配置有更新的時候,etcd 都會實時通知訂閱者,以此達到獲取最新配置信息的目的。

2)分佈式日誌收集系統

這個系統的核心工作是收集分佈在不同機器上的日誌。通過在 etcd 上創建一個以應用(或主題)爲名字的目錄,並將這個應用(或主題)相關的所有機器 IP 以子目錄的形式存儲在目錄下。然後設置一個遞歸的 etcd Watcher,遞歸式地監控應用(或主題)目錄下所有信息的變動。這樣就能夠實現在機器 IP(消息)發生變動時,系統能夠實時接受收集器調整的任務分配。

3.3 元數據存儲

Kubernetes 將自身所用的狀態存儲在 etcd 中,其狀態數據的高可用交給 etcd 來解決,Kubernetes 系統自身不需要再應對複雜的分佈式系統狀態處理,自身的系統架構得到了大幅的簡化。

3.4 分佈式鎖

因爲 ETCD 使用 Raft 算法保持數據的一致性,某次操作存儲到集羣中的值必然是唯一的,因此很容易實現分佈式鎖功能。在前文 “分佈式鎖實現機制” 中也有介紹,ETCD 分佈式鎖實現流程如下所示,主要分爲 6 個階段:

1)準備階段

客戶端連接 Etcd,以 / lock/mylock 爲前綴創建全局唯一的 key,假設第一個客戶端對應的 key 爲 “/lock/mylock/UUIDA”,第二個客戶端對應的 key 爲 “/lock/mylock/UUIDB”,第三個客戶端對應的 key 爲 “/lock/mylock/UUIDC”。客戶端分別爲自己的 key 創建租約 lease,租約的長度根據業務耗時確定。

2)創建定時任務作爲租約的 “心跳”

當客戶端持有鎖期間,其它客戶端只能等到,爲了避免等待期間租約失效,客戶端需要創建一個定時任務作爲心跳以保證租約的有效性。此外,如果持有鎖期間客戶端奔潰,心跳停止,key 值也會因爲租約到期而被刪除,從而釋放鎖資源,避免死鎖。

3)客戶端將自己全局唯一的 key 寫入 Etcd

客戶端進行 Put 操作,將步驟 1 中創建的 key 值綁定租約寫入 Etcd,根據 ETCD 的 revision 機制,ETCD 中會根據事務的操作順序記錄 revision 值。同時,客戶端需要記錄 Etcd 返回的 revision 值,用於接下來判斷是否獲得鎖。在圖中,Etcd 中插入三條 key-value 記錄,Revision 分別爲 1/2/3,其中客戶端 A 返回的 Revision 值爲 1。

4)客戶端判斷是否獲得鎖

客戶端以前綴 / lock/mylock 讀取 key-value 列表,判斷自己的 Revision 是否爲當前 key-value 列表中最小的,如果是則認爲獲得鎖;否則的話,會監聽 key-value 中前一個 Revision 比自己小的 key 的 DELETE 事件,一旦監聽到刪除事件或者因爲租約到期的刪除事件,則客戶端獲得鎖資源。在圖中,客戶端 A 執行完事務,釋放鎖資源執行 DELETE 操作,客戶端 B 即獲得鎖資源。

5)執行業務

客戶端在獲得鎖資源後,執行業務邏輯。

6)獲得鎖

完成業務流程後對應的 key 釋放鎖。

4、總結

ETCD 作爲持久化的 key-value 鍵值系統,以及基於 Raft 協議的分佈式系統數據一致性提供者,已經廣泛用於分佈式系統中的共享配置、服務發現等場景。相比於 Zookeeper,ETCD 安裝部署更爲簡單、架構更爲穩定可靠,支持多版本併發控制,能夠提供高性能和高吞吐的數據訪問,已被 Kubernetes 用於核心的存儲引擎。本文主要介紹了 ETCD 的基礎架構以及讀寫流程、基於 Raft 的日誌複製和 Watcher 這兩個關鍵特性、租約和數據版本機制、數據存儲的模型,最後簡要介紹了典型的使用場景。

參考資料:

  1. https://etcd.io/docs/v3.5/

  2. https://www.cnblogs.com/traditional/p/9445930.html

  3. ETCD 實戰培訓,https://time.geekbang.org/column/intro/100069901

  4. https://blog.csdn.net/songfeihu0810232/article/details/123786357

  5. https://www.infoq.cn/article/etcd-interpretation-application-scenario-implement-principle

  6. https://developer.aliyun.com/article/738563

  7. https://blog.csdn.net/qq_34556414/article/details/125582162

  8. https://blog.csdn.net/u012588879/article/details/119135021

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Byh5gtBdSJVXZzLGxQouXw