> 本篇內容主要來源於自己學習的視頻,如有侵權,請聯繫刪除,謝謝。 上一節我們學習了 etcd 讀請求執行流程,這一節,我們來學習 etcd 寫請求執行流程。 ### 1、etcd寫請求概覽 **etcd 一個寫請求執行流程又是怎樣的呢?** ``` sh etcdctl put hello wor ...
本篇內容主要來源於自己學習的視頻,如有侵權,請聯繫刪除,謝謝。
上一節我們學習了 etcd 讀請求執行流程,這一節,我們來學習 etcd 寫請求執行流程。
1、etcd寫請求概覽
etcd 一個寫請求執行流程又是怎樣的呢?
etcdctl put hello world ‐‐endpoints 192.168.65.210:2379
執行流程:
-
1、首先 client 端通過負載均衡演算法選擇一個 etcd 節點,發起 gRPC 調用;
-
2、然後 etcd 節點收到請求後經過 gRPC 攔截器、Quota 模塊後,進入
KVServer
模塊; -
3、KVServer 模塊向 Raft 模塊提交一個提案,提案內容為“大家好,請使用 put 方法執行一個 key 為 hello,value 為 world 的命令”。
-
4、隨後此提案通過
Raft HTTP
網路模塊轉發、經過集群多數節點持久化後,狀 態會變成已提交; -
5、etcdserver 從 Raft 模塊獲取已提交的日誌條目,傳遞給 Apply 模塊
-
6、Apply 模塊通過 MVCC 模塊執行提案內容,更新狀態機。
與讀流程不一樣的是,寫流程還涉及 Quota、WAL、Apply
三個模塊。etcd 的 crash-safe 及冪等性
也正是基於 WAL 和 Apply 流程的 consistent index 等
實現的。
2、詳細步驟解讀
2.1、Quota 模塊
流程二
Quota 模塊
主要用於檢查下當前 etcd db 大小加上你請求的 key-value 大小之和是否超過 了配額(quota-backend-bytes)
。 如果超過了配額,它會產生一個告警(Alarm)請求
,告警類型是 NO SPACE
,並通過 Raft 日誌
同步給其它節點,告知 db 無空間了,並將告警持久化存儲到 db 中。最終,無論 是 API 層 gRPC 模塊還是負責將 Raft 側已提交的日誌條目應用到狀態機的 Apply 模塊, 都拒絕寫入,集群只讀
。
常見的 etcdserver: mvcc: database space exceeded
錯誤就是因為Quota 模塊檢測到 db 大小超限
導致的。哪些情況會觸發這個錯誤:
-
一方面
預設 db 配額僅為 2G
,當你的業務數據、寫入 QPS、Kubernetes 集群規模增大後,你的 etcd db 大小就可能會超過 2G。 -
另一方面 etcd 是個 MVCC 資料庫,
保存了 key 的歷史版本
,當你未配置壓縮策略的時候,隨著數據不斷寫入,db 大小會不斷增大,導致超限。
解決辦法:
1、首先當然是調大配額,etcd 社區建議不超過 8G。
如果填的是個小於 0 的數,就會禁用配額功能,這可能會讓db 大小處於失控,導致性能下降,不建議你禁用配額。
2、檢查 etcd 的壓縮(compact)配置是否開啟、配置是否合理。
壓縮時只會給舊版本Key打上空閑(Free)標記,後續新的數據寫入的時候可復用這塊空間,db大小並不會減小。
如果需要回收空間,減少 db 大小,得使用
碎片整理 (defrag)
, 它會遍歷舊的 db 文件數據,寫入到一個新的 db 文 件。但是它對服務性能有較大影響,不建議你在生產集群頻繁使 用。
調整後還需要手動發送一個取消告警(etcdctl alarm disarm)的命令,以消除所有告警, 否則因為告警的存在,集群還是無法寫入。
這個註意別忘記了!!!
查看狀態
etcdctl --endpoints=192.168.91.68:2379,192.168.91.68:12379,192.168.91.68:22379 endpoint status -w table
壓縮
etcdctl --endpoints=192.168.91.68:2379,192.168.91.68:12379,192.168.91.68:22379 compact 1
碎片整理
etcdctl --endpoints=192.168.91.68:2379,192.168.91.68:12379,192.168.91.68:22379 defrag
etcdctl alarm disarm
2.2、KVServer 模塊
流程3
通過流程二的配額檢查後,請求就從 API 層轉發到了流程三的 KVServer 模塊的 put 方 法
。
KVServer 模塊主要功能為:
1、打包提案
: 將 put 寫請求內容打包成一個提案消息,提交給 Raft 模塊
2、請求限速、檢查
: 不過在提交提案前,還有限速、鑒權和大包檢查
。
2.2.1、Preflight Check
為了保證集群穩定性,避免雪崩,任何提交到 Raft 模塊的請求,都會做一些簡單的限速判斷。
限速
- 如果 Raft 模塊
已提交的日誌索引(committed index)
比已應用到狀態機的日誌索引 (applied index)
超過了5000
,那麼它就返回一個etcdserver: too many requests
錯誤給 client。
鑒權
- 然後它會嘗試去獲取請求中的鑒權信息,若使用了密碼鑒權、請求中攜帶了 token,如果 token 無效,則返回
auth: invalid auth token
錯誤給 client。
大包檢查
- 其次它會檢查你寫入的包大小是否超過預設的
1.5MB
, 如果超過了會返回etcdserver: request is too large
錯誤給 client。
2.3、Propose
流程4
通過檢查後會生成一個唯一的 ID
,將此請求關聯到一個對應的消息通知 channel(用於接收結果)
,然後向 Raft 模塊發起(Propose)
一個提案(Proposal)
。 Raft 模塊發起提案後,KVServer 模塊會等待此 put 請求,等待寫入結果通過消息通知 channel 返回或者超時。
etcd 預設超時時間是 7 秒(5 秒磁碟 IO 延時 +2*1 秒競選超時 時間)
,如果一個請求超時未返回結果,則可能會出現你熟悉的etcdserver: request timed out
錯誤。
2.4 WAL 模塊
流程5
Raft 模塊收到提案後,如果當前節點是 Follower,它會轉發給 Leader,只有 Leader 才能 處理寫請求
。
Leader 收到提案後,通過 Raft 模塊輸出待轉發給 Follower 節點的消息和待持久化的日誌 條目,日誌條目則封裝了我們上面所說的 put hello 提案內容。
etcdserver 從 Raft 模塊獲取到以上消息和日誌條目後,作為 Leader,它會將 put 提案消 息廣播給集群各個節點,同時需要把集群 Leader 任期號、投票信息、已提交索引、提案內 容持久化到一個 WAL(Write Ahead Log)日誌文件
中,用於保證集群的一致性、可恢復 性,也就是我們圖中的流程五模塊。
2.4.1、WAL 日誌結構
WAL 日誌結構如下:
WAL 文件它由多種類型的 WAL 記錄順序追加寫入
組成,每個記錄由類型、數據、迴圈冗 餘校驗碼組成
。不同類型的記錄通過 Type 欄位區分,Data 為對應記錄內容,CRC 為迴圈 校驗碼信息
。
WAL 記錄類型目前支持 5 種,分別是文件元數據記錄、日誌條目記錄、狀態信息記錄、 CRC 記錄、快照記錄
:
-
文件元數據記錄:包含節點 ID、集群 ID 信息,它
在 WAL 文件創建的時候 寫入
; -
日誌條目記錄:包含 Raft 日誌信息,如 put 提案內容;
-
狀態信息記錄:包含集群的任期號、節點投票信息等,
一個日誌文件中會有 多條,以最後的記錄為準
; -
CRC 記錄:包含上一個 WAL 文件的最後的 CRC(迴圈冗餘校驗碼)信息, 在創建、切割 WAL 文件時,作為第一條記錄寫入到新的 WAL 文件, 用於校驗數據 文件的完整性、準確性等;
-
快照記錄:包含快照的任期號、日誌索引信息,用於檢查快照文件的準確性。
2.4.2、WAL 持久化
首先會將 put 請求封裝成一個 Raft 日誌條目,Raft 日誌條目的數據結構信息如下:
type Entry struct {
Term uint64 `protobuf:"varint,2,opt,name=Term" json:"Term"`
Index uint64 `protobuf:"varint,3,opt,name=Index" json:"Index"`
Type EntryType `protobuf:"varint,1,opt,name=Type,enum=Raftpb.EntryType" json:"Type"`
Data []byte `protobuf:"bytes,4,opt,name=Data" json:"Data,omitempty"`
}
它由以下欄位組成:
Term
是 Leader 任期號,隨著 Leader 選舉增加;Index
是日誌條目的索引,單調遞增增加;Type
是日誌類型,比如是普通的命令日誌(EntryNormal),還是集群配置變更日誌(EntryConfChange);Data
保存我們上面描述的 put 提案內容。
具體持久化過程如下:
1、它首先先將 Raft 日誌條目內容(含任期號、索引、提案內容)序列化後保存到 WAL 記錄的 Data 欄位, 然後計算 Data 的 CRC 值,設置 Type 為 Entry Type, 以上信息就組成了一個完整的 WAL 記錄。
2、最後計算 WAL 記錄的長度,順序先寫入 WAL 長度(Len Field),然後寫 入記錄內容,調用 fsync 持久化到磁碟
,完成將日誌條目保存到持久化存儲中。
3、當一半以上節點持久化此日誌條目後, Raft 模塊就會通過 channel 告知 etcdserver 模塊,put 提案已經被集群多數節點確認,提案狀態為已提交,你可以執 行此提案內容了。
4、於是進入流程六
,etcdserver 模塊從 channel 取出提案內容,添加到先進 先出(FIFO)調度隊列,隨後通過 Apply 模塊按入隊順序,非同步、依次執行提案內 容。
2.5、Apply 模塊
流程 7
Apply 模塊主要用於執行處於 已提交狀態的提案,將其更新到狀態機
。
Apply 模塊在執行提案內容前,首先會判斷當前提案是否已經執行過了,如果執行了則直 接返回,若未執行同時無 db 配額滿告警,則進入到 MVCC 模塊,開始與持久化存儲模塊 打交道。
如果執行過程中 crash,重啟後如何找回異常提案,再次執行的呢?
主要依賴 WAL 日誌,因為提交給 Apply 模塊執行的提案已獲得多數節點確認、持久化, etcd 重啟時,會從 WAL 中解析出 Raft 日誌條目內容,追加到 Raft 日誌的存儲中,並重 放已提交的日誌提案給 Apply 模塊執行。
重啟恢復時,如何確保冪等性,防止提案重覆執行導致數據混亂呢?
etcd 通過引入一個
consistent index 的欄位
,來存儲系統當前已經執行過的日誌條目索 引,實現冪等性。因為 Raft 日誌條目中的索引(index)欄位,而且是全局單調遞增的,每個日誌條目索引 對應一個提案。 如果一個命令執行後,我們在 db 裡面也記錄下當前已經執行過的日誌條 目索引,就可以解決冪等性問題了。當然
還需要將執行命令和記錄index這兩個操作作為原子性事務提交,才能實現冪等
。
2.6、MVCC 模塊
流程 8 和 9
MVCC 主要由兩部分組成,一個是記憶體索引模塊 treeIndex,保存 key 的歷史版本號信 息
,另一個是 boltdb 模塊,用來持久化存儲 key-value 數據
。
MVCC 模塊執行 put hello 為 world 命令時,它是如何構建記憶體索引和保存哪些數據到 db呢?
2.6.1、treeIndex
MVCC 寫事務在執行 put hello 為 world 的請求時,會基於 currentRevision 自增生成新 的 revision
, 如{2,0},然後從 treeIndex 模塊中查詢 key 的創建版本號、修改次數信息。這 些信息將填充到 boltdb 的 value 中,同時將用戶的 hello key 和 revision 等信息存儲到 B-tree,也就是下麵簡易寫事務圖的流程一,整體架構圖中的流程八。
hello: revision{2,0}
這裡的 2,0 具體指的是什麼呢?
這裡不太懂,有清楚的朋友,請不吝賜教。
2.6.2、boltdb
MVCC 寫事務自增全局版本號後生成的 revision{2,0},它就是 boltdb 的 key
,通過它就 可以往 boltdb 寫數據了,進入了整體架構圖中的流程九。
那麼寫入 boltdb 的 value 含有哪些信息呢?
寫入 boltdb 的 value, 並不是簡單的"world",如果只存一個用戶 value,索引又是保存 在易失的記憶體上,那重啟 etcd 後,我們就丟失了用戶的 key 名,無法構建 treeIndex 模塊 了。
因此為了構建索引和支持 Lease(租約) 等特性,etcd 會持久化以下信息:
-
key 名稱;
-
key 創建時的版本號(create_revision)、最後一次修改時的版本號 (mod_revision)、key 自身修改的次數(version);
-
value 值;
-
租約信息。
boltdb value 的值就是將含以上信息的結構體序列化成的二進位數據,然後通過 boltdb 提供的 put 介面,etcd 就快速完成了將你的數據寫入 boltdb。
註意: 在以上流程中,etcd 並未提交事務(commit),因此數據只更新在 boltdb 所管理 的記憶體數據結構中。
事務提交的過程,包含 B+tree 的平衡、分裂,將 boltdb 的臟數據(dirty page)、元數 據信息刷新到磁碟,因此事務提交的開銷是昂貴的。
如果我們每次更新都提交事務,etcd寫性能就會較差。
etcd 的解決方案是合併再合併:
- 首先 boltdb key 是版本號,put/delete 操作時,都會基於當前版本號遞增生成新的版本 號,因此屬於順序寫入,可以調整 boltdb 的 bucket.FillPercent 參數,使每個 page 填充 更多數據,減少 page 的分裂次數並降低 db 空間。
- 其次 etcd 通過合併多個寫事務請求,通常情況下,是非同步機制定時(預設每隔 100ms) 將批量事務一次性提交(pending 事務過多才會觸發同步提交), 從而大大提高吞吐量。
但是這優化又引發了另外的一個問題, 因為事務未提交,讀請求可能無法從 boltdb 獲取 到最新數據
。 - 為瞭解決這個問題,etcd 引入了一個 bucket buffer 來保存暫未提交的事務數據。在更新 boltdb 的時候,etcd 也會同步數據到 bucket buffer。
因此 etcd 處理讀請求的時候會優 先從 bucket buffer 裡面讀取,其次再從 boltdb 讀,通過 bucket buffer 實現讀寫性能提 升,同時保證數據一致性。
以上就是 etcd 寫請求執行的流程,自己還是有蠻多地方不太懂,常看常新吧。
學習鏈接