1. 程式人生 > >etcd:從應用場景到實現原理的全方位解讀

etcd:從應用場景到實現原理的全方位解讀

原文連結

隨著CoreOS和Kubernetes等專案在開源社群日益火熱,它們專案中都用到的etcd元件作為一個高可用強一致性的服務發現儲存倉庫,漸漸為開發人員所關注。在雲端計算時代,如何讓服務快速透明地接入到計算叢集中,如何讓共享配置資訊快速被叢集中的所有機器發現,更為重要的是,如何構建這樣一套高可用、安全、易於部署以及響應快速的服務叢集,已經成為了迫切需要解決的問題。etcd為解決這類問題帶來了福音,本文將從etcd的應用場景開始,深入解讀etcd的實現方式,以供開發者們更為充分地享用etcd所帶來的便利。

經典應用場景

要問etcd是什麼?很多人第一反應可能是一個鍵值儲存倉庫,卻沒有重視官方定義的後半句,用於配置共享和服務發現。

A highly-available key value store for shared configuration and service discovery.

實際上,etcd作為一個受到ZooKeeper與doozer啟發而催生的專案,除了擁有與之類似的功能外,更專注於以下四點。

  • 簡單:基於HTTP+JSON的API讓你用curl就可以輕鬆使用。
  • 安全:可選SSL客戶認證機制。
  • 快速:每個例項每秒支援一千次寫操作。
  • 可信:使用Raft演算法充分實現了分散式。

隨著雲端計算的不斷髮展,分散式系統中涉及到的問題越來越受到人們重視。受阿里中介軟體團隊對

ZooKeeper典型應用場景一覽一文的啟發,筆者根據自己的理解也總結了一些etcd的經典使用場景。讓我們來看看etcd這個基於Raft強一致性演算法的分散式儲存倉庫能給我們帶來哪些幫助。

值得注意的是,分散式系統中的資料分為控制資料和應用資料。使用etcd的場景預設處理的資料都是控制資料,對於應用資料,只推薦資料量很小,但是更新訪問頻繁的情況

場景一:服務發現(Service Discovery)

服務發現要解決的也是分散式系統中最常見的問題之一,即在同一個分散式叢集中的程序或服務,要如何才能找到對方並建立連線。本質上來說,服務發現就是想要了解叢集中是否有程序在監聽udp或tcp埠,並且通過名字就可以查詢和連線。要解決服務發現的問題,需要有下面三大支柱,缺一不可。

  1. 一個強一致性、高可用的服務儲存目錄。基於Raft演算法的etcd天生就是這樣一個強一致性高可用的服務儲存目錄。
  2. 一種註冊服務和監控服務健康狀態的機制。使用者可以在etcd中註冊服務,並且對註冊的服務設定key TTL,定時保持服務的心跳以達到監控健康狀態的效果。
  3. 一種查詢和連線服務的機制。通過在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來進行監控實現起來非常簡單並且實時性強。

  1. 前面幾個場景已經提到Watcher機制,當某個節點消失或有變動時,Watcher會第一時間發現並告知使用者。
  2. 節點可以設定TTL key,比如每隔30s傳送一次心跳使代表該機器存活的節點繼續存在,否則節點消失。

這樣就可以第一時間檢測到各節點的健康狀態,以完成叢集的監控要求。

另外,使用分散式鎖,可以完成Leader競選。這種場景通常是一些長時間CPU計算或者使用IO操作的機器,只需要競選出的Leader計算或處理一次,就可以把結果複製給其他的Follower。從而避免重複勞動,節省計算資源。

這個的經典場景是搜尋系統中建立全量索引。如果每個機器都進行一遍索引的建立,不但耗時而且建立索引的一致性不能保證。通過在etcd的CAS機制同時建立一個節點,建立成功的機器作為Leader,進行索引計算,然後把計算結果分發到其它節點。

圖9 Leader競選

場景八:為什麼用etcd而不用ZooKeeper

閱讀了“ZooKeeper典型應用場景一覽”一文的讀者可能會發現,etcd實現的這些功能,ZooKeeper都能實現。那麼為什麼要用etcd而非直接使用ZooKeeper呢?

相較之下,ZooKeeper有如下缺點:

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

而etcd作為一個後起之秀,其優點也很明顯。

  1. 簡單。使用Go語言編寫部署簡單;使用HTTP作為介面使用簡單;使用Raft演算法保證強一致性讓使用者易於理解
  2. 資料持久化。etcd預設資料一更新就進行持久化。
  3. 安全。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.goNewServer函式準備啟動一個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.gosnapshot函式中看到。所以,實際上資料目錄中有用的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構建起來的大型系統會越來越多。

10 作者簡介

孫健波,浙江大學SEL實驗室碩士研究生,目前在雲平臺團隊從事科研和開發工作。浙大團隊對PaaS、Docker、大資料和主流開源雲端計算技術有深入的研究和二次開發經驗,團隊現將部分技術文章貢獻出來,希望能對讀者有所幫助。

參考文獻

感謝郭蕾對本文的策劃和審校。

給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至[email protected]。也歡迎大家通過新浪微博(@InfoQ)或者騰訊微博(@InfoQ)關注我們,並與我們的編輯和其他讀者朋友交流。