from http://www.infoq.com/cn/articles/etcd-interpretation-application-scenario-implement-principle?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=global
隨著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的場景默認處理的數據都是控制數據,對于應用數據,只推薦數據量很小,但是更新訪問頻繁的情況。
服務發現要解決的也是分布式系統中最常見的問題之一,即在同一個分布式集群中的進程或服務,要如何才能找到對方并建立連接。本質上來說,服務發現就是想要了解集群中是否有進程在監聽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 分布式隊列
通過etcd來進行監控實現起來非常簡單并且實時性強。
前面幾個場景已經提到Watcher機制,當某個節點消失或有變動時,Watcher會第一時間發現并告知用戶。
節點可以設置TTL key,比如每隔30s發送一次心跳使代表該機器存活的節點繼續存在,否則節點消失。
這樣就可以第一時間檢測到各節點的健康狀態,以完成集群的監控要求。
另外,使用分布式鎖,可以完成Leader競選。這種場景通常是一些長時間CPU計算或者使用IO操作的機器,只需要競選出的Leader計算或處理一次,就可以把結果復制給其他的Follower。從而避免重復勞動,節省計算資源。
這個的經典場景是搜索系統中建立全量索引。如果每個機器都進行一遍索引的建立,不但耗時而且建立索引的一致性不能保證。通過在etcd的CAS機制同時創建一個節點,創建成功的機器作為Leader,進行索引計算,然后把計算結果分發到其它節點。
圖9 Leader競選
閱讀了“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。
圖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節點以確認數據提交,最后進行數據的提交,再次同步。
獲得了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 屬性的方式,你可以用環境變量的方式代替。
通過自發現方式啟動集群必須要提供集群大小,這樣有助于用戶確定集群實際啟動的節點數量。
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來定位數據。
etcd作為一個高可用鍵值存儲系統,天生就是為集群化而設計的。由于Raft算法在做決策時需要多數節點的投票,所以etcd一般部署集群推薦奇數個節點,推薦的數量為3、5或者7個節點構成一個集群。
etcd有三種集群化啟動的配置方案,分別為靜態配置啟動、etcd自身服務發現、通過DNS進行服務發現。
通過配置內容的不同,你可以對不同的方式進行選擇。值得一提的是,這也是新版etcd區別于舊版的一大特性,它摒棄了使用配置文件進行參數配置的做法,轉而使用命令行參數或者環境變量的做法來配置參數。
這種方式比較適用于離線環境,在啟動整個集群之前,你就已經預先清楚所要配置的集群大小,以及集群上各節點的地址和端口信息。那么啟動時,你就可以通過配置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命令進行操作。
通過自發現的方式啟動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。
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來啟動。
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可以配合多塊網卡同時監聽不同網絡下的請求。
etcd集群啟動完畢后,可以在運行的過程中對集群進行重構,包括核心節點的增加、刪除、遷移、替換等。運行時重構使得etcd集群無須重啟即可改變集群的配置,這也是新版etcd區別于舊版包含的新特性。
只有當集群中多數節點正常的情況下,你才可以進行運行時的配置管理。因為配置更改的信息也會被etcd當成一個信息存儲和同步,如果集群多數節點損壞,集群就失去了寫入數據的能力。所以在配置etcd集群數量時,強烈推薦至少配置3個核心節點。
當你節點所在的機器出現硬件故障,或者節點出現如數據目錄損壞等問題,導致節點永久性的不可恢復時,就需要對節點進行遷移或者替換。當一個節點失效以后,必須盡快修復,因為etcd集群正常運行的必要條件是集群中多數節點都正常工作。
遷移一個節點需要進行四步操作:
暫停正在運行著的節點程序進程
把數據目錄從現有機器拷貝到新機器
使用api更新etcd中對應節點指向機器的url記錄更新為新機器的ip
使用同樣的配置項和數據目錄,在新的機器上啟動etcd。
增加節點可以讓etcd的高可用性更強。舉例來說,如果你有3個節點,那么最多允許1個節點失效;當你有5個節點時,就可以允許有2個節點失效。同時,增加節點還可以讓etcd集群具有更好的讀性能。因為etcd的節點都是實時同步的,每個節點上都存儲了所有的信息,所以增加節點可以從整體上提升讀的吞吐量。
增加一個節點需要進行兩步操作:
在集群中添加這個節點的url記錄,同時獲得集群的信息。
使用獲得的集群信息啟動新etcd節點。
有時你不得不在提高etcd的寫性能和增加集群高可用性上進行權衡。Leader節點在提交一個寫記錄時,會把這個消息同步到每個節點上,當得到多數節點的同意反饋后,才會真正寫入數據。所以節點越多,寫入性能越差。在節點過多時,你可能需要移除一個或多個。
移除節點非常簡單,只需要一步操作,就是把集群中這個節點的記錄刪除。然后對應機器上的該節點就會自動停止。
當集群超過半數的節點都失效時,就需要通過手動的方式,強制性讓某個節點以自己為Leader,利用原有數據啟動一個新集群。
此時你需要進行兩步操作。
備份原有數據到新機器。
使用-force-new-cluster加備份的數據重新啟動節點
注意:強制性重啟是一個迫不得已的選擇,它會破壞一致性協議保證的安全性(如果操作時集群中尚有其它節點在正常工作,就會出錯),所以在操作前請務必要保存好數據。
Proxy模式也是新版etcd的一個重要變更,etcd作為一個反向代理把客戶的請求轉發給可用的etcd集群。這樣,你就可以在每一臺機器都部署一個Proxy模式的etcd作為本地服務,如果這些etcd Proxy都能正常運行,那么你的服務發現必然是穩定可靠的。
圖11 Proxy模式示意圖
所以Proxy并不是直接加入到符合強一致性的etcd集群中,也同樣的,Proxy并沒有增加集群的可靠性,當然也沒有降低集群的寫入性能。
那么,為什么要有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模式有轉發代理功能,而不會進行角色轉換。
從代碼中可以看到,Proxy模式的本質就是起一個HTTP代理服務器,把客戶發到這個服務器的請求轉發給別的etcd節點。
etcd目前支持讀寫皆可和只讀兩種模式。默認情況下是讀寫皆可,就是把讀、寫兩種請求都進行轉發。而只讀模式只轉發讀的請求,對所有其他請求返回501錯誤。
值得注意的是,除了啟動過程中因為設置了proxy參數會作為Proxy模式啟動。在etcd集群化啟動時,節點注冊自身的時候監測到集群的實際節點數量已經符合要求,那么就會退化為Proxy模式。
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節點申請這個信息)。所以,為了最大化集群的安全性,一旦有任何數據損壞或丟失的可能性,你就應該把這個節點從集群中移除,然后加入一個不帶數據目錄的新節點。
WAL(Write Ahead Log)最大的作用是記錄了整個數據變化的全部歷程。在etcd中,所有數據的修改在提交前,都要先寫入到WAL中。使用WAL進行數據的存儲使得etcd擁有兩個重要功能。
故障快速恢復: 當你的數據遭到破壞時,就可以通過執行所有WAL中記錄的修改操作,快速從最原始的數據恢復到數據損壞前的狀態。
數據回滾(undo)/重做(redo):因為所有的修改操作都被記錄在WAL中,需要回滾或重做,只需要方向或正向執行日志中的操作即可。
在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節點狀態,當前的任期編號以及數據項位置信息。
從代碼邏輯中可以看到,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個歷史文件。
新版etcd中,raft包就是對Raft一致性算法的具體實現。關于Raft算法的講解,網上已經有很多文章,有興趣的讀者可以去閱讀一下Raft算法論文非常精彩。本文則不再對Raft算法進行詳細描述,而是結合etcd,針對算法中一些關鍵內容以問答的形式進行講解。有關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次數據寫入。節點越多,由于數據同步涉及到網絡延遲,會根據實際情況越來越慢,而讀性能會隨之變強,因為每個節點都能處理用戶請求。
在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。
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的請求都以事件的形式存儲在事件隊列中等待處理。
通過從應用場景到源碼分析的一系列回顧,我們了解到etcd并不是一個簡單的分布式鍵值存儲系統。它解決了分布式場景中最為常見的一致性問題,為服務發現提供了一個穩定高可用的消息注冊倉庫,為以微服務協同工作的架構提供了無限的可能。相信在不久的將來,通過etcd構建起來的大型系統會越來越多。
孫健波,浙江大學SEL實驗室碩士研究生,目前在云平臺團隊從事科研和開發工作。浙大團隊對PaaS、Docker、大數據和主流開源云計算技術有深入的研究和二次開發經驗,團隊現將部分技術文章貢獻出來,希望能對讀者有所幫助。
https://github.com/coreos/etcd
https://groups.google.com/forum/#!topic/etcd-dev/wmndjzBNdZo
http://jm-blog.aliapp.com/?p=1232
http://progrium.com/blog/2014/07/29/understanding-modern-service-discovery-with-docker/
http://devo.ps/blog/zookeeper-vs-doozer-vs-etcd/
http://jasonwilder.com/blog/2014/02/04/service-discovery-in-the-cloud/
http://www.infoworld.com/article/2612082/open-source-software/has-apache-lost-its-way-.html
http://en.wikipedia.org/wiki/WAL
http://www.infoq.com/cn/articles/coreos-analyse-etcd
http://www.activestate.com/blog/2014/05/service-discovery-solutions