隨着 CoreOS 和 Kubernetes 等項目在開源社區日益火熱,它們項目中都用到的 etcd 組件作爲一個高可用強一致性的服務發現存儲倉庫,漸漸爲開發人員所關注。在雲計算時代,如何讓服務快速透明地接入到計算集羣中,如何讓共享配置信息快速被集羣中的所有機器發現,更爲重要的是,如何構建這樣一套高可用、安全、易於部署以及響應快速的服務集羣,已經成爲了迫切需要解決的問題。etcd 爲解決這類問題帶來了福音,本文將從 etcd 的應用場景開始,深入解讀 etcd 的實現方式,以供開發者們更爲充分地享用 etcd 所帶來的便利。
經典應用場景
要問 etcd 是什麼?很多人第一反應可能是一個鍵值存儲倉庫,卻沒有重視官方定義的後半句,用於配置共享和服務發現。
實際上,etcd 作爲一個受到 ZooKeeper 與 doozer 啓發而催生的項目,除了擁有與之類似的功能外,更專注於以下四點。
- 簡單:基於 HTTP+JSON 的 API 讓你用 curl 就可以輕鬆使用。
- 安全:可選 SSL 客戶認證機制。
- 快速:每個實例每秒支持一千次寫操作。
- 可信:使用 Raft 算法充分實現了分佈式。
隨着雲計算的不斷髮展,分佈式系統中涉及到的問題越來越受到人們重視。受阿里中間件團隊對ZooKeeper 典型應用場景一覽一文的啓發,筆者根據自己的理解也總結了一些 etcd 的經典使用場景。讓我們來看看 etcd 這個基於 Raft 強一致性算法的分佈式存儲倉庫能給我們帶來哪些幫助。
值得注意的是,分佈式系統中的數據分爲控制數據和應用數據。使用 etcd 的場景默認處理的數據都是控制數據,對於應用數據,只推薦數據量很小,但是更新訪問頻繁的情況。
場景一:服務發現(Service Discovery)
服務發現要解決的也是分佈式系統中最常見的問題之一,即在同一個分佈式集羣中的進程或服務,要如何才能找到對方並建立連接。本質上來說,服務發現就是想要了解集羣中是否有進程在監聽 udp 或 tcp 端口,並且通過名字就可以查找和連接。要解決服務發現的問題,需要有下面三大支柱,缺一不可。
- 一個強一致性、高可用的服務存儲目錄。基於 Raft 算法的 etcd 天生就是這樣一個強一致性高可用的服務存儲目錄。
- 一種註冊服務和監控服務健康狀態的機制。用戶可以在 etcd 中註冊服務,並且對註冊的服務設置
key TTL
,定時保持服務的心跳以達到監控健康狀態的效果。 - 一種查找和連接服務的機制。通過在 etcd 指定的主題下注冊的服務也能在對應的主題下查找到。爲了確保連接,我們可以在每個服務機器上都部署一個 Proxy 模式的 etcd,這樣就可以確保能訪問 etcd 集羣的服務都能互相連接。
圖 1 服務發現示意圖
下面我們來看服務發現對應的具體場景。
- 微服務協同工作架構中,服務動態添加。隨着 Docker 容器的流行,多種微服務共同協作,構成一個相對功能強大的架構的案例越來越多。透明化的動態添加這些服務的需求也日益強烈。通過服務發現機制,在 etcd 中註冊某個服務名字的目錄,在該目錄下存儲可用的服務節點的 IP。在使用服務的過程中,只要從服務目錄下查找可用的服務節點去使用即可。
圖 2 微服務協同工作
- PaaS 平臺中應用多實例與實例故障重啓透明化。PaaS 平臺中的應用一般都有多個實例,通過域名,不僅可以透明的對這多個實例進行訪問,而且還可以做到負載均衡。但是應用的某個實例隨時都有可能故障重啓,這時就需要動態的配置域名解析(路由)中的信息。通過 etcd 的服務發現功能就可以輕鬆解決這個動態配置的問題。
圖 3 雲平臺多實例透明化
場景二:消息發佈與訂閱
在分佈式系統中,最適用的一種組件間通信方式就是消息發佈與訂閱。即構建一個配置共享中心,數據提供者在這個配置中心發佈消息,而消息使用者則訂閱他們關心的主題,一旦主題有消息發佈,就會實時通知訂閱者。通過這種方式可以做到分佈式系統配置的集中式管理與動態更新。
- 應用中用到的一些配置信息放到 etcd 上進行集中管理。這類場景的使用方式通常是這樣:應用在啓動的時候主動從 etcd 獲取一次配置信息,同時,在 etcd 節點上註冊一個 Watcher 並等待,以後每次配置有更新的時候,etcd 都會實時通知訂閱者,以此達到獲取最新配置信息的目的。
- 分佈式搜索服務中,索引的元信息和服務器集羣機器的節點狀態存放在 etcd 中,供各個客戶端訂閱使用。使用 etcd 的
key TTL
功能可以確保機器狀態是實時更新的。 - 分佈式日誌收集系統。這個系統的核心工作是收集分佈在不同機器的日誌。收集器通常是按照應用(或主題)來分配收集任務單元,因此可以在 etcd 上創建一個以應用(主題)命名的目錄 P,並將這個應用(主題相關)的所有機器 ip,以子目錄的形式存儲到目錄 P 上,然後設置一個 etcd 遞歸的 Watcher,遞歸式的監控應用(主題)目錄下所有信息的變動。這樣就實現了機器 IP(消息)變動的時候,能夠實時通知到收集器調整任務分配。
- 系統中信息需要動態自動獲取與人工干預修改信息請求內容的情況。通常是暴露出接口,例如 JMX 接口,來獲取一些運行時的信息。引入 etcd 之後,就不用自己實現一套方案了,只要將這些信息存放到指定的 etcd 目錄中即可,etcd 的這些目錄就可以通過 HTTP 的接口在外部訪問。
圖 4 消息發佈與訂閱
場景三:負載均衡
在場景一
中也提到了負載均衡,本文所指的負載均衡均爲軟負載均衡。分佈式系統中,爲了保證服務的高可用以及數據的一致性,通常都會把數據和服務部署多份,以此達到對等服務,即使其中的某一個服務失效了,也不影響使用。由此帶來的壞處是數據寫入性能下降,而好處則是數據訪問時的負載均衡。因爲每個對等服務節點上都存有完整的數據,所以用戶的訪問流量就可以分流到不同的機器上。
- etcd 本身分佈式架構存儲的信息訪問支持負載均衡。etcd 集羣化以後,每個 etcd 的核心節點都可以處理用戶的請求。所以,把數據量小但是訪問頻繁的消息數據直接存儲到 etcd 中也是個不錯的選擇,如業務系統中常用的二級代碼表(在表中存儲代碼,在 etcd 中存儲代碼所代表的具體含義,業務系統調用查表的過程,就需要查找表中代碼的含義)。
- 利用 etcd 維護一個負載均衡節點表。etcd 可以監控一個集羣中多個節點的狀態,當有一個請求發過來後,可以輪詢式的把請求轉發給存活着的多個狀態。類似 KafkaMQ,通過ZooKeeper來維護生產者和消費者的負載均衡。同樣也可以用 etcd 來做ZooKeeper的工作。
圖 5 負載均衡
場景四:分佈式通知與協調
這裏說到的分佈式通知與協調,與消息發佈和訂閱有些相似。都用到了 etcd 中的 Watcher 機制,通過註冊與異步通知機制,實現分佈式環境下不同系統之間的通知與協調,從而對數據變更做到實時處理。實現方式通常是這樣:不同系統都在 etcd 上對同一個目錄進行註冊,同時設置 Watcher 觀測該目錄的變化(如果對子目錄的變化也有需要,可以設置遞歸模式),當某個系統更新了 etcd 的目錄,那麼設置了 Watcher 的系統就會收到通知,並作出相應處理。
- 通過 etcd 進行低耦合的心跳檢測。檢測系統和被檢測系統通過 etcd 上某個目錄關聯而非直接關聯起來,這樣可以大大減少系統的耦合性。
- 通過 etcd 完成系統調度。某系統有控制檯和推送系統兩部分組成,控制檯的職責是控制推送系統進行相應的推送工作。管理人員在控制檯作的一些操作,實際上是修改了 etcd 上某些目錄節點的狀態,而 etcd 就把這些變化通知給註冊了 Watcher 的推送系統客戶端,推送系統再作出相應的推送任務。
- 通過 etcd 完成工作彙報。大部分類似的任務分發系統,子任務啓動後,到 etcd 來註冊一個臨時工作目錄,並且定時將自己的進度進行彙報(將進度寫入到這個臨時目錄),這樣任務管理者就能夠實時知道任務進度。
圖 6 分佈式協同工作
場景五:分佈式鎖
因爲 etcd 使用 Raft 算法保持了數據的強一致性,某次操作存儲到集羣中的值必然是全局一致的,所以很容易實現分佈式鎖。鎖服務有兩種使用方式,一是保持獨佔,二是控制時序。
- 保持獨佔即所有獲取鎖的用戶最終只有一個可以得到。etcd 爲此提供了一套實現分佈式鎖原子操作 CAS(
CompareAndSwap
)的 API。通過設置prevExist
值,可以保證在多個節點同時去創建某個目錄時,只有一個成功。而創建成功的用戶就可以認爲是獲得了鎖。 - 控制時序,即所有想要獲得鎖的用戶都會被安排執行,但是獲得鎖的順序也是全局唯一的,同時決定了執行順序。etcd 爲此也提供了一套 API(自動創建有序鍵),對一個目錄建值時指定爲
POST
動作,這樣 etcd 會自動在目錄下生成一個當前最大的值爲鍵,存儲這個新的值(客戶端編號)。同時還可以使用 API 按順序列出所有當前目錄下的鍵值。此時這些鍵的值就是客戶端的時序,而這些鍵中存儲的值可以是代表客戶端的編號。
圖 7 分佈式鎖
場景六:分佈式隊列
分佈式隊列的常規用法與場景五中所描述的分佈式鎖的控制時序用法類似,即創建一個先進先出的隊列,保證順序。
另一種比較有意思的實現是在保證隊列達到某個條件時再統一按順序執行。這種方法的實現可以在 /queue 這個目錄中另外建立一個 /queue/condition 節點。
- condition 可以表示隊列大小。比如一個大的任務需要很多小任務就緒的情況下才能執行,每次有一個小任務就緒,就給這個 condition 數字加 1,直到達到大任務規定的數字,再開始執行隊列裏的一系列小任務,最終執行大任務。
- condition 可以表示某個任務在不在隊列。這個任務可以是所有排序任務的首個執行程序,也可以是拓撲結構中沒有依賴的點。通常,必須執行這些任務後才能執行隊列中的其他任務。
- condition 還可以表示其它的一類開始執行任務的通知。可以由控制程序指定,當 condition 出現變化時,開始執行隊列任務。
圖 8 分佈式隊列
場景七:集羣監控與 Leader 競選
通過 etcd 來進行監控實現起來非常簡單並且實時性強。
- 前面幾個場景已經提到 Watcher 機制,當某個節點消失或有變動時,Watcher 會第一時間發現並告知用戶。
- 節點可以設置
TTL key
,比如每隔 30s 發送一次心跳使代表該機器存活的節點繼續存在,否則節點消失。
這樣就可以第一時間檢測到各節點的健康狀態,以完成集羣的監控要求。
另外,使用分佈式鎖,可以完成 Leader 競選。這種場景通常是一些長時間 CPU 計算或者使用 IO 操作的機器,只需要競選出的 Leader 計算或處理一次,就可以把結果複製給其他的 Follower。從而避免重複勞動,節省計算資源。
這個的經典場景是搜索系統中建立全量索引。如果每個機器都進行一遍索引的建立,不但耗時而且建立索引的一致性不能保證。通過在 etcd 的 CAS 機制同時創建一個節點,創建成功的機器作爲 Leader,進行索引計算,然後把計算結果分發到其它節點。
圖 9 Leader 競選
場景八:爲什麼用 etcd 而不用ZooKeeper?
閱讀了“ZooKeeper 典型應用場景一覽”一文的讀者可能會發現,etcd 實現的這些功能,ZooKeeper都能實現。那麼爲什麼要用 etcd 而非直接使用ZooKeeper呢?
相較之下,ZooKeeper有如下缺點:
- 複雜。ZooKeeper的部署維護複雜,管理員需要掌握一系列的知識和技能;而 Paxos 強一致性算法也是素來以複雜難懂而聞名於世;另外,ZooKeeper的使用也比較複雜,需要安裝客戶端,官方只提供了 Java 和 C 兩種語言的接口。
- Java 編寫。這裏不是對 Java 有偏見,而是 Java 本身就偏向於重型應用,它會引入大量的依賴。而運維人員則普遍希望保持強一致、高可用的機器集羣儘可能簡單,維護起來也不易出錯。
- 發展緩慢。Apache 基金會項目特有的“Apache Way”在開源界飽受爭議,其中一大原因就是由於基金會龐大的結構以及鬆散的管理導致項目發展緩慢。
而 etcd 作爲一個後起之秀,其優點也很明顯。
- 簡單。使用 Go 語言編寫部署簡單;使用 HTTP 作爲接口使用簡單;使用 Raft 算法保證強一致性讓用戶易於理解。
- 數據持久化。etcd 默認數據一更新就進行持久化。
- 安全。etcd 支持 SSL 客戶端安全認證。
最後,etcd 作爲一個年輕的項目,真正告訴迭代和開發中,這既是一個優點,也是一個缺點。優點是它的未來具有無限的可能性,缺點是無法得到大項目長時間使用的檢驗。然而,目前 CoreOS、Kubernetes 和 CloudFoundry 等知名項目均在生產環境中使用了 etcd,所以總的來說,etcd 值得你去嘗試。
etcd 實現原理解讀
上一節中,我們概括了許多 etcd 的經典場景,這一節,我們將從 etcd 的架構開始,深入到源碼中解析 etcd。
1 架構
圖 10 etcd 架構圖
從 etcd 的架構圖中我們可以看到,etcd 主要分爲四個部分。
- HTTP Server: 用於處理用戶發送的 API 請求以及其它 etcd 節點的同步與心跳信息請求。
- Store:用於處理 etcd 支持的各類功能的事務,包括數據索引、節點狀態變更、監控與反饋、事件處理與執行等等,是 etcd 對用戶提供的大多數 API 功能的具體實現。
- Raft:Raft 強一致性算法的具體實現,是 etcd 的核心。
- WAL:Write Ahead Log(預寫式日誌),是 etcd 的數據存儲方式。除了在內存中存有所有數據的狀態以及節點的索引以外,etcd 就通過 WAL 進行持久化存儲。WAL 中,所有的數據提交前都會事先記錄日誌。Snapshot 是爲了防止數據過多而進行的狀態快照;Entry 表示存儲的具體日誌內容。
通常,一個用戶的請求發送過來,會經由 HTTP Server 轉發給 Store 進行具體的事務處理,如果涉及到節點的修改,則交給 Raft 模塊進行狀態的變更、日誌的記錄,然後再同步給別的 etcd 節點以確認數據提交,最後進行數據的提交,再次同步。
2 新版 etcd 重要變更列表
- 獲得了 IANA 認證的端口,2379 用於客戶端通信,2380 用於節點通信,與原先的(4001 peers / 7001 clients)共用。
- 每個節點可監聽多個廣播地址。監聽的地址由原來的一個擴展到多個,用戶可以根據需求實現更加複雜的集羣環境,如一個是公網 IP,一個是虛擬機(容器)之類的私有 IP。
- etcd 可以代理訪問 leader 節點的請求,所以如果你可以訪問任何一個 etcd 節點,那麼你就可以無視網絡的拓撲結構對整個集羣進行讀寫操作。
- etcd 集羣和集羣中的節點都有了自己獨特的 ID。這樣就防止出現配置混淆,不是本集羣的其他 etcd 節點發來的請求將被屏蔽。
- etcd 集羣啓動時的配置信息目前變爲完全固定,這樣有助於用戶正確配置和啓動。
- 運行時節點變化 (Runtime Reconfiguration)。用戶不需要重啓 etcd 服務即可實現對 etcd 集羣結構進行變更。啓動後可以動態變更集羣配置。
- 重新設計和實現了 Raft 算法,使得運行速度更快,更容易理解,包含更多測試代碼。
- Raft 日誌現在是嚴格的只能向後追加、預寫式日誌系統,並且在每條記錄中都加入了 CRC 校驗碼。
- 啓動時使用的 _etcd/* 關鍵字不再暴露給用戶
- 廢棄集羣自動調整功能的 standby 模式,這個功能使得用戶維護集羣更困難。
- 新增 Proxy 模式,不加入到 etcd 一致性集羣中,純粹進行代理轉發。
- ETCD_NAME(-name)參數目前是可選的,不再用於唯一標識一個節點。
- 摒棄通過配置文件配置 etcd 屬性的方式,你可以用環境變量的方式代替。
- 通過自發現方式啓動集羣必須要提供集羣大小,這樣有助於用戶確定集羣實際啓動的節點數量。
3 etcd 概念詞彙表
- Raft:etcd 所採用的保證分佈式系統強一致性的算法。
- Node:一個 Raft 狀態機實例。
- Member: 一個 etcd 實例。它管理着一個 Node,並且可以爲客戶端請求提供服務。
- Cluster:由多個 Member 構成可以協同工作的 etcd 集羣。
- Peer:對同一個 etcd 集羣中另外一個 Member 的稱呼。
- Client: 向 etcd 集羣發送 HTTP 請求的客戶端。
- WAL:預寫式日誌,etcd 用於持久化存儲的日誌格式。
- snapshot:etcd 防止 WAL 文件過多而設置的快照,存儲 etcd 數據狀態。
- Proxy:etcd 的一種模式,爲 etcd 集羣提供反向代理服務。
- Leader:Raft 算法中通過競選而產生的處理所有數據提交的節點。
- Follower:競選失敗的節點作爲 Raft 中的從屬節點,爲算法提供強一致性保證。
- Candidate:當 Follower 超過一定時間接收不到 Leader 的心跳時轉變爲 Candidate 開始競選。
- Term:某個節點成爲 Leader 到下一次競選時間,稱爲一個 Term。
- Index:數據項編號。Raft 中通過 Term 和 Index 來定位數據。
4 集羣化應用實踐
etcd 作爲一個高可用鍵值存儲系統,天生就是爲集羣化而設計的。由於 Raft 算法在做決策時需要多數節點的投票,所以 etcd 一般部署集羣推薦奇數個節點,推薦的數量爲 3、5 或者 7 個節點構成一個集羣。
4.1 集羣啓動
etcd 有三種集羣化啓動的配置方案,分別爲靜態配置啓動、etcd 自身服務發現、通過 DNS 進行服務發現。
通過配置內容的不同,你可以對不同的方式進行選擇。值得一提的是,這也是新版 etcd 區別於舊版的一大特性,它摒棄了使用配置文件進行參數配置的做法,轉而使用命令行參數或者環境變量的做法來配置參數。
4.1.1. 靜態配置
這種方式比較適用於離線環境,在啓動整個集羣之前,你就已經預先清楚所要配置的集羣大小,以及集羣上各節點的地址和端口信息。那麼啓動時,你就可以通過配置initial-cluster
參數進行 etcd 集羣的啓動。
在每個 etcd 機器啓動時,配置環境變量或者添加啓動參數的方式如下。
ETCD_INITIAL_CLUSTER="infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380"
ETCD_INITIAL_CLUSTER_STATE=new
參數方法:
-initial-cluster
infra0=http://10.0.1.10:2380,http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
-initial-cluster-state new
值得注意的是,-initial-cluster
參數中配置的 url 地址必須與各個節點啓動時設置的initial-advertise-peer-urls
參數相同。(initial-advertise-peer-urls
參數表示節點監聽其他節點同步信號的地址)
如果你所在的網絡環境配置了多個 etcd 集羣,爲了避免意外發生,最好使用-initial-cluster-token
參數爲每個集羣單獨配置一個 token 認證。這樣就可以確保每個集羣和集羣的成員都擁有獨特的 ID。
綜上所述,如果你要配置包含 3 個 etcd 節點的集羣,那麼你在三個機器上的啓動命令分別如下所示。
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
-listen-peer-urls http://10.0.1.10:2380 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
-initial-cluster-state new
$ etcd -name infra1 -initial-advertise-peer-urls http://10.0.1.11:2380 \
-listen-peer-urls http://10.0.1.11:2380 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
-initial-cluster-state new
$ etcd -name infra2 -initial-advertise-peer-urls http://10.0.1.12:2380 \
-listen-peer-urls http://10.0.1.12:2380 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \
-initial-cluster-state new
在初始化完成後,etcd 還提供動態增、刪、改 etcd 集羣節點的功能,這個需要用到etcdctl
命令進行操作。
4.1.2. etcd 自發現模式
通過自發現的方式啓動 etcd 集羣需要事先準備一個 etcd 集羣。如果你已經有一個 etcd 集羣,首先你可以執行如下命令設定集羣的大小,假設爲 3.
$ curl -X PUT http://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83/_config/size -d value=3
然後你要把這個 url 地址http://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
作爲-discovery
參數來啓動 etcd。節點會自動使用http://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
目錄進行 etcd 的註冊和發現服務。
所以最終你在某個機器上啓動 etcd 的命令如下。
$ etcd -name infra0 -initial-advertise-peer-urls http://10.0.1.10:2380 \
-listen-peer-urls http://10.0.1.10:2380 \
-discovery http://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
如果你本地沒有可用的 etcd 集羣,etcd 官網提供了一個可以公網訪問的 etcd 存儲地址。你可以通過如下命令得到 etcd 服務的目錄,並把它作爲-discovery
參數使用。
$ curl http://discovery.etcd.io/new?size=3
http://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
同樣的,當你完成了集羣的初始化後,這些信息就失去了作用。當你需要增加節點時,需要使用etcdctl
來進行操作。
爲了安全,請務必每次啓動新 etcd 集羣時,都使用新的 discovery token 進行註冊。另外,如果你初始化時啓動的節點超過了指定的數量,多餘的節點會自動轉化爲 Proxy 模式的 etcd。
4.1.3. DNS 自發現模式
etcd 還支持使用 DNS SRV 記錄進行啓動。關於 DNS SRV 記錄如何進行服務發現,可以參閱RFC2782,所以,你要在 DNS 服務器上進行相應的配置。
(1) 開啓 DNS 服務器上 SRV 記錄查詢,並添加相應的域名記錄,使得查詢到的結果類似如下。
$ dig +noall +answer SRV _etcd-server._tcp.example.com
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra0.example.com.
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra1.example.com.
_etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra2.example.com.
(2) 分別爲各個域名配置相關的 A 記錄指向 etcd 核心節點對應的機器 IP。使得查詢結果類似如下。
$ dig +noall +answer infra0.example.com infra1.example.com infra2.example.com
infra0.example.com. 300 IN A 10.0.1.10
infra1.example.com. 300 IN A 10.0.1.11
infra2.example.com. 300 IN A 10.0.1.12
做好了上述兩步 DNS 的配置,就可以使用 DNS 啓動 etcd 集羣了。配置 DNS 解析的 url 參數爲-discovery-srv
,其中某一個節點地啓動命令如下。
$ etcd -name infra0 \
-discovery-srv example.com \
-initial-advertise-peer-urls http://infra0.example.com:2380 \
-initial-cluster-token etcd-cluster-1 \
-initial-cluster-state new \
-advertise-client-urls http://infra0.example.com:2379 \
-listen-client-urls http://infra0.example.com:2379 \
-listen-peer-urls http://infra0.example.com:2380
當然,你也可以直接把節點的域名改成 IP 來啓動。
4.2 關鍵部分源碼解析
etcd 的啓動是從主目錄下的main.go
開始的,然後進入etcdmain/etcd.go
,載入配置參數。如果被配置爲 Proxy 模式,則進入 startProxy 函數,否則進入 startEtcd,開啓 etcd 服務模塊和 http 請求處理模塊。
在啓動 http 監聽時,爲了保持與集羣其他 etcd 機器(peers)保持連接,都採用的transport.NewTimeoutListener
啓動方式,這樣在超過指定時間沒有獲得響應時就會出現超時錯誤。而在監聽 client 請求時,採用的是transport.NewKeepAliveListener
,有助於連接的穩定。
在etcdmain/etcd.go
中的 setupCluster 函數可以看到,根據不同 etcd 的參數,啓動集羣的方法略有不同,但是最終需要的就是一個 IP 與端口構成的字符串。
在靜態配置的啓動方式中,集羣的所有信息都已經在給出,所以直接解析用逗號隔開的集羣 url 信息就好了。
DNS 發現的方式類似,會預先發送一個 tcp 的 SRV 請求,先查看etcd-server-ssl._tcp.example.com
下是否有集羣的域名信息,如果沒有找到,則去查看etcd-server._tcp.example.com
。根據找到的域名,解析出對應的 IP 和端口,即集羣的 url 信息。
較爲複雜是 etcd 式的自發現啓動。首先就用自身單個的 url 構成一個集羣,然後在啓動的過程中根據參數進入discovery/discovery.go
源碼的JoinCluster
函數。因爲我們事先是知道啓動時使用的 etcd 的 token 地址的,裏面包含了集羣大小 (size) 信息。在這個過程其實是個不斷監測與等待的過程。啓動的第一步就是在這個 etcd 的 token 目錄下注冊自身的信息,然後再監測 token 目錄下所有節點的數量,如果數量沒有達標,則循環等待。當數量達到要求時,才結束,進入正常的啓動過程。
配置 etcd 過程中通常要用到兩種 url 地址容易混淆,一種用於 etcd 集羣同步信息並保持連接,通常稱爲 peer-urls;另外一種用於接收用戶端發來的 HTTP 請求,通常稱爲 client-urls。
peer-urls
:通常監聽的端口爲2380
(老版本使用的端口爲7001
),包括所有已經在集羣中正常工作的所有節點的地址。client-urls
:通常監聽的端口爲2379
(老版本使用的端口爲4001
),爲適應複雜的網絡環境,新版 etcd 監聽客戶端請求的 url 從原來的 1 個變爲現在可配置的多個。這樣 etcd 可以配合多塊網卡同時監聽不同網絡下的請求。
4.3 運行時節點變更
etcd 集羣啓動完畢後,可以在運行的過程中對集羣進行重構,包括核心節點的增加、刪除、遷移、替換等。運行時重構使得 etcd 集羣無須重啓即可改變集羣的配置,這也是新版 etcd 區別於舊版包含的新特性。
只有當集羣中多數節點正常的情況下,你纔可以進行運行時的配置管理。因爲配置更改的信息也會被 etcd 當成一個信息存儲和同步,如果集羣多數節點損壞,集羣就失去了寫入數據的能力。所以在配置 etcd 集羣數量時,強烈推薦至少配置 3 個核心節點。
4.3.1. 節點遷移、替換
當你節點所在的機器出現硬件故障,或者節點出現如數據目錄損壞等問題,導致節點永久性的不可恢復時,就需要對節點進行遷移或者替換。當一個節點失效以後,必須儘快修復,因爲 etcd 集羣正常運行的必要條件是集羣中多數節點都正常工作。
遷移一個節點需要進行四步操作:
- 暫停正在運行着的節點程序進程
- 把數據目錄從現有機器拷貝到新機器
- 使用 api 更新 etcd 中對應節點指向機器的 url 記錄更新爲新機器的 ip
- 使用同樣的配置項和數據目錄,在新的機器上啓動 etcd。
4.3.2. 節點增加
增加節點可以讓 etcd 的高可用性更強。舉例來說,如果你有 3 個節點,那麼最多允許 1 個節點失效;當你有 5 個節點時,就可以允許有 2 個節點失效。同時,增加節點還可以讓 etcd 集羣具有更好的讀性能。因爲 etcd 的節點都是實時同步的,每個節點上都存儲了所有的信息,所以增加節點可以從整體上提升讀的吞吐量。
增加一個節點需要進行兩步操作:
- 在集羣中添加這個節點的 url 記錄,同時獲得集羣的信息。
- 使用獲得的集羣信息啓動新 etcd 節點。
4.3.3. 節點移除
有時你不得不在提高 etcd 的寫性能和增加集羣高可用性上進行權衡。Leader 節點在提交一個寫記錄時,會把這個消息同步到每個節點上,當得到多數節點的同意反饋後,纔會真正寫入數據。所以節點越多,寫入性能越差。在節點過多時,你可能需要移除一個或多個。
移除節點非常簡單,只需要一步操作,就是把集羣中這個節點的記錄刪除。然後對應機器上的該節點就會自動停止。
4.3.4. 強制性重啓集羣
當集羣超過半數的節點都失效時,就需要通過手動的方式,強制性讓某個節點以自己爲 Leader,利用原有數據啓動一個新集羣。
此時你需要進行兩步操作。
- 備份原有數據到新機器。
- 使用
-force-new-cluster
加備份的數據重新啓動節點
注意:強制性重啓是一個迫不得已的選擇,它會破壞一致性協議保證的安全性(如果操作時集羣中尚有其它節點在正常工作,就會出錯),所以在操作前請務必要保存好數據。
5 Proxy 模式
Proxy 模式也是新版 etcd 的一個重要變更,etcd 作爲一個反向代理把客戶的請求轉發給可用的 etcd 集羣。這樣,你就可以在每一臺機器都部署一個 Proxy 模式的 etcd 作爲本地服務,如果這些 etcd Proxy 都能正常運行,那麼你的服務發現必然是穩定可靠的。
圖 11 Proxy 模式示意圖
所以 Proxy 並不是直接加入到符合強一致性的 etcd 集羣中,也同樣的,Proxy 並沒有增加集羣的可靠性,當然也沒有降低集羣的寫入性能。
5.1 Proxy 取代 Standby 模式的原因
那麼,爲什麼要有 Proxy 模式而不是直接增加 etcd 核心節點呢?實際上 etcd 每增加一個核心節點(peer),都會增加 Leader 節點一定程度的包括網絡、CPU 和磁盤的負擔,因爲每次信息的變化都需要進行同步備份。增加 etcd 的核心節點可以讓整個集羣具有更高的可靠性,但是當數量達到一定程度以後,增加可靠性帶來的好處就變得不那麼明顯,反倒是降低了集羣寫入同步的性能。因此,增加一個輕量級的 Proxy 模式 etcd 節點是對直接增加 etcd 核心節點的一個有效代替。
熟悉 0.4.6 這個舊版本 etcd 的用戶會發現,Proxy 模式實際上是取代了原先的 Standby 模式。Standby 模式除了轉發代理的功能以外,還會在覈心節點因爲故障導致數量不足的時候,從 Standby 模式轉爲正常節點模式。而當那個故障的節點恢復時,發現 etcd 的核心節點數量已經達到的預先設置的值,就會轉爲 Standby 模式。
但是新版 etcd 中,只會在最初啓動 etcd 集羣時,發現核心節點的數量已經滿足要求時,自動啓用 Proxy 模式,反之則並未實現。主要原因如下。
- etcd 是用來保證高可用的組件,因此它所需要的系統資源(包括內存、硬盤和 CPU 等)都應該得到充分保障以保證高可用。任由集羣的自動變換隨意地改變核心節點,無法讓機器保證性能。所以 etcd 官方鼓勵大家在大型集羣中爲運行 etcd 準備專有機器集羣。
- 因爲 etcd 集羣是支持高可用的,部分機器故障並不會導致功能失效。所以機器發生故障時,管理員有充分的時間對機器進行檢查和修復。
- 自動轉換使得 etcd 集羣變得複雜,尤其是如今 etcd 支持多種網絡環境的監聽和交互。在不同網絡間進行轉換,更容易發生錯誤,導致集羣不穩定。
基於上述原因,目前 Proxy 模式有轉發代理功能,而不會進行角色轉換。
5.2 關鍵部分源碼解析
從代碼中可以看到,Proxy 模式的本質就是起一個 HTTP 代理服務器,把客戶發到這個服務器的請求轉發給別的 etcd 節點。
etcd 目前支持讀寫皆可和只讀兩種模式。默認情況下是讀寫皆可,就是把讀、寫兩種請求都進行轉發。而只讀模式只轉發讀的請求,對所有其他請求返回 501 錯誤。
值得注意的是,除了啓動過程中因爲設置了proxy
參數會作爲 Proxy 模式啓動。在 etcd 集羣化啓動時,節點註冊自身的時候監測到集羣的實際節點數量已經符合要求,那麼就會退化爲 Proxy 模式。
6 數據存儲
etcd 的存儲分爲內存存儲和持久化(硬盤)存儲兩部分,內存中的存儲除了順序化的記錄下所有用戶對節點數據變更的記錄外,還會對用戶數據進行索引、建堆等方便查詢的操作。而持久化則使用預寫式日誌(WAL:Write Ahead Log)進行記錄存儲。
在 WAL 的體系中,所有的數據在提交之前都會進行日誌記錄。在 etcd 的持久化存儲目錄中,有兩個子目錄。一個是 WAL,存儲着所有事務的變化記錄;另一個則是 snapshot,用於存儲某一個時刻 etcd 所有目錄的數據。通過 WAL 和 snapshot 相結合的方式,etcd 可以有效的進行數據存儲和節點故障恢復等操作。
既然有了 WAL 實時存儲了所有的變更,爲什麼還需要 snapshot 呢?隨着使用量的增加,WAL 存儲的數據會暴增,爲了防止磁盤很快就爆滿,etcd 默認每 10000 條記錄做一次 snapshot,經過 snapshot 以後的 WAL 文件就可以刪除。而通過 API 可以查詢的歷史 etcd 操作默認爲 1000 條。
首次啓動時,etcd 會把啓動的配置信息存儲到data-dir
參數指定的數據目錄中。配置信息包括本地節點的 ID、集羣 ID 和初始時集羣信息。用戶需要避免 etcd 從一個過期的數據目錄中重新啓動,因爲使用過期的數據目錄啓動的節點會與集羣中的其他節點產生不一致(如:之前已經記錄並同意 Leader 節點存儲某個信息,重啓後又向 Leader 節點申請這個信息)。所以,爲了最大化集羣的安全性,一旦有任何數據損壞或丟失的可能性,你就應該把這個節點從集羣中移除,然後加入一個不帶數據目錄的新節點。
6.1 預寫式日誌(WAL)
WAL(Write Ahead Log)最大的作用是記錄了整個數據變化的全部歷程。在 etcd 中,所有數據的修改在提交前,都要先寫入到 WAL 中。使用 WAL 進行數據的存儲使得 etcd 擁有兩個重要功能。
- 故障快速恢復: 當你的數據遭到破壞時,就可以通過執行所有 WAL 中記錄的修改操作,快速從最原始的數據恢復到數據損壞前的狀態。
- 數據回滾(undo)/ 重做(redo):因爲所有的修改操作都被記錄在 WAL 中,需要回滾或重做,只需要方向或正向執行日誌中的操作即可。
WAL 與 snapshot 在 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 起始狀態來定。
snapshot 的存儲命名則比較容易理解,以$term-$index.wal
格式進行命名存儲。term 和 index 就表示存儲 snapshot 時數據所在的 raft 節點狀態,當前的任期編號以及數據項位置信息。
6.2 關鍵部分源碼解析
從代碼邏輯中可以看到,WAL 有兩種模式,讀模式(read)和數據添加(append)模式,兩種模式不能同時成立。一個新創建的 WAL 文件處於 append 模式,並且不會進入到 read 模式。一個本來存在的 WAL 文件被打開的時候必然是 read 模式,並且只有在所有記錄都被讀完的時候,才能進入 append 模式,進入 append 模式後也不會再進入 read 模式。這樣做有助於保證數據的完整與準確。
集羣在進入到etcdserver/server.go
的NewServer
函數準備啓動一個 etcd 節點時,會檢測是否存在以前的遺留 WAL 數據。
檢測的第一步是查看 snapshot 文件夾下是否有符合規範的文件,若檢測到 snapshot 格式是 v0.4 的,則調用函數升級到 v0.5。從 snapshot 中獲得集羣的配置信息,包括 token、其他節點的信息等等,然後載入 WAL 目錄的內容,從小到大進行排序。根據 snapshot 中得到的 term 和 index,找到 WAL 緊接着 snapshot 下一條的記錄,然後向後更新,直到所有 WAL 包的 entry 都已經遍歷完畢,Entry 記錄到 ents 變量中存儲在內存裏。此時 WAL 就進入 append 模式,爲數據項添加進行準備。
當 WAL 文件中數據項內容過大達到設定值(默認爲 10000)時,會進行 WAL 的切分,同時進行 snapshot 操作。這個過程可以在etcdserver/server.go
的snapshot
函數中看到。所以,實際上數據目錄中有用的 snapshot 和 WAL 文件各只有一個,默認情況下 etcd 會各保留 5 個歷史文件。
7 Raft
新版 etcd 中,raft 包就是對 Raft 一致性算法的具體實現。關於 Raft 算法的講解,網上已經有很多文章,有興趣的讀者可以去閱讀一下Raft 算法論文非常精彩。本文則不再對 Raft 算法進行詳細描述,而是結合 etcd,針對算法中一些關鍵內容以問答的形式進行講解。有關 Raft 算法的術語如果不理解,可以參見概念詞彙表一節。
7.1 Raft 常見問答一覽
- Raft 中一個 Term(任期)是什麼意思? Raft 算法中,從時間上,一個任期講即從一次競選開始到下一次競選開始。從功能上講,如果 Follower 接收不到 Leader 節點的心跳信息,就會結束當前任期,變爲 Candidate 發起競選,有助於 Leader 節點故障時集羣的恢復。發起競選投票時,任期值小的節點不會競選成功。如果集羣不出現故障,那麼一個任期將無限延續下去。而投票出現衝突也有可能直接進入下一任再次競選。
圖 12 Term 示意圖
- Raft 狀態機是怎樣切換的? Raft 剛開始運行時,節點默認進入 Follower 狀態,等待 Leader 發來心跳信息。若等待超時,則狀態由 Follower 切換到 Candidate 進入下一輪 term 發起競選,等到收到集羣多數節點的投票時,該節點轉變爲 Leader。Leader 節點有可能出現網絡等故障,導致別的節點發起投票成爲新 term 的 Leader,此時原先的老 Leader 節點會切換爲 Follower。Candidate 在等待其它節點投票的過程中如果發現別的節點已經競選成功成爲 Leader 了,也會切換爲 Follower 節點。
圖 13 Raft 狀態機
- 如何保證最短時間內競選出 Leader,防止競選衝突? 在 Raft 狀態機一圖中可以看到,在 Candidate 狀態下, 有一個 times out,這裏的 times out 時間是個隨機值,也就是說,每個機器成爲 Candidate 以後,超時發起新一輪競選的時間是各不相同的,這就會出現一個時間差。在時間差內,如果 Candidate1 收到的競選信息比自己發起的競選信息 term 值大(即對方爲新一輪 term),並且新一輪想要成爲 Leader 的 Candidate2 包含了所有提交的數據,那麼 Candidate1 就會投票給 Candidate2。這樣就保證了只有很小的概率會出現競選衝突。
- 如何防止別的 Candidate 在遺漏部分數據的情況下發起投票成爲 Leader? Raft 競選的機制中,使用隨機值決定超時時間,第一個超時的節點就會提升 term 編號發起新一輪投票,一般情況下別的節點收到競選通知就會投票。但是,如果發起競選的節點在上一個 term 中保存的已提交數據不完整,節點就會拒絕投票給它。通過這種機制就可以防止遺漏數據的節點成爲 Leader。
- Raft 某個節點宕機後會如何? 通常情況下,如果是 Follower 節點宕機,如果剩餘可用節點數量超過半數,集羣可以幾乎沒有影響的正常工作。如果是 Leader 節點宕機,那麼 Follower 就收不到心跳而超時,發起競選獲得投票,成爲新一輪 term 的 Leader,繼續爲集羣提供服務。需要注意的是;etcd 目前沒有任何機制會自動去變化整個集羣總共的節點數量,即如果沒有人爲的調用 API,etcd 宕機後的節點仍然被計算爲總節點數中,任何請求被確認需要獲得的投票數都是這個總數的半數以上。
圖 14 節點宕機
- 爲什麼 Raft 算法在確定可用節點數量時不需要考慮拜占庭將軍問題? 拜占庭問題中提出,允許 n 個節點宕機還能提供正常服務的分佈式架構,需要的總節點數量爲 3n+1,而 Raft 只需要 2n+1 就可以了。其主要原因在於,拜占庭將軍問題中存在數據欺騙的現象,而 etcd 中假設所有的節點都是誠實的。etcd 在競選前需要告訴別的節點自身的 term 編號以及前一輪 term 最終結束時的 index 值,這些數據都是準確的,其他節點可以根據這些值決定是否投票。另外,etcd 嚴格限制 Leader 到 Follower 這樣的數據流向保證數據一致不會出錯。
- 用戶從集羣中哪個節點讀寫數據? Raft 爲了保證數據的強一致性,所有的數據流向都是一個方向,從 Leader 流向 Follower,也就是所有 Follower 的數據必須與 Leader 保持一致,如果不一致會被覆蓋。即所有用戶更新數據的請求都最先由 Leader 獲得,然後存下來通知其他節點也存下來,等到大多數節點反饋時再把數據提交。一個已提交的數據項纔是 Raft 真正穩定存儲下來的數據項,不再被修改,最後再把提交的數據同步給其他 Follower。因爲每個節點都有 Raft 已提交數據準確的備份(最壞的情況也只是已提交數據還未完全同步),所以讀的請求任意一個節點都可以處理。
- etcd 實現的 Raft 算法性能如何? 單實例節點支持每秒 1000 次數據寫入。節點越多,由於數據同步涉及到網絡延遲,會根據實際情況越來越慢,而讀性能會隨之變強,因爲每個節點都能處理用戶請求。
7.2 關鍵部分源碼解析
在 etcd 代碼中,Node 作爲 Raft 狀態機的具體實現,是整個算法的關鍵,也是瞭解算法的入口。
在 etcd 中,對 Raft 算法的調用如下,你可以在etcdserver/raft.go
中的startNode
找到:
storage := raft.NewMemoryStorage()
n := raft.StartNode(0x01, []int64{0x02, 0x03}, 3, 1, storage)
通過這段代碼可以瞭解到,Raft 在運行過程記錄數據和狀態都是保存在內存中,而代碼中raft.StartNode
啓動的 Node 就是 Raft 狀態機 Node。啓動了一個 Node 節點後,Raft 會做如下事項。
首先,你需要把從集羣的其他機器上收到的信息推送到 Node 節點,你可以在etcdserver/server.go
中的Process
函數看到。
func (s *EtcdServer) Process(ctx context.Context, m raftpb.Message) error {
if m.Type == raftpb.MsgApp {
s.stats.RecvAppendReq(types.ID(m.From).String(), m.Size())
}
return s.node.Step(ctx, m)
}
在檢測發來請求的機器是否是集羣中的節點,自身節點是否是 Follower,把發來請求的機器作爲 Leader,具體對 Node 節點信息的推送和處理則通過node.Step()
函數實現。
其次,你需要把日誌項存儲起來,在你的應用中執行提交的日誌項,然後把完成信號發送給集羣中的其它節點,再通過node.Ready()
監聽等待下一次任務執行。有一點非常重要,你必須確保在你發送完成消息給其他節點之前,你的日誌項內容已經確切穩定的存儲下來了。
最後,你需要保持一個心跳信號Tick()
。Raft 有兩個很重要的地方用到超時機制:心跳保持和 Leader 競選。需要用戶在其 raft 的 Node 節點上週期性的調用 Tick() 函數,以便爲超時機制服務。
綜上所述,整個 raft 節點的狀態機循環類似如下所示:
for {
select {
case <-s.Ticker:
n.Tick()
case rd := <-s.Node.Ready():
saveToStorage(rd.State, rd.Entries)
send(rd.Messages)
process(rd.CommittedEntries)
s.Node.Advance()
case <-s.done:
return
}
}
而這個狀態機真實存在的代碼位置爲etcdserver/server.go
中的run
函數。
對狀態機進行狀態變更(如用戶數據更新等)則是調用n.Propose(ctx, data)
函數,在存儲數據時,會先進行序列化操作。獲得大多數其他節點的確認後,數據會被提交,存爲已提交狀態。
之前提到 etcd 集羣的啓動需要藉助別的 etcd 集羣或者 DNS,而啓動完畢後這些外力
就不需要了,etcd 會把自身集羣的信息作爲狀態存儲起來。所以要變更自身集羣節點數量實際上也需要像用戶數據變更那樣添加數據條目到 Raft 狀態機中。這一切由n.ProposeConfChange(ctx, cc)
實現。當集羣配置信息變更的請求同樣得到大多數節點的確認反饋後,再進行配置變更的正式操作,代碼如下。
var cc raftpb.ConfChange
cc.Unmarshal(data)
n.ApplyConfChange(cc)
注意:一個 ID 唯一性的表示了一個集羣,所以爲了避免不同 etcd 集羣消息混亂,ID 需要確保唯一性,不能重複使用舊的 token 數據作爲 ID。
8 Store
Store 這個模塊顧名思義,就像一個商店把 etcd 已經準備好的各項底層支持加工起來,爲用戶提供五花八門的 API 支持,處理用戶的各項請求。要理解 Store,只需要從 etcd 的 API 入手即可。打開etcd 的 API 列表,我們可以看到有如下 API 是對 etcd 存儲的鍵值進行的操作,亦即 Store 提供的內容。API 中提到的目錄(Directory)和鍵(Key),上文中也可能稱爲 etcd 節點(Node)。
- 爲 etcd 存儲的鍵賦值
curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="Hello world" { "action": "set", "node": { "createdIndex": 2, "key": "/message", "modifiedIndex": 2, "value": "Hello world" } }
反饋的內容含義如下:
- action: 剛剛進行的動作名稱。
- node.key: 請求的 HTTP 路徑。etcd 使用一個類似文件系統的方式來反映鍵值存儲的內容。
- node.value: 剛剛請求的鍵所存儲的內容。
- node.createdIndex: etcd 節點每次有變化時都會自增的一個值,除了用戶請求外,etcd 內部運行(如啓動、集羣信息變化等)也會對節點有變動而引起這個值的變化。
- node.modifiedIndex: 類似 node.createdIndex,能引起 modifiedIndex 變化的操作包括 set, delete, update, create, compareAndSwap and compareAndDelete。
- 查詢 etcd 某個鍵存儲的值
curl http://127.0.0.1:2379/v2/keys/message
- 修改鍵值:與創建新值幾乎相同,但是反饋時會有一個
prevNode
值反應了修改前存儲的內容。curl http://127.0.0.1:2379/v2/keys/message -XPUT -d value="Hello etcd"
- 刪除一個值
curl http://127.0.0.1:2379/v2/keys/message -XDELETE
- 對一個鍵進行定時刪除:etcd 中對鍵進行定時刪除,設定一個 TTL 值,當這個值到期時鍵就會被刪除。反饋的內容會給出 expiration 項告知超時時間,ttl 項告知設定的時長。
curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -d ttl=5
- 取消定時刪除任務
curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -d ttl= -d prevExist=true
- 對鍵值修改進行監控:etcd 提供的這個 API 讓用戶可以監控一個值或者遞歸式的監控一個目錄及其子目錄的值,當目錄或值發生變化時,etcd 會主動通知。
curl http://127.0.0.1:2379/v2/keys/foo?wait=true
- 對過去的鍵值操作進行查詢:類似上面提到的監控,只不過監控時加上了過去某次修改的索引編號,就可以查詢歷史操作。默認可查詢的歷史記錄爲 1000 條。
curl 'http://127.0.0.1:2379/v2/keys/foo?wait=true&waitIndex=7'
- 自動在目錄下創建有序鍵。在對創建的目錄使用
POST
參數,會自動在該目錄下創建一個以 createdIndex 值爲鍵的值,這樣就相當於以創建時間先後嚴格排序了。這個 API 對分佈式隊列這類場景非常有用。curl http://127.0.0.1:2379/v2/keys/queue -XPOST -d value=Job1 { "action": "create", "node": { "createdIndex": 6, "key": "/queue/6", "modifiedIndex": 6, "value": "Job1" } }
- 按順序列出所有創建的有序鍵。
curl -s 'http://127.0.0.1:2379/v2/keys/queue?recursive=true&sorted=true'
- 創建定時刪除的目錄:就跟定時刪除某個鍵類似。如果目錄因爲超時被刪除了,其下的所有內容也自動超時刪除。
curl http://127.0.0.1:2379/v2/keys/dir -XPUT -d ttl=30 -d dir=true
刷新超時時間。
curl http://127.0.0.1:2379/v2/keys/dir -XPUT -d ttl=30 -d dir=true -d prevExist=true
- 自動化 CAS(Compare-and-Swap)操作:etcd 強一致性最直觀的表現就是這個 API,通過設定條件,阻止節點二次創建或修改。即用戶的指令被執行當且僅當 CAS 的條件成立。條件有以下幾個。
- prevValue 先前節點的值,如果值與提供的值相同才允許操作。
- prevIndex 先前節點的編號,編號與提供的校驗編號相同才允許操作。
- prevExist 先前節點是否存在。如果存在則不允許操作。這個常常被用於分佈式鎖的唯一獲取。
假設先進行了如下操作:設定了 foo 的值。
curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=one
然後再進行操作:
curl http://127.0.0.1:2379/v2/keys/foo?prevExist=false -XPUT -d value=three
就會返回創建失敗的錯誤。
- 條件刪除(Compare-and-Delete):與 CAS 類似,條件成立後才能刪除。
- 創建目錄
curl http://127.0.0.1:2379/v2/keys/dir -XPUT -d dir=true
- 列出目錄下所有的節點信息,最後以
/
結尾。還可以通過 recursive 參數遞歸列出所有子目錄信息。curl http://127.0.0.1:2379/v2/keys/
- 刪除目錄:默認情況下只允許刪除空目錄,如果要刪除有內容的目錄需要加上
recursive=true
參數。curl 'http://127.0.0.1:2379/v2/keys/foo_dir?dir=true' -XDELETE
- 創建一個隱藏節點:命名時名字以下劃線
_
開頭默認就是隱藏鍵。curl http://127.0.0.1:2379/v2/keys/_message -XPUT -d value="Hello hidden world"
相信看完這麼多 API,讀者已經對 Store 的工作內容基本瞭解了。它對 etcd 下存儲的數據進行加工,創建出如文件系統般的樹狀結構供用戶快速查詢。它有一個Watcher
用於節點變更的實時反饋,還需要維護一個WatcherHub
對所有Watcher
訂閱者進行通知的推送。同時,它還維護了一個由定時鍵構成的小頂堆,快速返回下一個要超時的鍵。最後,所有這些 API 的請求都以事件的形式存儲在事件隊列中等待處理。
9 總結
通過從應用場景到源碼分析的一系列回顧,我們瞭解到 etcd 並不是一個簡單的分佈式鍵值存儲系統。它解決了分佈式場景中最爲常見的一致性問題,爲服務發現提供了一個穩定高可用的消息註冊倉庫,爲以微服務協同工作的架構提供了無限的可能。相信在不久的將來,通過 etcd 構建起來的大型系統會越來越多。