雲原生K8S精選的分散式可靠的鍵值儲存etcd原理和實踐

2023-03-23 06:00:34

@

概述

定義

etcd 官網地址 https://etcd.io/ 最新版本3.5.7

etcd 官網檔案地址 https://etcd.io/docs/v3.5/

etcd 原始碼地址 https://github.com/etcd-io/etcd

etcd是一個強一致、可靠的分散式鍵值儲存,使用Go語言開發(docker和k8s也是),其提供可靠的分散式鍵值(key-value)儲存、設定共用和服務發現等功能,即使在叢集腦裂網路分割區情況下也可以優雅地處理leader選舉;官方上有明確說明etcd是一個CNCF專案。可以說,etcd 已經成為了雲原生和分散式系統的儲存基石。

應用場景

分散式系統中的資料分為控制資料和應用資料。etcd的使用場景預設處理的資料都是控制資料,對於應用資料,只推薦資料量很小,但是更新存取頻繁的情況。應用場景有如下幾類

  • 鍵值儲存的設定管理
  • 服務註冊與發現
  • 訊息釋出與訂閱
  • 負載均衡
  • 分散式通知與協調
  • 分散式鎖、分散式佇列
  • 叢集監控與Leader選舉

如果需要一個分散式儲存倉庫來儲存設定資訊,並且希望這個倉庫讀寫速度快、支援高可用、部署簡單、支援http介面,那麼就可以使用雲原生專案etcd。

特性

  • 介面簡潔:使用標準HTTP工具(如curl)讀取和寫入值。
  • KV儲存:將資料儲存在按層次結構組織的目錄中,就像在標準檔案系統中一樣。
  • 監聽變化:觀察特定鍵或目錄的變化,並對值的變化做出反應。
  • 可靠:通過Raft協定實現分散式功能。
  • 安全:可選SSL使用者端證書認證,用於金鑰過期的可選ttl。
  • 快速:基準測試為10,000寫入/秒。

為何使用etcd

etcd實現的絕大多數功能Zookeeper都能實現,那為何還要用etcd?相較之下,Zookeeper有如下缺點:

  • 複雜:Zookeeper的部署維護複雜,管理員需要掌握一系列的知識和技能;而Paxos強一致性演演算法也是素來以複雜難懂而聞名於世;另外,Zookeeper的使用也比較複雜,需要安裝使用者端,官方只提供了java和C兩種語言的介面。
  • Java編寫:Java本身就偏向於重型應用,它會引入大量的依賴;而運維人員則普遍希望機器叢集儘可能簡單,維護起來也不易出錯。
  • 發展緩慢:Apache基金會專案特有的「Apache Way」在開源界飽受爭議,其中一大原因就是由於基金會龐大的結構以及鬆散的管理導致專案發展緩慢。

而etcd作為一個後起之秀,對比Zookeeper其優點如下

  • 簡單:使用Go語言編寫部署簡單;使用HTTP作為介面使用簡單;使用Raft演演算法保證強一致性讓使用者易於理解。
  • 資料持久化:etcd預設資料一更新就進行持久化。
  • 安全:etcd支援SSL使用者端安全認證。

etcd作為一個年輕的專案,正在高速迭代和開發中,這既優點也是缺點。優點在於它的未來具有無限的可能性,缺點是版本的迭代導致其使用的可靠性無法保證,無法得到大專案長時間使用的檢驗。但由於CoreOS、Kubernetes和Cloudfoundry等知名專案均在生產環境中使用了etcd,所以總的來說etcd值得去嘗試。

術語

  • Alarm:當叢集需要操作員干預以保持可靠性時,etcd伺服器就會發出警報
  • Authentication:認證管理etcd資源的使用者存取許可權。
  • client:使用者端連線到etcd叢集以發出服務請求,例如獲取鍵-值對、寫入資料或監視更新。
  • Cluster:叢集由幾個成員組成;每個成員中的節點遵循raft共識協定進行紀錄檔複製。叢集接收來自成員的提案,提交併申請到本地儲存。
  • Compaction:壓縮將丟棄給定修訂之前的所有etcd事件歷史記錄和被取代的鍵。它用於回收etcd後端資料庫中的儲存空間。Election
  • Election:作為共識協定的一部分,etcd叢集在其成員之間舉行選舉,以選擇領導人。
  • Endpoint:指向etcd服務或資源的URL。
  • Key:用於在etcd中儲存和檢索使用者定義值的使用者定義識別符號。
  • Key range:一組鍵,其中包含單個鍵、所有x的詞法間隔(A < x <= b)或所有大於給定鍵的鍵。
  • Keyspace:etcd叢集中所有鍵的集合。
  • Lease:一種短期可再生合同,相當於租期,到期時刪除與其相關的key。
  • Member:參與服務etcd叢集的邏輯etcd伺服器。
  • Modification Revision:儲存對給定鍵的最後一次寫操作的第一個修訂。
  • Peer:Peer是同一叢集的另一個成員。
  • Proposal:提案是需要通過Raft協定的請求(例如寫請求、設定更改請求)。
  • Quorum:修改叢集狀態所需的協商一致的活動成員數量。Etcd要求會員過半數才能達到法定人數。
  • Revision:64位元叢集範圍的計數器,從1開始,每次修改keyspace時遞增。
  • Role:許可權單位,一組key範圍內的許可權單位,可授予一組使用者進行存取控制。
  • Snapshot:etcd叢集狀態的時間點備份。
  • Store:支援叢集keyspace的物理儲存。
  • Transaction:一組原子執行的操作。事務中的所有修改鍵共用相同的修改修訂。
  • Key Version:自建立key以來對其進行寫操作的次數,從1開始。不存在或已刪除的金鑰的版本號為0。
  • Watcher:使用者端開啟一個監視器來觀察給定鍵範圍的更新。

架構

etcd按照分層模型可分為 Client 層、API 網路層、Raft 演演算法層、邏輯層和儲存層。各層功能如下:

  • Client 層:Client 層包括 client v2 和 v3 兩個大版本 API 使用者端庫,提供了簡潔易用的 API,同時支援負載均衡、節點間故障自動轉移,可極大降低業務使用 etcd 複雜度,提升開發效率、服務可用性。

  • API 網路層:API 網路層主要包括 client 存取 server 和 server 節點之間的通訊協定。一方面,client 存取 etcd server 的 API 分為 v2 和 v3 兩個大版本。v2 API 使用 HTTP/1.x 協定,v3 API 使用 gRPC 協定。同時 v3 通過 etcd grpc-gateway 元件也支援 HTTP/1.x 協定,便於各種語言的服務呼叫。另一方面,server 之間通訊協定,是指節點間通過 Raft 演演算法實現資料複製和 Leader 選舉等功能時使用的 HTTP 協定。etcdv3版本中client 和 server 之間的通訊,使用的是基於 HTTP/2 的 gRPC 協定。相比 etcd v2 的 HTTP/1.x,HTTP/2 是基於二進位制而不是文字、支援多路複用而不再有序且阻塞、支援資料壓縮以減少包大小、支援 server push 等特性。因此,基於 HTTP/2 的 gRPC 協定具有低延遲、高效能的特點,有效解決etcd v2 中 HTTP/1.x 效能問題。

  • Raft 演演算法層:Raft 演演算法層實現了 Leader 選舉、紀錄檔複製、ReadIndex 等核心演演算法特性,用於保障 etcd 多個節點間的資料一致性、提升服務可用性等,是 etcd 的基石和亮點。

  • 功能邏輯層:etcd 核心特性實現層,如典型的 KVServer 模組、MVCC 模組、Auth 鑑權模組、Lease 租約模組、Compactor 壓縮模組等,其中 MVCC 模組主要由 treeIndex(記憶體樹形索引) 模組和 boltdb(嵌入式的 KV 持久化儲存庫) 模組組成。treeIndex 模組使用B-tree 資料結構來儲存使用者 key 和版本號的對映關係,使用B-tree是因為etcd支援範圍查詢,使用hash表不適合,從效能上看,B-tree相對於二元樹層級較矮,效率更高;boltdb是個基於 B+ tree 實現的 key-value 鍵值庫,支援事務,提供 Get/Put 等簡易 API 給 etcd 操作。

  • 儲存層:儲存層包含預寫紀錄檔 (WAL) 模組、快照 (Snapshot) 模組、boltdb 模組。其中 WAL 可保障 etcd crash 後資料不丟失,boltdb 則儲存了叢集後設資料和使用者寫入的資料。

原理

etcd 是典型的讀多寫少儲存,在我們實際業務場景中,讀一般佔據 2/3 以上的請求。

  • 讀請求:使用者端通過負載選擇一個etcd節點發出讀請求,API介面層提供Range RPC方法,etcd伺服器端攔截gRPC 讀請求後呼叫的處理請求。

  • 寫請求:使用者端通過負載均衡選擇一個etcd節點發起請求etcd伺服器端攔截gRPC寫請求,涉及校驗和監控後KVServer向raft模組發起提案,內容寫入資料命令,經過網路轉發,當叢集中多數節點達成一致持久化資料後,狀態變更MVCC模組執行提案內容。

讀操作

etcd使用者端工具通過etcdctl執行一個讀命令,解析完請求中的引數建立clientv3 庫物件,然後通過EndPoint列表使用Round-Robin負載均衡演演算法選擇一個etcd server節點,呼叫 KVServer API模組基於 HTTP/2 的 gRPC 協定的把請求傳送給 etcd server,攔截器攔截,主要做一些校驗和監控,然後呼叫KVserver模組的Range介面獲取資料。讀操作的核心步驟:

  • 線性讀ReadIndex模組
  • MVCC(包含treeindex和BlotDB)模組

線性讀是相對序列讀來講的概念,叢集模式下會有多個etcd節點,不同節點間可能存在一致性的問題。序列讀直接返回狀態資料,不需要與叢集中其他節點互動。這種方式速度快,開銷小,但是會存在資料不一致的情況。

線性讀則需要叢整合員之間達成共識,存在開銷,響應速度相對慢,但是能保證資料的一致性,etcd預設的讀模式線性讀。

etcd中查詢請求,查詢單個鍵或者一組鍵及查詢數量,到底層實際會呼叫Range keys方法。

流程如下:

  • 在treeIndex中根據鍵利用BTree快速查詢該鍵對應索引項KeyIndex,索引項中包含Revison
  • 根據查詢到的版本號資訊Revision,在Backend的快取Buffer中用二分法查詢,如命中則直接返回
  • 若快取中不符合條件,在BlotDB中查詢,(基於BlotDB的索引),查詢後返回鍵值對的資訊。

ReadTx和BatchTx是兩個幾口,用於讀寫請求建立Backend結構體,預設也會建立readTx和batchTx。readTx實現了ReadTx,負責處理唯讀請求batchTx,實現了BatchTx介面,負責處理讀寫請求。

對於上層的鍵值儲存,它會利用返回的Revision從正真的儲存資料中的BoltDB中,查詢當前key對應的Revsion資料。BoltDB內部用類似buctket的方式儲存對應MySQL中的表結構,使用者key資料存放bucket的名字是key etcd mvcc後設資料存放bucket的meta。

核心模組的功能:

  • KVServer
    • 序列讀:狀態機資料返回、無需通過 Raft 協定與叢集進行互動。它具有低延時、高吞吐量的特點,適合對資料一致性要求不高的場景。
    • 線性讀:etcd 預設讀模式是線性讀,在延時和吞吐量上相比序列讀略差一點,適用於對資料一致性要求高的場景。
    • 當收到一個線性讀請求時,它首先會從 Leader 獲取叢集最新的已提交的紀錄檔索引。
      Leader 收到 ReadIndex 請求時,為防止腦裂等異常場景,會向 Follower 節點傳送心跳確認,一半以上節點確認 Leader 身份後才能將已提交的索引 (committed index) 返回給節點。節點則會等待,直到狀態機已應用索引 (applied index) 大於等於 Leader 的已提交索引時 (committed Index),然後去通知讀請求,資料已趕上 Leader,你可以去狀態機中存取資料了。
  • MVCC
    • 多版本並行控制 (Multiversion concurrency control) 模組是為了解決 etcd v2 不支援儲存 key 的歷史版本、不支援多 key 事務等問題而產生的。
    • etcd儲存一個key的多個歷史版本的方案為:每次修改操作,生成一個新的版本號 (revision),以版本號為 key, value 為使用者 key-value 等資訊組成的結構體。
  • treeIndex
    • 基於btree庫實現,只儲存使用者的 key 和相關版本號資訊。而用於的key,value資料則儲存在boltdb裡面,相比於etcd v2 全記憶體儲存,etcd v3 對記憶體要求更低。
  • buffer
    • 並不是所有請求都一定要從 boltdb 獲取資料。etcd 出於資料一致性、效能等考慮,在存取 boltdb 前,首先會從一個記憶體讀事務 buffer 中,二分查詢你要存取 key 是否在 buffer 裡面,若命中則直接返回。
  • boltdb
    • 若 buffer 未命中,此時就真正需要向 boltdb 模組查詢資料了。

寫操作

  • 使用者端通過負載均衡演演算法選擇一個etcd節點,發起gRPC呼叫。
  • etcd Server收到使用者端請求。
  • 經過gRPC攔截,Quota校驗,Quota模組用於校驗etcd db檔案大小是否超過了配額。
  • KVserver模組將請求傳送給本模組的raft,負責與etcd raft模組進行通訊,發起一個提案,命令為put foo bar,即使用put方法將foo更新為bar。
  • 提案經過轉發之後,半數節點成功持久化。
  • MVCC模組更新狀態機。

寫操作涉及核心模組功能如下:

  • Quoto模組

    • client 端發起 gRPC 呼叫到 etcd 節點,和讀請求不一樣的是,寫請求需要經過流程二 db 配額(Quota)模組。
    • 當 etcd server 收到 put/txn 等寫請求的時候,會首先檢查下當前 etcd db 大小加上你請求的 key-value 大小之和是否超過了配額(quota-backend-bytes)。如果超過了配額,它會產生一個告警(Alarm)請求,告警型別是 NO SPACE,並通過 Raft 紀錄檔同步給其它節點,告知 db 無空間了,並將告警持久化儲存到 db 中。
    • 配額為'0'表示使用 etcd 預設的 2GB 大小,可以根據業務常見進行調優。etcd社群建議不超過8G。如果填小於0的數,表示禁用配額功能,但這會讓db大小處於失控狀態,導致效能下降,所以不建議使用。
  • KVServer模組

    • etcd 是基於 Raft 演演算法實現節點間資料複製的,因此它需要將 put 寫請求內容打包成一個提案訊息,提交給 Raft 模組。
  • WAL模組

    • Raft 模組收到提案後,如果當前節點是 Follower,它會轉發給 Leader,只有 Leader 才能處理寫請求。Leader 收到提案後,通過 Raft 模組輸出待轉發給 Follower 節點的訊息和待持久化的紀錄檔條目,紀錄檔條目則封裝了提案內容。
  • Apply模組

    • put請求如果在執行提案內容的時候crash了,重啟恢復的時候,會從 WAL 中解析出 Raft 紀錄檔條目內容,追加到 Raft 紀錄檔的儲存中,並重放已提交的紀錄檔提案給 Apply 模組執行。
    • etcd 是個 MVCC 資料庫,每次更新都會生成新的版本號。如果沒有冪等性保護,同樣的命令,一部分節點執行一次,一部分節點遭遇異常故障後執行多次,則系統的各節點一致性狀態無法得到保證,導致資料混亂,這是嚴重故障。
    • Raft 紀錄檔條目中的索引(index)欄位是全域性單調遞增的,每個紀錄檔條目索引對應一個提案,在 db 裡面也記錄下當前已經執行過的紀錄檔條目索引。
  • MVCC模組

    • Apply 模組判斷此提案未執行後,就會呼叫 MVCC 模組來執行提案內容。MVCC 主要由兩部分組成,一個是記憶體索引模組 treeIndex,儲存 key 的歷史版本號資訊,另一個是 boltdb 模組,用來持久化儲存 key-value 資料。

紀錄檔複製

紀錄檔由一個個遞增的有序序號索引標識。Leader維護了所有Follow節點的紀錄檔複製進度,在新增一個紀錄檔後,會將其廣播給所有Follow節點。Follow節點處理完成後,會告知Leader當前已複製的最大紀錄檔索引。Leader收到後,會計算被一半以上節點複製過的最大索引位置,標記為已提交位置,在心跳中告訴Follow節點。只有被提交位置以前的紀錄檔才會應用到儲存狀態機。

部署

單範例快速部署

在本地安裝、執行和測試etcd的單成員叢集,部署詳細可以檢視下上一篇《雲原生API閘道器全生命週期管理Apache APISIX探究實操》中有關於etcd單節點部署,單節點部署完畢後驗證讀寫和檢視版本資訊如下:

多範例叢集部署

靜態地啟動etcd叢集要求叢集中的每個成員都認識叢集中的其他成員;但通常叢整合員的ip可能事先未知,可以通過發現服務引導etcd叢集。在生產環境中,為了整個叢集的高可用,etcd 正常都會叢集部署,避免單點故障。引導 etcd 叢集的啟動有以下三種機制:

  • 靜態
  • etcd 動態發現
  • DNS 發現

靜態

在部署之前已經知道了叢整合員、它們的地址和叢集的大小,name可以通過設定initial-cluster標誌來使用離線引導設定。分別在各個節點上執行下面語句

etcd --name infra1 --initial-advertise-peer-urls http://192.168.3.111:2380 \
  --listen-peer-urls http://192.168.3.111:2380 \
  --listen-client-urls http://192.168.3.111:2379,http://127.0.0.1:2379 \
  --advertise-client-urls http://192.168.3.111:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster infra1=http://192.168.3.111:2380,infra2=http://192.168.3.112:2380,infra3=http://192.168.3.113:2380 \
  --initial-cluster-state new
  
etcd --name infra2 --initial-advertise-peer-urls http://192.168.3.112:2380 \
  --listen-peer-urls http://192.168.3.112:2380 \
  --listen-client-urls http://192.168.3.112:2379,http://127.0.0.1:2379 \
  --advertise-client-urls http://192.168.3.112:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster infra1=http://192.168.3.111:2380,infra2=http://192.168.3.112:2380,infra3=http://192.168.3.113:2380 \
  --initial-cluster-state new
  
etcd --name infra3 --initial-advertise-peer-urls http://192.168.3.113:2380 \
  --listen-peer-urls http://192.168.3.113:2380 \
  --listen-client-urls http://192.168.3.113:2379,http://127.0.0.1:2379 \
  --advertise-client-urls http://192.168.3.113:2379 \
  --initial-cluster-token etcd-cluster-1 \
  --initial-cluster infra1=http://192.168.3.111:2380,infra2=http://192.168.3.112:2380,infra3=http://192.168.3.113:2380 \
  --initial-cluster-state new  

也可以通過nohup &後臺啟動etcd,獲取叢集的member資訊

etcdctl --endpoints=192.168.5.52:2379 member list

etcd 動態發現

# 建立紀錄檔目錄
mkdir /var/log/etcd
# 建立資料目錄
mkdir /data/etcd
mkdir /home/commons/data/etcd

發現URL標識唯一的etcd叢集。每個etcd範例共用一個新的發現URL來引導新叢集,而不是重用現有的發現URL。如果沒有可用的現有叢集,則使用discovery.etc .io託管的公共發現服務。要使用「new」端點建立一個私有發現URL,使用命令:

# 通過curl生成
curl https://discovery.etcd.io/new?size=3
https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200
# 通過上面返回組裝
ETCD_DISCOVERY=https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200
--discovery https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200

分別在各個節點上執行下面語句

etcd --name myectd1 --data-dir /home/commons/data --initial-advertise-peer-urls http://192.168.5.111:2380 \
  --listen-peer-urls http://192.168.5.111:2380 \
  --listen-client-urls http://192.168.5.111:2379,http://127.0.0.1:2379 \
  --advertise-client-urls http://192.168.5.111:2379 \
  --discovery https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200
etcd --name myectd2 --data-dir /home/commons/data --initial-advertise-peer-urls http://192.168.5.112:2380 \
  --listen-peer-urls http://192.168.5.112:2380 \
  --listen-client-urls http://192.168.5.112:2379,http://127.0.0.1:2379 \
  --advertise-client-urls http://192.168.5.112:2379 \
  --discovery https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200
etcd --name myectd3 --data-dir /home/commons/data --initial-advertise-peer-urls http://192.168.5.113:2380 \
  --listen-peer-urls http://192.168.5.113:2380 \
  --listen-client-urls http://192.168.5.113:2379,http://127.0.0.1:2379 \
  --advertise-client-urls http://192.168.5.113:2379 \
  --discovery https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200

常見命令

#寫入KV
etcdctl put /key1 value1
etcdctl put /key2 value2
etcdctl put /key3 value3
# 範圍,左閉右開
etcdctl get /key1 /key3
# 以十六進位制格式讀取key foo值的命令:
etcdctl get /key1 --hex
# 僅列印value
etcdctl get /key1 --print-value-only
# 字首匹配和返回條數
etcdctl get --prefix /key --limit 2
# 按照key的字典順序讀取,大於或等於
etcdctl get --from key /key1
# 監聽key,可以獲取key變更資訊
etcdctl watch /key1
# 重新修改
etcdctl put /key1 value111
# 讀取版本
etcdctl get /key1 --rev=5
# 刪除key
etcdctl del /key3
# 租約,例如授予60秒生存時間的租約
etcdctl lease grant 60
lease 5ef786eee44b831d granted with TTL(60s)
# 寫入帶租約
etcdctl put --lease=5ef786eee44b831d /key4 value4
# 復原租約
etcdctl lease revoke 32695410dcc0ca06
# 授權建立角色
etcdctl role add testrole
etcdctl role list
etcdctl role grant-permission testrole read /permission
etcdctl role revoke-permission testrole /permission
etcdctl role del testrole
# 授權建立使用者
etcdctl user add testuser
etcdctl user list
etcdctl user passwd
etcdctl user get testuser
etcdctl user del testuser
etcdctl user grant-role testuser testrole
# 建立測試賬號2
etcdctl role add testrole2
etcdctl role grant-permission testrole2 readwrite /permission
etcdctl user add testuser2
etcdctl user grant-role testuser2 testrole2

#1. 新增root角色
etcdctl role add root
#2. 新增root使用者
etcdctl user add root  
#3. 給root使用者授予root角色
etcdctl user grant-role root root
#4.啟用auth
etcdctl auth enable
etcdctl put /permission all2 --user=testuser2
etcdctl get /permission --user=testuser2
etcdctl get /permission --user=testuser
etcdctl put /permission allhello --user=testuser

# 直接帶上密碼
etcdctl --user='testuser2' --password='123456' put /permission all2 
# 叢集鑑權
etcdctl --endpoints http://192.168.3.111:2379,http://192.168.3.111:2379,http://192.168.3.111:2379 --user=root --password=123456 auth enable
etcdctl --endpoints http://192.168.3.111:2379,http://192.168.3.111:2379,http://192.168.3.111:2379 --user=root:123456 auth enable
  • 本人部落格網站IT小神 www.itxiaoshen.com