etcd 入門:Kubernetes 叢集的記憶
Kubernetes 叢集的所有狀態,都存放在同一個地方:etcd。對有後端經驗的工程師來說,它本質上就是一個資料庫,複製、交易與樂觀鎖這些熟悉的概念,都能在這裡找到對應。
問題是:etcd 靠哪些核心機制成為 Kubernetes 的「唯一事實來源」(Source of Truth)?身為 DevOps 工程師,我們又需要掌握到什麼程度?
本文以 etcd v3.5 與 Kubernetes v1.37 為依據,跳過晦澀的論文級證明與原始碼細節,聚焦於核心機制與維運實務。
為什麼 Kubernetes 需要 etcd
etcd 是一個專為「少量、關鍵、需要跨機器一致」的資料所設計的分散式 Key-Value Store。名稱源自 Unix 的設定檔目錄 /etc 加上代表分散式的 d(Distributed),由 CoreOS 於 2013 年以 Go 語言開發。etcd 的核心使命,就是讓多台機器共同維護「一份大家都認同的真相」,並在資料變更時即時推播通知。
從傳統後端開發的痛點出發,最容易看清 etcd 要解決的難題:
- 單機資料庫:具備完整的 ACID 交易保證,但一旦伺服器硬體故障或當機,整個服務就會中斷,形成單點故障(SPOF)。
- 主從非同步複製:為了高可用性加入 Replica,但在主節點容錯移轉(Failover)時,尚未同步完成的寫入容易遺失。若發生網路分割(Network Partition),被隔開的兩側節點可能各自接受寫入,引發無法仲裁的「腦裂」(Split-Brain)。
- 快取系統:Redis 預設的複製模式同樣為非同步,無法安全地作為分散式系統的唯一事實來源。
etcd 給出的解法是「分散式共識」:任何一筆寫入請求,必須先讓叢集內過半數成員(Quorum)成功持久化到磁碟,才算寫入成功。這條規則同時兼顧了可用性(容許少數節點失效)與一致性(確保永遠只有一個合法狀態)。
圍繞著共識核心,etcd 建構了四大支柱機制:
- Raft 共識演算法:決定誰的寫入與提案算數。
- MVCC 與 Revision:以多版本並行控制(Multi-Version Concurrency Control)記錄每一次資料修改的歷史版本。
- Watch 機制:允許客戶端訂閱變更,並可從指定的歷史版本重播事件。
- Lease 租約:提供帶存活時間(TTL)與心跳續約的基礎機制。
Kubernetes 之所以選擇 etcd,是因為控制平面的所有決策都必須建立在「即時且唯一」的狀態上。從 Pod、Node、Deployment 到 Secret,全部以 Protobuf 編碼保存在 etcd 的 /registry/ 前綴路徑下。
整個 Kubernetes 叢集中,只有 kube-apiserver 會直接讀寫 etcd(透過 --etcd-servers 參數設定;大量事件時亦可用 --etcd-servers-overrides 將 Events 導向獨立的 etcd 叢集)。
在 CAP 定理中,etcd 展現了明確的 CP 傾向(Consistency & Partition Tolerance)。當遭遇網路分割時,它寧可直接中斷少數方的寫入服務,也絕不回傳可能過期的髒資料。
在設計取捨上,etcd 是為了叢集中繼資料(Metadata)而生,而非通用資料庫。其預設的儲存配額(Backend Quota)僅有 2 GiB(--quota-backend-bytes),官方建議一般環境最多不超過 8 GiB。每一次寫入都必須走完共識流程,寫入延遲的上限,取決於過半數節點中最慢的那一台把資料寫入磁碟的時間。
Raft 共識:多數決如何實現高可靠性
Raft 是 etcd 所採用的分散式共識演算法,其初衷是打造一套比老牌共識演算法 Paxos 更直觀、更容易理解的共識模型。理解 Raft 在維運中的運作方式,只需要掌握一個核心原則、三個角色與兩個關鍵流程。
[Raft Write Flow]
Client ---> [ Leader ]
|
|--(1) Append Log Entry--> [ Follower 1 ]
| (Disk OK)
|
|--(2) Append Log Entry--> [ Follower 2 ]
| (Disk OK)
|
v
(Quorum Reached: 2/3)
|
|--(3) Commit & Apply Machine
v
Client <--- [ Response OK ]
一個核心原則
任何狀態修改都必須獲得過半數(Quorum)成員同意。關鍵在於「過半」只可能出現在一邊:3 台節點被網路切成 2 台與 1 台時,只有 2 台那側湊得到多數、能繼續寫入;落單的 1 台無法完成任何寫入。兩邊不可能同時各自過半,也就不會出現兩份互相矛盾的資料,腦裂從規則上就被排除了。
三個角色與兩個流程
叢集在任意時刻至多只有一個 Leader,其餘節點皆為 Follower;當 Follower 發起選舉時,會暫時轉為 Candidate(候選者)。所有寫入請求,都由 Leader 統一受理。
- 流程一:Leader 選舉。Leader 會以固定週期向所有 Follower 傳送心跳(etcd 預設為 100ms)。若 Follower 在選舉逾時時間內(預設為 1000ms,並隨機浮動以防平票)未收到心跳,便會將任期(Term)加一,升級為 Candidate 並發起投票。獲得過半數選票者即成為新任 Leader。在重新選舉的短暫期間內,叢集無法接受任何寫入,客戶端請求會發生 Timeout;etcd 不穩定時常見的逾時錯誤,多半就是這麼來的。
- 流程二:日誌複製(Log Replication)。當 Leader 收到寫入請求後,會先將操作寫入自己的日誌,並平行廣播給所有 Follower。當包含 Leader 在內的過半數節點都將日誌成功寫入磁碟(
fsync)後,該筆寫入才算提交(Commit),隨後套用到狀態機——也就是實際對外服務的 Key-Value 資料——並回覆客戶端。
在讀取方面,etcd 預設採用線性一致讀(Linearizable Read):即便請求發給 Follower,背後也會先向 Leader 確認最新的已提交進度,保證讀到的一定是最新已提交的資料;若追求極致吞吐量,也可改用本地讀(Serializable Read),直接由單一節點本地回應,速度更快但可能讀到短暫落後的舊資料。
容錯算術與節點數奇數原則
台節點的叢集,其容錯能力為:
也就是把 N−1 除以 2 之後無條件捨去——這意味著 3 台節點可容忍 1 台故障,5 台節點可容忍 2 台故障。
| 節點總數() | 達成法定多數(Quorum)需求 | 容許故障節點數 | 容錯說明 |
|---|---|---|---|
| 2 | 2(需 2/2 全體同意) | 0 | 壞 1 台即無法湊足多數,無容錯能力 |
| 3 | 2(過半需 2/3) | 1 | 壞 1 台仍有 2 台可服務 |
| 4 | 3(過半需 3/4) | 1 | 容錯能力與 3 台相同,成本卻白白增加 |
| 5 | 3(過半需 3/5) | 2 | 生產環境推薦配置 |
由多數決的數學可知:2 台節點的容錯是 0,而 4 台的容錯能力與 3 台完全相同,卻增加了通訊成本與故障機率。因此,etcd 叢集的成員數應一律配置為奇數(通常為 3 或 5 台)。
對於 DevOps 而言,必須深刻理解少數方節點的行為:當叢集失去法定多數時,etcd 會立刻拒絕所有寫入;預設的線性一致讀也會一併失敗,只剩本地讀還能回應,但讀到的可能是舊資料。
對應到 Kubernetes:若 3 台 etcd 壞掉 2 台,整個 Kubernetes 控制平面將會陷入「凍結」——無法建立或修改任何資源,排程器也無法指派新 Pod。然而,已經在 Worker Node 上運作的既有業務 Pod 完全不受影響,依然能正常處理網路流量。
MVCC 與 Revision:每一次修改都是歷史
傳統 Key-Value 資料庫的 PUT 操作多半是就地覆蓋,舊資料直接抹除。而 etcd 採用了多版本並行控制(MVCC):每次寫入都不會覆蓋舊值,而是新增一個版本,並賦予一個全域單調遞增的 Revision。
Revision 是一個 64 位元的叢集全域計數器。無論一次交易修改了多少個 Key,全域 Revision 都只會增加 1,這相當於整個分散式系統的全域邏輯時鐘。
[MVCC Key Lifecycle]
Global Revision: 1 2 3 4
| | | |
Key: /app/config |-- "v1" --|-- "v2" --|-- "v3" --|
| | |
(create=1) (mod=2) (mod=3)
(ver=1) (ver=2) (ver=3)
注意 Global Revision 已走到 4,但 /app/config 只畫到 3——因為 Revision 4 來自其他 Key 的寫入。這正是「全域」計數器的意義:任何寫入都會推進它,不論改的是哪個 Key。
查詢 etcd 中的 Key 時,會獲得三個核心屬性:
create_revision:該 Key 最初被建立時的全域 Revision。mod_revision:該 Key 最後一次被修改時的全域 Revision。version:該 Key 在目前生命週期內被修改的累計次數。
$ etcdctl put /app/config "v1"
$ etcdctl put /app/config "v2"
$ etcdctl get /app/config -w json
樂觀並行控制與事件重播
保留版本歷史,為 Kubernetes 帶來了兩項關鍵能力:
- 樂觀並行控制(CAS, Compare-And-Swap):也就是後端常說的樂觀鎖。寫入前不先上鎖,而是在更新時附帶條件:「資料仍是我讀到的那個版本,才允許寫入」;若期間已被別人改過,這次寫入就失敗,重讀後再試。後端常見的實作是在資料表加一個
version欄位,用UPDATE ... WHERE version = ?比對。etcd 的條件交易(If / Then / Else)做的是同一件事,而mod_revision就是那個版本欄位:客戶端可以要求「僅在mod_revision == X時才允許更新」。Kubernetes 物件中的resourceVersion正是基於此機制,它直接對應物件在 etcd 中的mod_revision。當kubectl edit或控制器更新物件時遇到 409 Conflict(the object has been modified),本質上就是樂觀鎖比對失敗。 - 事件重播(Watch 的底層基礎):因為歷史完整留存,客戶端隨時可以發起查詢:「請把 Revision 100 之後的所有變更事件,依序播放給我」。
歷史壓縮(Compaction)
歷史記錄若無止境增長,硬碟終將耗盡。etcd 透過 Compaction 機制清理過期歷史:
$ etcdctl compact 5
$ etcdctl get /app/config --rev=4
# 輸出:rpc error: ... etcdserver: mvcc: required revision has been compacted
執行 Compaction 後,若有請求試圖讀取或監聽已被壓縮的 Revision,etcd 會回傳 ErrCompacted 錯誤。
在自動化維運上,etcd 支援 --auto-compaction-mode(可設定為週期性壓縮,或依版本數壓縮);而在 Kubernetes 架構中,kube-apiserver 預設會以每 5 分鐘一次的頻率(--etcd-compaction-interval=5m0s)主動向 etcd 傳送 Compaction 請求。
需要特別注意的是:Compaction 並不會讓磁碟上的資料庫檔案變小,原因與解法見後文的 Defrag 一節。
Watch 與 Lease:事件驅動與心跳機制
Watch:可靠的事件串流
客戶端在建立 Watch 時可以指定 start_revision。etcd 會把該版本之後發生的所有事件按順序推送給客戶端。
$ etcdctl watch /app/config --rev=6
這項設計對斷線情境特別友善:就算連線中斷,客戶端重新連上時只要帶上「最後收到的 Revision」,etcd 就會先補播斷線期間遺漏的事件,再無縫接上即時推播。
Kubernetes 的控制器客戶端(Client-go)提供了一套稱為 Informer 的快取與事件同步機制。無論是官方 Controller、自訂 Operator 還是 kubectl get -w,背後都是透過 List-and-Watch 模式與 apiserver 同步狀態:
[Informer List-and-Watch Pattern]
Controller / Informer
|
|--(1) List Request (Full State)
| ---> kube-apiserver ---> etcd
|
|<-(2) Return Objects + resourceVersion=100
|
|--(3) Watch Request (resourceVersion=100)
| ---> kube-apiserver ---> etcd
|
|<-(4) Stream Incremental Events (rev > 100)
客戶端不會直接連線 etcd,而是連到 kube-apiserver。kube-apiserver 內建 Watch Cache(--watch-cache,預設啟用),由它承接大量的 Watch 連線,並在本地快取事件。
當 Controller 斷線太久、要求的 Revision 已經在 etcd 被 Compaction 清除時,Watch 會收到 kube-apiserver 回傳的 410 Gone(too old resource version)。此時 Controller 會自動觸發 Re-list,重新拉取完整資料並建立新的 Watch。
Lease:帶 TTL 的租約機制
Lease(租約)是 etcd 提供的基礎機制,用來管理 Key 的生命週期:客戶端先向 etcd 申請一個帶有存活時間(TTL)的 Lease ID,再將特定 Key 綁定到該租約上。
客戶端必須定期傳送 KeepAlive 心跳來續約;一旦客戶端崩潰、停止續約,租約一過期,綁定在該租約下的所有 Key 都會被 etcd 自動刪除,並產生刪除事件。如果你要自行開發微服務,以 etcd clientv3 實作服務註冊與健康檢查,Lease 正是最核心的底層工具。
澄清誤解:Kubernetes 的節點心跳
許多工程師常誤以為 Kubernetes 的 Node 心跳使用的是 etcd 原生 Lease。實際上並非如此。
由於只有 kube-apiserver 能直接存取 etcd,Kubernetes 的節點心跳是在 API 物件層實作的;而獨立的 Lease 物件比每次更新整份 NodeStatus 輕量得多,大型叢集的心跳負擔也因此大幅降低:
- 節點心跳維護在
kube-node-lease命名空間下的 Lease 物件(coordination.k8s.io)。 - kubelet 定期(預設每 10 秒)續約其對應的 Node Lease 物件。
- 控制平面的 Node Lifecycle Controller 定期巡檢。一旦節點心跳逾期未更新,便會將其標記為
NotReady,並在超過寬限期後觸發 Pod 驅逐。 - 同樣的機制也應用於控制平面元件(
kube-scheduler、kube-controller-manager)的 Leader Election 搶鎖。
四大維運實戰情境
掌握了底層原理後,面對線上突發故障時便能迅速推導出病因。以下是 DevOps 在維運 etcd 時最常遭遇的四大情境:
1. Backend Quota 滿載:叢集陷入唯讀
etcd 預設擁有 2 GiB 的配額上限。一旦資料庫檔案大小超過 Quota,etcd 會立刻觸發 NOSPACE 警報,並拒絕所有寫入操作,僅允許讀取與刪除。在 Kubernetes 中,這會導致整個叢集無法建立 Pod 或更新任何狀態。
最常見的肇因是「事件轟炸」:某些業務 Pod 陷入崩潰重啟迴圈,或是排程反覆失敗,產生了大量 Events 塞爆儲存空間。
# 處置 SOP 四步驟:
# 1. 取得目前 Revision
$ rev=$(etcdctl endpoint status --write-out="json" | jq .[0].Status.header.revision)
# 2. 壓縮過期歷史
$ etcdctl compact $rev
# 3. 釋放檔案系統碎片空間
$ etcdctl defrag
# 4. 解除 NOSPACE 警報恢復寫入
$ etcdctl alarm disarm
維運人員平時應監控 Prometheus 指標:
etcd_mvcc_db_total_size_in_bytes(檔案總大小)etcd_mvcc_db_total_size_in_use_in_bytes(實際有效資料大小)
兩者的差值即為內部碎片空間。
2. Defrag 碎片整理:為什麼 Compact 後硬碟沒變小
etcd 底層使用 bbolt(基於 B+Tree 的 Key-Value 引擎)。Compaction 與刪除資料只是在資料庫內部將資料頁面標記為釋放,供後續寫入重複使用;作業系統看到的檔案大小並不會縮小。
執行 etcdctl defrag 能夠重建資料庫檔案並將空間歸還給作業系統。但務必注意:Defrag 執行期間會暫時阻塞該節點的讀寫請求。在線上環境中,嚴禁對整個叢集同時執行 Defrag,必須採逐台輪流的方式執行,並盡量避開業務尖峰時段。
3. 延遲傳染:磁碟 IOPS 瓶頸引發雪崩
Raft 要求每次寫入都必須獲得過半數節點 fsync 寫入磁碟確認。如果其中有節點磁碟寫入緩慢,該節點就會拖慢整體 API 回應,並向上傳染給 kube-apiserver 與所有下游 Controller。
更嚴重的狀況是:重度 IO 競爭會導致 etcd Leader 錯過 100ms 的心跳傳送,Follower 誤判 Leader 失聯而發起重新選舉。反覆改選會造成叢集長時間拒絕寫入。
架構部署原則:
- etcd 應使用高效能、低延遲的本機 SSD。
- 避免與其他高 IO 負載的服務(如資料庫、日誌收集器)共用磁碟。
- 定期檢視
etcd_server_proposals_failed_total(提案失敗次數,異常升高通常代表選舉頻繁或磁碟延遲)與請求延遲。
4. 備份與還原:最後一道防線
當 etcd 發生多數節點永久損毀且無法恢復時,沒有備份就意味著叢集狀態徹底遺失。
在 etcd 工具鏈中,線上操作(如快照保存)使用 etcdctl,而離線的資料目錄維護與還原,則自 etcd v3.5 起獨立為 etcdutl 工具:
# 1. 建立線上快照(Snapshot)
$ etcdctl snapshot save /backup/etcd-snapshot-$(date +%Y%m%d).db
# 2. 驗證快照狀態(使用 etcdutl 離線檢視)
$ etcdutl snapshot status /backup/etcd-snapshot-*.db
執行還原時的標準程序:
- 停止所有 kube-apiserver 實例,避免還原期間寫入髒資料。
- 在所有 etcd 節點上使用
etcdutl snapshot restore重建全新的資料目錄。 - 啟動所有 etcd 節點,並確認叢集健康狀態。
- 重新啟動 kube-apiserver、kube-scheduler 與 kube-controller-manager。
機制總覽與對照
最後用一張表,整理全文的核心機制,以及它們對應到 Kubernetes 的哪些應用:
| etcd 核心機制 | 一句話本質 | Kubernetes 映射與應用 |
|---|---|---|
| Raft 共識 | 寫入需過半數持久化;讀取預設線性一致 | 叢集成員數採 3 或 5 台奇數;失去多數時控制平面凍結但既有 Pod 繼續運作 |
| MVCC + Revision | 全域單調遞增的版本歷史時鐘 | resourceVersion 物件版本、樂觀鎖(409 Conflict 衝突偵測) |
| Watch 機制 | 指定 Revision 訂閱並重播增量事件流 | Informer 的 List-and-Watch、kube-apiserver Watch Cache |
| Lease 租約 | 帶 TTL 與 KeepAlive 的存活宣告 | 節點狀態上報(API 物件層封裝)、控制平面元件 Leader 搶鎖 |
理解 etcd 的核心原理,能幫助我們在建構與維護 Kubernetes 叢集時,建立清晰的故障推導心智模型:從容錯節點數量的配置,到磁碟 IO 效能的把關,再到 Quota 滿載時的冷靜處置。