1. 程式人生 > >SERF中去中心化系統的原理和實現

SERF中去中心化系統的原理和實現

楊諭黔,FreeWheel基礎架構部高階軟體工程師。 目前主要從事服務化框架、容器化平臺相關的研發與推廣。關注和感興趣的技術主要有Golang, Docker, Kubernetes等。

serf是出自Hashicorp的開源專案, 實現了去中心化的gossip(八卦)協議,其中gossip協議定義了一種類似病毒感染的訊息傳播過程。 一些著名的開源專案,如DockerConsul,網路管理和服務發現的核心元件是基於serf實現的,然而它們背後的serf似乎還鮮為人知,一方面其複雜的理論以及不完善的文件讓人望而卻步;另一方面,gossip協議天然的資料弱一致性也制約了serf的使用場景。

本文希望從

serf背後的分散式系統理論和部分原始碼實現出發,為專案中serf的使用帶來一些啟發,分為四個部分:

·serf初體驗

·serf背後的分散式系統理論

·serf部分原始碼分析

·serf叢集引數與調優

SERF初體驗

serf提供了一種輕量級的方式來管理去中心化叢集,並基於這個叢集提供了UserEventQuery等介面,處理一些使用者層的事件,如服務發現、自動化部署等。本節通過具體的例子,介紹serf中的一些基本特性。

叢集管理

基於serf搭建去中心化叢集非常簡單:在每個節點上啟動serf agent,然後通過每個agent上的rpc介面(或使用serf命令列工具),就可以讓agent快速建立連線並形成叢集。

1.1.1 啟動獨立的serf agent

640?wx_fmt=png&wxfrom=5&wx_lazy=1

1.1.1 中啟動了三個獨立的serf agent,配置分別為:

Node

Bind Address

RPC Address

node1

127.0.0.1:5001

127.0.0.1:7473

node2

127.0.0.1:5002

127.0.0.1:7474

node3

127.0.0.1:5003

127.0.0.1:7475

順便提一下,agent啟動時需要指定兩個addressbind addressrpc address,分別提供叢集間的通訊介面和客戶端操作叢集的介面,serf預設採用UDP來廣播gossip訊息,因此在實際網路中部署時,需要為Bind Address配置相應的防火牆規則。另外還有一個

advertise address,如果agent跑在容器等需要NAT的網路環境中時,可以使用advertise address對叢集中其他節點暴露自己。

啟動了serf agent之後,這些agent還沒有建立叢集,相互之間也沒有通訊,緊接著要做的是讓這些agent連線起來成為一個叢集,圖1.1.1中啟動了三個節點,接下來分兩步讓這三個節點建立連線,形成去中性化叢集:

·node2 join node1 (如圖1.1.2

·node1 join node3 (如圖1.1.3

1.1.2 node2 join node1

640?wx_fmt=png

再次說明一下,rpc addr是客戶端控制agent的介面,而bind addr才是叢集間互相通訊的介面,因此這裡node2 join node1,實際上是通過node2rpc介面告訴node2join node1,它的bind address127.0.0.1:5001”。

1.1.3 node1 join node3

640?wx_fmt=png

經過圖1.1.21.1.3中的簡單兩步,我們就建立了node1,2,3間的去中心化叢集,每個節點都維護了叢集的狀態資訊。有人可能好奇叢集中如果有節點掛掉,重啟的話,其他節點能不能再次找到它呢?為了模擬這種故障重啟的行為,圖1.1.4中直接將node2程序kill掉。

1.1.4 直接killnode2

640?wx_fmt=png

很快叢集中的節點就發現node2可能掛了,然後就開始了suspect的過程,大家互相傳播了幾次node2掛掉的“八卦訊息”之後,就判定這個節點很可能真的掛了,就開始定期去嘗試重新連線這個節點,讓它重新加入叢集。

1.1.5 重新啟動node2

640?wx_fmt=png

1.1.5node2重啟後,自動加入到叢集中來了,叢集對每個節點都抱著“不拋棄不放棄”的原則,每個節點都可能承擔故障節點的恢復,整個叢集不再依賴有限的幾個master節點。

User EventQuery

UserEventQueryserf基於去中心化叢集提供的高效訊息封裝。UserEvent是一些單向的不需要叢集反饋的訊息,而Query是雙向的,需要叢集給出反饋(QueryResponse)的訊息。向叢集傳送訊息可以針對任意節點進行,然後節點間可以互相傳播這些訊息。

serf中可以為EventQuery指定Handlerserf命令列中定義的Handler可以是任何一條shell命令,serf會將Event/Query的一些上下文以環境變數形式傳入;如果在Go程式中引入serf API,也可以實現自定義的Handler,非常靈活。

1.2.1 node1,2,3指定不同的Event Handler,並建立節點之間的連線形成叢集

640?wx_fmt=png

1.2.1中為node1,2,3分別指定了EventQueryHandlerTag來宣告訊息的廣播範圍

Node

Tag

Event Handler

Query Handler

node1

role=apiserver

echo node1 >> node1.log

echo hello,node1

node2

role=ui

echo node2 >> node2.log

echo hello,node2

node2

role=apiserver

echo node3 >> node3.log

echo hello,node3

1.2.2中傳送一個log Event,所有節點都會處理該Event,向對應的日誌檔案中寫入一段文字

640?wx_fmt=png

1.2.3中向role=ui的節點發送一個Queryagent處理完訊息之後將結果傳回客戶端

640?wx_fmt=png

為了保證一般性,這裡選擇向叢集中的node1傳送該Query,而Query實際的處理節點是node2,最終可見返回的結果和日誌中只有node2處理了該資訊。

通過UserEvent/QueryTag的組合,可以實現很多靈活的監控、日誌收集和自動化運維工具。

小結

本節中的一些例子展示了serf的擴充套件性和EventQuery的使用場景,然而這些問題似乎在中心化的系統裡也完全可以解決,那麼去中心化叢集的到底有什麼優勢讓DockerConsul選擇用serf來實現其核心的服務發現和網路配置管理模組?

常見的中心化叢集中,叢集的狀態是由有限的master節點維護的,通常使用heart-beat協議來更新和同步masterslave節點之間的狀態和配置資訊:

·master需要slaveheart-beat來維護slave的狀態

·slave需要定時從master同步叢集的配置變化

在叢集規模較小時,去中心化叢集與中心化叢集相比並沒有明顯優勢。

去中心化系統更多的是面向大規模分散式平臺,當叢集規模增加到數萬甚至數十萬節點時,主從模型的資訊傳播給節點和網路都會帶來不小的負載,叢集狀態管理的效率和正確性也會隨著規模的增大而下降,這就是為什麼很多基於“主-從”模型的中心化系統都有叢集規模的上限。

例如Kubernetes的叢集規模上限為6k左右(v1.6+的官方資料),原因很簡單,因為Kubernetes整個系統都受限於它背後的etcd叢集的處理能力(QPS為萬級別),在Kubernetes的上一個版本,叢集規模上限只有2kv1.6+裡面容量增加可能是因為etcd3裡面引入了rpc的介面,通訊更高效了),如果超過這個規模,叢集狀態就無法保證有效同步,可能會出現各種不預期的行為。而基於serf的去中心化叢集就沒有這種限制,叢集處理能力可以做到近似線性的擴充套件,效率和可靠性基本不受叢集規模的影響。

從這個角度來看來,Docker的叢集管理潛力比今天競爭者如Kubernetes要大得多,基於serf去中心化的Docker Swarm平臺基本不受叢集規模的影響,在節點規模達到數十萬時依舊可以很好的運轉,這是Kubernetes之類的平臺所做不到的。

serf背後的分散式系統理論

serf可以從功能上,自上往下分為三個層次:

·客戶端介面:提供rpc介面來處理客戶端對serf叢集的輸入,和格式化的輸出

·訊息中介軟體:封裝了各種訊息,包含叢集管理和UserEventQuery等的處理邏輯

·gossip協議層:封裝了gossip協議和基本的叢集管理操作

本節簡單介紹gossip協議層背後的分散式系統理論。

vivaldi algorithm

在去中心化系統中,資訊是通過節點之間互相傳播完成的,如果每個訊息都向整個叢集廣播,會形成嚴重的網路風暴,這是需要嚴格避免的。那麼如何在避免網路風暴的前提下,同時保證訊息傳遞的效率和可靠性呢?Vivaldi演算法就是為了解決這個問題提出的。

Vivaldi演算法通過啟發式的學習演算法,在叢集通訊的過程中計算節點之間的RTT,並動態學習網路的拓撲結構,每個節點可以選擇離自己“最近”的N個節點進行廣播,這樣就可以最大程度減少網路風暴的出現,調整N可以修改資訊的傳播速度和效率以及對網路頻寬的影響。

SWIM協議

SWIM(Scalable Weakly-consistentInfection-style Process Group Membership Protocol)是在去中心化系統中節點狀態監測的協議,定義了FailureSuspicion等狀態的監測協議和節點之間的訊息傳輸模型。節點之間可以相互“告知”叢集中所有節點的狀態,當出現短暫的節點狀態失效時,叢集中會互相同步suspicion(疑似失效)狀態並持續觀察該“問題”節點的狀態,並在多個節點都確認了該失效節點的狀態後最終更新其狀態為Failure,這也減少了叢集中因為偶然網路環境干擾導致的狀態誤判。

Lamport timestamps

分散式系統中要解決的另外一個問題就是事件順序性的問題,無論是中心化系統還是去中心化系統,都需要判斷資料的時效性。物理時間是不可靠的,物理時間微小的偏差就可能造成程式中重大邏輯錯誤。

考慮有兩個客戶端AB,發請求獲取資料庫的鎖,那麼資料庫伺服器應該把鎖給誰呢?為了公平起見,通常使用客戶端發請求的時間來判斷“誰先誰後”,那麼緊接著需要面臨的問題就是物理時鐘不可靠,不可靠的物理時鐘可能導致本該分配給A的鎖卻最終被分配給了B。在分散式系統中,事件順序的判斷就變得尤為關鍵。

試想在分散式系統中,如果一個節點短暫的失效,斷開了叢集,稍後又加入叢集,那麼其之前可能記憶了自己的一些狀態,重新加入集群后可能會廣播一些過期和重複的訊息,如果叢集中無法識別訊息的時效性,那麼這些過期的、重複的訊息可能就會被重複、錯誤的執行。

Lamport Timestamp為叢集中每個事件都設定一個物理時鐘無關的邏輯時鐘,通過一種弱一致性的手段判斷事件的因果關係。節點可以通過Lamport Timestamp過濾重複、過期的事件。

serf部分原始碼分析

前面介紹的例子中,通過使用serf提供的命令列工具來管理叢集和訊息。在Go程式中,可以使用serf為應用提供原生的叢集管理和去中心化的訊息中介軟體。

Docker libnetwork中就使用了serf來管理容器化部署相關的ARP配置,在我看來這種比較野的路子確實需要一些“腦洞”。

本節總結了serf的部分原始碼實現,希望能為對serf感興趣的團隊的技術人提供一些思路。

memberlist(https://github.com/hashicorp/memberlist)封裝了serf中的gossip協議層,並暴露了一些介面,讓上層的應用與gossip協議層進行互動,本節介紹使用serf的原始碼(d787b2e8f72b5da48c43b84c2617234401084050)

serf自上往下劃分為兩個層次:

·serf層:實現對協議層介面的封裝,包含對叢集狀態管理以及EventQuery的封裝

·gossip協議層:處理叢集檢測、廣播、以及資訊保安相關的問題

其中gossip協議層暴露了Delegate介面,serf層通過實現Delegate介面來擴充套件協議層的功能。

3.1.1 Delegate的定義

640?wx_fmt=png

Delegate介面提供了5個回撥函式介面,下面介紹serf中的實現。

NodeMeta

serf中可以為每個節點設定具體的tag,用來過濾叢集中節點。

3.2.1 NodeMeta將節點的tag資訊釋出到叢集中

640?wx_fmt=pngNotifyMsg

NotifyMsgserf中整個訊息中介軟體的核心介面,gossip協議層所有訊息都會回撥NotifyMsg

3.3.1 NotifyMsg實現

640?wx_fmt=png

這裡只關注兩種使用者層訊息的處理機制,即UserEventQuery(QueryResponse),其中UserEvent實現了叢集中不需要反饋的、單向通訊,QueryQueryResponse實現了叢集中的雙向通訊。

先看一下當節點收到一個事件時的處理邏輯。

3.3.2 handleUserEvent接收事件並進行預處理 

640?wx_fmt=png

在傳遞給EventCh之前handlerUserEvent做了一些預處理:

·捨棄過期事件

·捨棄已經處理過的事件

·接收一個事件並加入recent cache(以便識別該事件是否在當前節點已經被處理過)

·將事件通過channel傳送給後續的Handler

順帶說一下serf中“最近處理過事件”的快取機制,serf初始化一個節點時會定義一個最近事件的緩衝池,是N個不限長度的佇列,快取了N個連續Lamport Timestamp的事件,通過對其Lamport TimestampN的模來獲取快取中佇列,並加入佇列尾。

handleQuery傳送資料時的邏輯和handleUserEvent是一樣的,這裡不再贅述。

定義訊息有效性是“去中心化”叢集中訊息正確性的重要保障,“去中心化”叢集中隨時可以因為各種原因導致叢集中一些節點和叢集斷開,當它們再次加入集群后有可能本地的一些狀態沒來得及與叢集同步,就會出現廣播了過期和重複訊息的情況。對過期和重複訊息的容錯機制是“去中心化”叢集需要具備的關鍵能力之一。

另外值得一提的是,serf配置中的幾個channel,上面的程式碼中涉及其中的EventCh,既所有的UserEvent會被轉發至這個channel,而後續的處理邏輯,即自定義的handler就可以來處理這些事件,這就是前面提到的serf中可以自定義event handler的問題。

serf叢集引數與調優

構建serf叢集涉及以下幾個變數,實際應用中需要針對叢集規模和當前的網路環境對這些引數進行調整,讓叢集獲得較好的收斂速率:

·gossip interval: 向叢集廣播訊息的時間間隔

·gossip fanout: 控制gossip協議層向叢集中多少個相鄰節點廣播訊息

640?wx_fmt=jpeg

4.1 serf叢集收斂模擬

640?wx_fmt=png

可見叢集收斂速率幾乎不受受叢集規模影響,即使叢集節點數達到10w,叢集也能在數秒內快速收斂。

注:這裡的模擬結果是針對特定的平臺引數,即丟包率,節點失效概率等引數進行的,實際應用中可以利用這裡給出的公式,結合平臺實際情況進行模擬。

總結

在現代分散式系統中,無論是工具還是應用模組,相互之間通訊的能力顯得越來越重要,serf提供了一種弱一致性的溝通渠道,讓應用模組之間直接“溝通”,提升整個軟體平臺和服務的自主性。serf對我的吸引力來自它的兩方面潛力:

·簡單、高效、可靠的去中心化叢集管理

·可以和Go程式整合,為Go程式模組提供原生的、不依賴外部儲存的叢集管理和通訊能力

儘管如此,serf和去中心化系統不是“銀彈”,在生產中應用serf時需要進行周詳的考慮和嚴謹的規劃,還需要針對平臺具體情況對叢集引數進行調整。相信serf和去中心繫統在未來會為技術領域創造新的機遇和挑戰。