一、簡介

Etcd按照官方介紹:
Etcd is a distributed, consistent key-value store for shared configuration and service discovery
是一個分布式的,一致的鍵值對存儲,主要用於共享配置和服務發現。
Etcd是CoreOS團隊於2013年6月發起的開源項目,它的目標是構建一個高可用的分布式鍵值對(key-value)數據庫,基於GO語言實現。
在分布式系統中,最基本最重要的問題就是各種信息的一致性,包括對服務的配置信息的管理、服務的發現、更新、同步等等。
而要解決這些問題,往往需要基於一套能保證一致性的分布式數據庫系統,比如經典的Apache ZooKeeper項目,通過維護文件目錄信息來實現數據的一致性。
Etcd就是專門為集群環境設計,可以很好的實現數據的一致性,提供集群節點管理和服務自動發現等。
受到Apache ZooKeeper項目和doozer項目的啟發,Etcd在進行設計的時候重點考慮下面四個要素:
簡單:支持REST風格的HTTP+JSON API;
安全:支持HTTPS方式的訪問;
快速:支持並發每秒一千次的寫操作;
可靠:支持分布式結構,基於Raft算法實現一致性。
通常情況下,使用Etcd可以在多個節點上啟動多個實例,並將他們添加為一個集群。
同一個集群中的Etcd實例將會自動保持彼此信息的一致性,這意味着分布在各個節點上的應用也將獲取到一致的信息。
二、安裝和使用
Etcd基於Go語言實現,可以直接從項目主頁:https://github.com/coreos/etcd下載源碼自行編譯,
也可以下載編譯好的二進制文件,甚至直接使用制作好的Docker鏡像文件來使用,下面使用二進制文件的方式進行安裝。
1.下載和安裝
下載和解壓:
wget https://github.com/etcd-io/etcd/releases/download/v3.3.10/etcd-v3.3.10-linux-arm64.tar.gz
tar -vxf etcd-v3.3.10-linux-arm64.tar.gz
解壓后,可以看到的文件包括:

其中etcd是服務主文件,etcdctl是提供給用戶的的客戶端命名,其它都是文檔文件。
將etcd和etcdctl放到系統的可執行目錄/usr/bin/或者/usr/local/bin/下面就可以使用了
cp etcd* /usr/local/bin/
2.使用Etcd
直接執行Etcd命令,將啟動一個實例監聽本地的2379和4001端口。
此時客戶端可以通過本地的2379和4001端口訪問Etcd,其他Etcd本地實例可以通過2380和7001端口連接到新啟動實例。
(1)啟動

(2)查看版本

(3)查看集群健康狀態
通過REST API直接查看集群健康狀態:

通過ectdctl查看:

3.服務啟動參數
Etcd服務啟動的時候支持一些參數,用戶可以通過這些參數來調整服務和集群的行為。
參數可以通過環境變量形式傳入,命名全部為大寫,並且加ETCD_前綴,例如ETCD_NAME='etcd-cluster'。
主要參數包括:通用參數、集群參數、安全相關參數、代理參數。
(1)通用參數

(2)集群參數

(3)安全相關參數

(4)代理參數

這些參數主要是當Etcd服務自身僅作為代理模式時候使用,即轉發來自客戶端的請求到指定的Etcd集群。
此時Etcd服務本省並不參與集群中去,不保存數據和參加選舉。
(5)日志參數

(6)其它

三、使用etcdctl客戶端
etcdctl是Etcd官方提供的命令行客戶端,它支持一些基於HTTP API封裝好的命令,
供用戶直接跟Etcd服務打交道,而無需基於HTTP API的方式。
當然這些命令跟HTTP API實際上是對應的,最終效果上並無不同之處。
Etcd項目二進制文件包中已經包含了etcdctl工具,也可以專門下載一個。
etcdctl的命令格式:
etcdctl [global options] command [command options] [arguments...]
etcdctl [全局選項] 命令 [命令選項] [命令參數]
etcdctl命令的全局選項參數:

支持的命令大體上分為數據類操作和非數據類操作兩類。
etcdctl數據類操作命令:

etcdctl非數據類操作命令:

1.數據類操作
數據類操作圍繞對鍵值和目錄CRUD(符合REST風格的一套操作:Create)完整生命周期的管理。
CRUD即Create、Read、Update、Delete,是符合REST風格的一套API操作規范。
Etcd在鍵的組織上采用了層次化的空間結構(類似於文件系統中目錄的概念),用戶指定的鍵可以為單獨的名字。
例如,testkey放在根目錄/下面,也可以為指定目錄結構,如clusterl/node2/testkey,則將創建相應的目錄結構。
(1)set
為某個鍵設置定值

參數說明:
--ttl value 鍵值的超時時間(單位為秒),默認為0則為永不超時。
--swap-with-value value 若該鍵現在的值是value,則進行設置操作
--swap-with-index value 若該鍵現在的索引值是指定索引,則進行設置操作。
(2)get
獲取指定鍵的值:

當鍵不存在時報錯:

參數說明:
--sort 對返回結果進行排序
(3)update
當鍵存在時,更新內容:

當不存在時,報錯:

參數說明:
--ttl value 鍵值的超時時間(單位為秒),默認為0則為永不超時。
(4)mk
如果給定的鍵不存在,則創建一個新的鍵值:

如果鍵存在會直接報錯:

參數說明:
--in-order 在目錄<key>下創建按順序鍵
--ttl value 鍵值的超時時間(單位為秒),默認為0則為永不超時。
(5)rm
刪除某個鍵:

當鍵不存在的時候,報錯:

參數說明:
--dir 如果鍵是個空目錄或者是鍵值對則刪除
--recursive, -r 刪除目錄和所有子鍵
--with-value value 檢查現有的值是否匹配
--with-index value 檢查現有的index是否匹配
(6)watch
監測一個鍵值的變化,一旦鍵值發生更新,就會輸出最新的值並退出。

參數說明:
--forever, -f 一直監測,直到用戶按“ctrl+c”退出;
--after-index value 在指定index之前一直監測;
--recursive, -r 返回所有的鍵值和子鍵值;
(7)exec-watch
檢測一個鍵值的變化,一旦鍵值發生更新,就執行給定命令。
很多時候用戶實時根據鍵值更新本地服務的配置信息,並重新加載服務。
可以實現分布式應用配置的自動分發。
參數說明:
--after-index value 在指定index之前一直監測;
--recursive, -r 返回所有的鍵值和子鍵值;
(8)ls
列出目錄(默認為根目錄)下的鍵或者子目錄,默認不顯示子目錄中的內容。

參數說明:
--sort 將輸出結果排序
--recursive 如果目錄下有子目錄,則遞歸輸出其中的內容
-p 如果輸出為目錄,在最后添加‘/’進行分區
(9)mkdir
如果給定的目錄不存在,則創建一個新的鍵目錄:

如果鍵目錄存在,報錯:

參數說明:
--ttl value 鍵值的超時時間(單位為秒),默認為0則為永不超時。
(10)rmdir
刪除一個空目錄或者鍵值對,如目錄不為空則報錯:


(11)setdir
創建一個鍵目錄,無論存在與否:

(12)updatedir
更新一個已經存在的目錄屬性。

2.非數據類操作
(1)backup
備份Etcd的配置狀態數據目錄
選項:
--data-dir 要進行備份的Etcd的數據存放目錄
--backup-dir 備份數據到指定路徑


其中,snap為快照目錄,保存節點狀態文件,wal保存了數據庫預寫日志信息。
預寫日志要求數據庫在發生實際提交之前必須先將操作寫入日志,可以保障系統在崩潰后更具日志回復狀態。
(2)cluster-health
查看Etcd集群的健康狀態:

參數說明:
--forever, -f 每10秒檢查一次,直到手動終止
(3)member
通過list、add、remove等子命令列出、添加、刪除Etcd實例到Etcd集群中。
etcdctl member command [command options] [arguments...]
例如本地啟動一個Etcd服務實例后,可以用如下命令查看默認的成員實例:

(4)import
導入舊版本的快照文件到系統
(5)user
對用戶進行管理,包括一系列子命令
add 添加一個用戶
get 查詢用戶細節
list 列出所有用戶
remove 刪除用戶
grant 添加用戶到角色
revoke 刪除用戶角色
passwd 修改用戶密碼
默認情況下,需要先創建(啟用)root用戶作為etcd集群的最高權限管理員。

創建一個testuser用戶:

分配某些已有角色給用戶:

(6)role
對用戶角色進行管理
add 添加一個角色
get 查詢角色信息
list 列出所有用戶角色
remove 刪除用戶角色
grant 添加路徑到角色控制
revoke 刪除某路徑的角色用戶信息
默認帶有root、guest兩種角色,前者為全局最高權限。

(7)auth
是否啟用訪問驗證,enable為啟用,disable為禁用。
四、集群管理
Etcd的集群也采用了典型的“主從”模型,通過Raft協議來保證在一段時間內有一個節點為主節點,其它節點為從節點。
一旦主節點發生故障,其它節點可以自動再重新選舉出新的節點。
和其它分布式系統類似,急群眾節點個數推薦為基數個,最少為3個,此時(quorum為2),
越多節點自然能提供更多的冗余性,但同時會帶來寫數據能力的下降。
1.構建集群
構建集群無非是讓節點知道自己加入哪個集群,其它對等節點的訪問信息是什么。
Etcd支持兩種模式來構建集群:靜態配置和動態探測。
(1)靜態配置集群信息
靜態配置就是提取寫好集群中的有關信息。
三個節點的ip信息分別為:
172.16.16.15 172.16.0.4 172.16.0.15
通過如下命令來分別啟動各個節點上的etcd服務。
節點1: etcd --name infra0 --initial-advertise-peer-urls http://172.16.16.15:2380 --listen-peer-urls http://172.16.16.15:2380 --listen-client-urls http://172.16.16.15:2379,http://127.0.0.1:2379 --advertise-client-urls http://172.16.16.15:2379 --initial-cluster-token etcd-cluster-1 --initial-cluster infra0=http://172.16.16.15:2380,infra1=http://172.16.0.4:2380,infra2=http://172.16.0.15:2380 --initial-cluster-state new 節點2: etcd --name infra1 --initial-advertise-peer-urls http://172.16.0.4:2380 --listen-peer-urls http://172.16.0.4:2380 --listen-client-urls http://172.16.0.4:2379,http://127.0.0.1:2379 --advertise-client-urls http://172.16.0.4:2379 --initial-cluster-token etcd-cluster-1 --initial-cluster infra0=http://172.16.16.15:2380,infra1=http://172.16.0.4:2380,infra2=http://172.16.0.15:2380 --initial-cluster-state new 節點3: etcd --name infra2 --initial-advertise-peer-urls http://172.16.0.15:2380 --listen-peer-urls http://172.16.0.15:2380 --listen-client-urls http://172.16.0.15:2379,http://127.0.0.1:2379 --advertise-client-urls http://172.16.0.15:2379 --initial-cluster-token etcd-cluster-1 --initial-cluster infra0=http://172.16.16.15:2380,infra1=http://172.16.0.4:2380,infra2=http://172.16.0.15:2380 --initial-cluster-state new
啟動成功之后,可以在任意一個節點查看當前集群中的成員信息:
[root@centos002 ~]# etcdctl member list 1f146a283033baa3: name=infra2 peerURLs=http://172.16.0.15:2380 clientURLs=http://172.16.0.15:2379 isLeader=false 6de1f3013b32aaf9: name=infra1 peerURLs=http://172.16.0.4:2380 clientURLs=http://172.16.0.4:2379 isLeader=false d89aedc239d376e5: name=infra0 peerURLs=http://172.16.16.15:2380 clientURLs=http://172.16.16.15:2379 isLeader=true
還可以查看各個節點的健康狀態:
[root@centos002 ~]# etcdctl cluster-health member 1f146a283033baa3 is healthy: got healthy result from http://172.16.0.15:2379 member 6de1f3013b32aaf9 is healthy: got healthy result from http://172.16.0.4:2379 member d89aedc239d376e5 is healthy: got healthy result from http://172.16.16.15:2379 cluster is healthy
(2)動態發現
靜態配置的方法雖然簡單,但是如果節點的信息需要變動的時候,就需要手動修改。
可以通過動態發現的方法,讓集群自動更新節點信息。
要實現動態發現,首先需要一套支持動態發現的服務。
CoreOS提供了一個公開的Etcd發現服務,地址是:https://discovery.etcd.io。
這個網址會為創建的集群提供一個獨一無二的uuid,需要提供的唯一參數是節點的個數
[root@centos001 ~]# curl https://discovery.etcd.io/new?size=3 https://discovery.etcd.io/37435a480ba59aa81a682fee7f415856
分別在各個節點上指定服務發現地址信息,替換掉原先動態指定的節點列表。
節點1: etcd --name infra0 --initial-advertise-peer-urls http://172.16.16.15:2380 --listen-peer-urls http://172.16.16.15:2380 --listen-client-urls http://172.16.16.15:2379,http://127.0.0.1:2379 --advertise-client-urls http://172.16.16.15:2379 --initial-cluster-token etcd-cluster-1 --discovery https://discovery.etcd.io/37435a480ba59aa81a682fee7f415856 --initial-cluster-state new 節點2: etcd --name infra1 --initial-advertise-peer-urls http://172.16.0.4:2380 --listen-peer-urls http://172.16.0.4:2380 --listen-client-urls http://172.16.0.4:2379,http://127.0.0.1:2379 --advertise-client-urls http://172.16.0.4:2379 --initial-cluster-token etcd-cluster-1 --discovery https://discovery.etcd.io/37435a480ba59aa81a682fee7f4158560 --initial-cluster-state new 節點3: etcd --name infra2 --initial-advertise-peer-urls http://172.16.0.15:2380 --listen-peer-urls http://172.16.0.15:2380 --listen-client-urls http://172.16.0.15:2379,http://127.0.0.1:2379 --advertise-client-urls http://172.16.0.15:2379 --initial-cluster-token etcd-cluster-1 --discovery https://discovery.etcd.io/37435a480ba59aa81a682fee7f415856 --initial-cluster-state new
(3)DNS發現
dns發現主要通過dns服務來記錄集群中各節點的域名信息,各節點到dns服務中獲取相互的地址信息,從而建立集群。
即為每個節點指定同一個子域的域名,然后通過域名發現來自動注冊。例如:
infra0.example.com infra1.example.com infra2.example.com
則啟動參數中的集群節點列表信息可以替換為--discovery-srv example.com
2.集群參數配置
影響集群性能的因素可能有很多,包括時間同步、網絡抖動、存儲壓力、讀寫壓力等。
需要通過優化配置盡量減少這些因素的影響。
(1)時間同步
對於分布式集群來說,各個節點上的同步時鍾十分重要,
Etcd集群需要各個節點時鍾差異不超過1s。否則可能會導致Raft協議的異常。
可以修改/etc/ntp.conf這個配置文件,來指定ntp服務器的地址。
(2)心跳消息時間間隔和選舉時間間隔
對於Etcd集群來說,有兩個因素十分重要:心跳消息時間間隔和選舉時間間隔。
前者意味着主節點每隔多久來通過心跳消息通知從節點自身的存活狀態;
后者意味着從節點多久沒收到心跳通知后可以嘗試發起選舉自身為主節點。
顯然后者要比前者大,一般建議設為前者的5倍以上。
時間越短,發生故障后回復的越快,但心跳信息占用的計算和網絡資源也就越多。
默認情況下,心跳消息間隔為100ms。選舉時間間隔為1s。
可以在啟動服務的時候通過--heartbeat-interval和--election-timeout來指定。
當然也可以通過環境變量來指定。ETCD_HEARTBEAT_INTERVAL=100 ETCD_ELECTION_TIMEOUT=1000。
(3)snapshop頻率
Etcd會定期將數據的修改存儲為snapshop,默認情況下每10000次修改才會存一個snapshot。
在存儲的時候會有大量數據進行寫入,影響Etcd的性能。
啟動時通過--snapshot-count '100000'指定,也可以使用環境變量ETCD_SNAPSHOP_COUNT=2000 etcd來指定。
(4)修改節點
無論是添加、刪除還是遷移節點,都要一個一個的進行,並且確保先修改配置信息
(包括節點廣播的監聽地址、集群中節點列表),然后再進行操作。
例如要刪多個節點,當有主節點被刪除時,需要先刪掉一個,等集群中狀態穩定后(新的節點重新生成),再刪除另外節點。
要遷移或者替換節點的時候,先將節點從集群中刪除掉,等集群狀態重新穩定后,再添加上新的節點。
當然,使用舊節點的數據目錄會加快新節點的同步過程,但要保證這些數據是完整的,且是比較新的。
(5)節點恢復
Etcd集群中的節點會通過數據目錄來存放修改信息和集群配置。
一般來說,當某個節點出現故障的時候,本地數據已經過期甚至格式破壞。
如果只是簡單的重啟進程,容易造成數據的不一致。
這個時候保險的做法是先通過命令來刪除該節點,然后清空數據目錄,再重新作為空節點加入。
Etcd提供了-strict-reconfig-check選項,確保當集群狀態不穩定的時候拒絕對配置狀態的修改。
(6)重啟集群
極端情況下,集群中大部分節點都出現問題,需要重啟整個集群。
這個時候,最保險的做法就是找到一個數據記錄完整且比較新的節點,
先以它為唯一節點創建新的集群,然后將其他節點一個一個的添加進來。
五、Etcd架構與實現分析
1.為什么需要Etcd?
所有的分布式系統,都面臨的一個問題是多個節點之間的數據共享,這和團隊協作的道理是一樣的,成員可以分頭干活,
但總是需要共享一些必要的信息,比如誰是leader,都有那些成員,依賴任務之間的順序協調等。
所以分布式系統要么自己實現一個可靠的共享存儲來同步信息(比如Elasticsearch),要么依賴一個可靠的共享存儲,而Etcd就是這樣一個服務。
2.Etcd提供什么能力?
(1)提供存儲以及獲取數據的接口,它通過協議保證Etcd集群中的多個節點數據的強一致性,用於存儲元信息和共享配置。
(2)提供監聽機制,客戶端可以監聽某個key或者某些key的變更(v2和v3的機制不同),用於監聽和推送變更。
(3)提供key的過期以及續約機制,客戶端通過定時刷新來實現續約(v2和v3實現的機制也不一樣),用於集群監控以及服務注冊發現。
(4)提供原子的CAS(Compare-and-swap)和CAD(Compare-and-Delete)支持(v2通過接口參數實現,v3通過批量事物實現),用於分布式鎖以及leader選舉。
3.Etcd是如何實現一致性的?
(1)raft算法通過對不同的場景(選主,日志復制)設計不同的機制,雖然降低 了通用性(相對paxos),但同時也降低了復雜度,便於理解和實現。
(2)raft內置的選主協議是給自己用的,用於選出主節點,理解raft的選主機制的關鍵在於理解raft的時鍾周期以及超時機制。
(3)理解Etcd的數據同步的關鍵在於理解raft的日志同步機制。
Etcd實現raft的時候,充分利用go語言CSP並發模型和chan的魔法,想更進一步了解的話可以去閱讀源碼,下面簡單分析一下它的wal日志。

wal日志是二進制的,解析出來后是以上數據結構LogEntry。
其中第一個字段type,只有兩種,一種是0表示Normal,1表示ConfChange(ConfChange表示Etcd本身的配置變更同步,比如有新的節點加入等)。
第二個字段是term,每個term代表一個主節點的任期,每次主節點變更term就會變化。
第三個字段是index,這個序號是嚴格有序遞增的,代表變更序號。
第四個字段是二進制的data,將raft request對象的pb結構整個保存下。
Etcd源碼下有個tools/etcd-dump-logs,可以將wal日志dump成文本查看,可以協助分析raft協議。
raft協議本身不關心應用數據,也就是data中的部分,一致性都通過同步wal日志來實現,
每個節點將從主節點收到的data apply到本地的存儲,raft只關心日志的同步狀態,
如果本地存儲實現的有bug,比如沒有正確的將data apply到本地,可能會導致數據不一致。
4.Etcd v2與v3
Etcd v2與v3本質上是共享同一套raft協議代碼的兩個獨立應用,接口不一樣,存儲不一樣,數據相互隔離。
也就是說如果從Etcd v2升級到Etcd v3,原來v2的數據還是只能用v3的接口訪問,v3的接口創建的數據只能訪問通過v3的接口訪問。
Etcd v2存儲,Watch以及過期機制

Etcd v2是個純內存的實現,並未實時將數據寫入到磁盤,持久化機制很簡單,就是將store整合序列成json寫入文件。
數據在內存中是一個簡單的樹結構,比如以下結構數據存儲到Etcd中的結構如圖所示:

store中有一個全局currentindex,每次變更,index會加1,然后每個event都會關聯到currentindex。
當客戶端調用watch接口(參數中怎加wait參數)時,如果請求參數中有waitindex,
並且waitindex小於currentindex,則從EventHistory表中查詢index小於等於waitindex,並且和watch key匹配的event。
如果有數據,則直接返回。如果歷史表中沒有或者請求沒有帶waitindex,則放入WatchHub中,每個key會關聯以和watcher列表。
當有變更操作時,變更生成的event會放入EventHistory表中,同時通知和該key相關的watcher。
這里面有幾個影響使用的細節問題:
(1)EventHistory是有長度限制的,最長1000,也就是說,如果你的客戶端停了許久,
然后重新watch的時候,可能和該waitindex相關的event已經被淘汰了,這種情況下會丟失變更。
(2)如果通知watch的時候,出現了阻塞(每個watch的channel有100個緩沖空間),
Etcd會直接把watch刪除,也就是會導致wait請求的連接中斷,客戶端需要重新連接。
(3)Etcd store的每個node中保存了過期時間,通過定時機制進行清理。
Etcd v2的一些限制:
(1)過期時間只能設置到每個key上,如果多個key要保證生命周期一致則比較困難。
(2)watch只能watch某一個key以及其子節點(通過參數 recursive),不能進行多個watch。
(3)很難通過watch機制來實現完整的數據同步(有丟失變更的風險),所以當前的大多數使用方式是通過watch得知變更,
然后通過get重新獲取數據,並不完全依賴於watch的變更event。
