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 要解決的難題:

etcd 給出的解法是「分散式共識」:任何一筆寫入請求,必須先讓叢集內過半數成員(Quorum)成功持久化到磁碟,才算寫入成功。這條規則同時兼顧了可用性(容許少數節點失效)與一致性(確保永遠只有一個合法狀態)。

圍繞著共識核心,etcd 建構了四大支柱機制:

  1. Raft 共識演算法:決定誰的寫入與提案算數。
  2. MVCC 與 Revision:以多版本並行控制(Multi-Version Concurrency Control)記錄每一次資料修改的歷史版本。
  3. Watch 機制:允許客戶端訂閱變更,並可從指定的歷史版本重播事件。
  4. 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 統一受理。

在讀取方面,etcd 預設採用線性一致讀(Linearizable Read):即便請求發給 Follower,背後也會先向 Leader 確認最新的已提交進度,保證讀到的一定是最新已提交的資料;若追求極致吞吐量,也可改用本地讀(Serializable Read),直接由單一節點本地回應,速度更快但可能讀到短暫落後的舊資料。

容錯算術與節點數奇數原則

NN 台節點的叢集,其容錯能力為:

⌊(N−1)/2⌋{\lfloor(N-1)/2\rfloor}

也就是把 N−1 除以 2 之後無條件捨去——這意味著 3 台節點可容忍 1 台故障,5 台節點可容忍 2 台故障。

節點總數(NN)達成法定多數(Quorum)需求容許故障節點數容錯說明
22(需 2/2 全體同意)0壞 1 台即無法湊足多數,無容錯能力
32(過半需 2/3)1壞 1 台仍有 2 台可服務
43(過半需 3/4)1容錯能力與 3 台相同,成本卻白白增加
53(過半需 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 時,會獲得三個核心屬性:

$ etcdctl put /app/config "v1"
$ etcdctl put /app/config "v2"
$ etcdctl get /app/config -w json

樂觀並行控制與事件重播

保留版本歷史,為 Kubernetes 帶來了兩項關鍵能力:

  1. 樂觀並行控制(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),本質上就是樂觀鎖比對失敗。
  2. 事件重播(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 輕量得多,大型叢集的心跳負擔也因此大幅降低:


四大維運實戰情境

掌握了底層原理後,面對線上突發故障時便能迅速推導出病因。以下是 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 指標:

兩者的差值即為內部碎片空間。

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 失聯而發起重新選舉。反覆改選會造成叢集長時間拒絕寫入。

架構部署原則:

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

執行還原時的標準程序:

  1. 停止所有 kube-apiserver 實例,避免還原期間寫入髒資料。
  2. 在所有 etcd 節點上使用 etcdutl snapshot restore 重建全新的資料目錄。
  3. 啟動所有 etcd 節點,並確認叢集健康狀態。
  4. 重新啟動 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 滿載時的冷靜處置。