在分布式微服務架構中,一個應用可能由一組職責單一化的服務組成。這時候就需要一個注冊服務的機制,注冊某個服務或者某個節點是可用的,還需要一個發現服務的機制來找到哪些服務或者哪些節點還在提供服務。
在實際應用中,通常還都需要一個配置文件告訴我們一些配置信息,比如數據連接的地址,redis 的地址等等。但很多時候,我們想要動態地在不修改代碼的情況下得到這些信息,並且能很好地管理它們。
有了這些需求,於是發展出了一系列的開源框架,比如 ZooKeeper, Etcd, Consul 等等。
這些框架一般會提供這樣的服務:
服務注冊
主機名,端口號,版本號,或者一些環境信息。
服務發現
可以讓客戶端拿到服務端地址。
一個分布式的通用 k/v 存儲系統
基於 Paxos 算法或者 Raft 算法
領導者選舉 (Leader Election)
其它一些例子:
分布式鎖 (Distributed locking)
原子廣播 (Atomic broadcast)
序列號 (Sequence numbers)
…
我們都知道 CAP 是 Eric Brewer 提出的分布式系統三要素:
強一致性 (Consistency)
可用性 (Availability)
分區容忍性 (Partition Tolerance)
幾乎所有的服務發現和注冊方案都是 CP 系統,也就是說滿足了在同一個數據有多個副本的情況下,對於數據的更新操作達到最終的效果和操作一份數據是一樣的,但是在出現網絡分區(分區間的節點無法進行網絡通信)這樣的故障的時候,在節點之間恢復通信並且同步數據之前,一些節點將會無法提供服務(一些系統在節點丟失的情況下支持 stale reads )。
ZooKeeper 作為這類框架中歷史最悠久的之一,是由 Java 編寫。Java 在許多方面非常偉大,然而對於這種類型的工作還是顯得有些沉重,也正因為其復雜性和對新鮮事物的向往,我們第一時間就放棄了選擇它。
本文將從協議和應用層來比較 Etcd 和 Consul,並最終給出了筆者的偏好。
Etcd
Etcd 是一個使用 go 語言寫的分布式 k/v 存儲系統。考慮到它是 coreos 公司的項目,以及在 GitHub 上的 star 數量 (9000+),Google 著名的容器管理工具 Kuberbetes 就是基於 Etcd 的。我們最先考慮的就是它。
在介紹 Etcd 之前,我們需要了解一些基本的概念。我們知道在單台服務器單個進程中維護一個狀態是很容易的,讀寫的時候不會產生沖突。即便在多進程或者多線程環境中,使用鎖機制或者協程(coroutine)也可以讓讀寫有序地進行。但是在分布式系統中,情況就會復雜很多,服務器可能崩潰,節點間的機器可能網絡不通等等。所以一致性協議就是用來在一個集群里的多台機器中維護一個一致的狀態。
很多的分布式系統都會采用 Paxos 協議,但是 Paxos 協議難以理解,並且在實際實現中差別比較大。所以 Etcd 選擇了 Raft 作為它的一致性協議。Raft 是 Diego Ongaro 和 John Ousterhout 在 ‘In Search of an Understandable Consensus Algorithm’ 中提出的。它在犧牲很少可用性,達到相似功能的情況下,對 Paxos 做了很大的優化,並且比 Paxos 簡單易懂很多。
它主要集中在解決兩個問題:
領導者選舉(Leader Election)
Raft 先通過領導選舉選出一個 Leader,后續的一致性維護都由 Leader 來完成,這就簡化了一致性的問題。Raft 會保證一個時間下只會有一個 Leader,並且在超過一半節點投票的情況下才會被選為 Leader。當 Leader 掛掉的時候,新的 Leader 將會被選出來。
日志復制 (Log Replication)
為了維護狀態,系統會記錄下來所有的操作命令日志。Leader 在收到客戶端操作命令后,會追加到日志的尾部。然后 Leader 會向集群里所有其它節點發送AppendEntriesRPC請求,每個節點都通過兩階段提交來復制命令,這保證了大部分的節點都能完成。
在實際的應用中,一般 Etcd 集群以 5 個或者 7 個為宜,可以忍受 2 個或者 3 個節點掛掉,為什么不是越多越好呢?是出於性能的考慮,因為節點多了以后,日志的復制會導致 CPU 和網絡都出現瓶頸。
Etcd 的 API 比較簡單,可以對一個目錄或者一個 key 進行 GET,PUT,DELETE 操作,是基於 HTTP 的。Etcd 提供 watch 某個目錄或者某個 key 的功能,客戶端和 Etcd 集群之間保持着長連接 (long polling)。基於這個長連接,一旦數據發生改變,客戶端馬上就會收到通知,並且返回的結果是改變后的值和改變前的值,這一點在實際應用中會很有用(這也是后面的 Consul 的槽點之一)。
Etcd 的 watch 和在一般情況下不會漏掉任何的變更。因為 Etcd 不僅存儲了當前的鍵值對,還存儲了最近的變更記錄,所以如果一個落后於當前狀態的 watch 還是可以通過遍歷歷史變更記錄來獲取到所有的更新。Etcd 還支持 CompareAndSwap 這個原子操作,首先對一個 key 進行值比較,只有結果一致才會進行下一步的賦值操作。利用這個特性就像利用 x86 的 CAS 實現鎖一樣可以實現分布式鎖。
在 Etcd 中有個 proxy 的概念,它其實是個轉發服務器,啟動的時候需要指定集群的地址,然后就可以轉發客戶端的請求到集群,它本身不存儲數據。一般來說,在每個服務節點都會啟動一個 proxy,所以這個 proxy 也是一個本地 proxy,這樣服務節點就不需要知道 Etcd 集群的具體地址,只需要請求本地 proxy。之前提到過一個 k/v 系統還應該支持 leader election,Etcd 可以通過 TTL (time to live) key 來實現。
Consul
Consul 和 Etcd 一樣也有兩種節點,一種叫 client (和 Etcd 的 proxy 一樣) 只負責轉發請求,另一種是 server,是真正存儲和處理事務的節點。
Consul 使用基於 Serf 實現的 gossip 協議來管理從屬關系,失敗檢測,事件廣播等等。gossip 協議是一個神奇的一致性協議,之所以取名叫 gossip 是因為這個協議是模擬人類中傳播謠言的行為而來。要傳播謠言就要有種子節點,種子節點每秒都會隨機向其它節點發送自己所擁有的節點列表,以及需要傳播的消息。任何新加入的節點,就在這種傳播方式下很快地被全網所知道。這個協議的神奇就在於它從設計開始就沒想要信息一定要傳遞給所有的節點,但是隨着時間的增長,在最終的某一時刻,全網會得到相同的信息。當然這個時刻可能僅僅存在於理論,永遠不可達。
Consul 使用了兩個不同的 gossip pool,分別叫做 LAN 和 WAN,這是因為 Consul 原生支持多數據中心。在一個數據中心內部,LAN gossip pool 包含了這個數據中心所有的節點,包括 proxy 和 servers。WAN pool 是全局唯一的,所有數據中心的 servers 都在這個 pool 中。這兩個 pool 的區別就是 LAN 處理的是數據中心內部的失敗檢測,事件廣播等等,而 WAN 關心的是跨數據中心的。除了 gossip 協議之外,Consul 還使用了 Raft 協議來進行 leader election,選出 leader 之后復制日志的過程和 Etcd 基本一致。
回到應用層面上來說,Consul 更像是一個 full stack 的解決方案,它不僅提供了一致性 k/v 存儲,還封裝了服務發現,健康檢查,內置了 DNS server 等等。這看上去非常美好,簡直可以開箱即用。於是,我們初步選定了 Consul 作為我們的服務發現和動態配置的框架。但現實往往是冰冷的,在深入研究它的 API 之后發現了比較多的坑,可能設計者有他自己的考慮。
在使用獲取所有 services 的 API 的時候返回的只是所有服務的名字和 tag,如果想要每個服務的具體信息,你還需要挨個去請求。這在客戶端就會十分不方便,因為在筆者看來,獲取所有服務的列表以及具體信息應該是一個很常見的需求,並且請求的次數越少越好。
如果 watch 服務是否有變化,當值發生改變的時候,返回的結果居然是相當於重新讀取所有 services,沒有能夠體現出服務信息的變化,這會導致客戶端很難進行更新操作。
健康檢查是 Consul 的內置功能,在注冊一個服務的時候可以加上自定義的健康檢查,這聽上去很不錯。但是如果你忘記給你某個服務加上健康檢查,那它在各種 API 的返回結果中變得難以預料。
…
結語
在折騰了數天之后,最終我們決定回歸 Etcd,事實證明這個決定是正確的。我們基於 Etcd 實現了穩定的服務發現和動態配置功能,當然還有一些比較細節的問題沒有在文中闡述,歡迎一起探討。