深入淺出etcd系列 – 心跳和選舉

雲容器大師發表於2018-12-17

作者:寶爺 校對:DJ

1、緒論

etcd作為華為雲PaaS的核心部件,實現了PaaS大多陣列件的資料持久化、叢集選舉、狀態同步等功能。如此重要的一個部件,我們只有深入地理解其架構設計和內部工作機制,才能更好地學習華為雲Kubernetes容器技術,笑傲雲原生的“江湖”。本系列將從整體框架再細化到內部流程,對etcd的程式碼和設計進行全方位解讀。本文是《深入淺出etcd》系列的第二篇,重點解析etcd的心跳和選舉機制,下文所用到的程式碼均基於etcd v3.2.X版本。

另,由華為雲容器服務團隊傾情打造的《雲原生分散式儲存基石:etcd深入解析》一書已正式出版,各大平臺均有發售,購書可瞭解更多關於分散式儲存和etcd的相關內容!

 

2、什麼是etcd的選舉

選舉是raft共識協議的重要組成部分,重要的功能都將是由選舉出的leader完成。不像Paxos,選舉對Paxos只是效能優化的一種方式。選舉是raft叢集啟動後的第一件事,沒有leader,叢集將不允許任何的資料更新操作。選舉完成以後,叢集會通過心跳的方式維持leader的地位,一旦leader失效,會有新的follower起來競選leader。

 

3、etcd選舉詳細流程

選舉的發起,一般是從Follower檢測到心跳超時開始的,v3支援客戶端指定某個節點強行開始選舉。選舉的過程其實很簡單,就是一個candidate廣播選舉請求,如果收到多數節點同意就把自己狀態變成leader。下圖是選舉和心跳的詳細處理流程。我們將在下文詳細描述這個圖中的每個步驟。

3.1 tick

raftNode的建立函式newRaftNode會建立一個Ticker。傳入的heartbeat預設為100ms,可以通過--heartbeat-interval配置。 

這裡要介紹一下程式碼中出現的幾個變數,我把這幾個變數都翻譯成XXX計數,是因為這些值都是整數,初始化為0,每次tick完了以後會遞增1。因此實際這是一個計數。也就是說實際的時間是這個計數值乘以tick的時間。

1. 選舉過期計數(electionElapsed):主要用於follower來判斷leader是不是正常工作,如果這個值遞增到大於隨機化選舉超時計數(randomizedElectionTimeout),follower就認為leader已掛,它自己會開始競選leader。

2. 心跳過期計數(heartbeatElapsed):用於leader判斷是不是要開始傳送心跳了。只要這個值超過或等於心跳超時計數(heartbeatTimeout),就會觸發leader廣播heartbeat資訊。 

3. 心跳超時計數(heartbeatTimeout):心跳超時時間和tick時間的比值。當前程式碼中是寫死的1。也就是每次tick都應該傳送心跳。實際上tick的週期就是通過--heartbeat-interval來配置的。 

4. 隨機化選舉超時計數(randomizedElectionTimeout):這個值是一個每次任期都不一樣的隨機值,主要是為了避免分裂選舉的問題引入的隨機化方案。這個時間隨機化以後,每個競選者傳送的競選訊息的時間就會錯開,避免了同時多個節點同時競選。從程式碼中可以看到,它的值是[electiontimeout, 2*electiontimeout-1] 之間,而electionTimeout就是下圖中的ElectionTicks,是ElectionMs相對於TickMs的倍數。ElectionMs是由--election-timeout來配置的,TickMs就是--heartbeat-interval。 

raftNode的start()方法啟動的協程中,會監聽ticker的channel,呼叫node的Tick方法,該方法往tickc通道中推入一個空物件。(流程圖中1) 

node啟動時是啟動了一個協程,處理node的裡的多個通道,包括tickc,呼叫tick()方法。該方法會動態改變,對於follower和candidate,它就是tickElection,對於leader和,它就是tickHeartbeat。tick就像是一個etcd節點的心臟跳動,在follower這裡,每次tick會去檢查是不是leader的心跳是不是超時了。對於leader,每次tick都會檢查是不是要傳送心跳了。

3.2 傳送心跳

當叢集已經產生了leader,則leader會在固定間隔內給所有節點傳送心跳。其他節點收到心跳以後重置心跳等待時間,只要心跳等待不超時,follower的狀態就不會改變。 具體的過程如下:

1. 對於leader,tick被設定為tickHeartbeat,tickHeartbeat會產生增長遞增心跳過期時間計數(heartbeatElapsed),如果心跳過期時間超過了心跳超時時間計數(heartbeatTimeout),它會產生一個MsgBeat訊息。心跳超時時間計數是系統設定死的,就是1。也就是說只要1次tick時間過去,基本上會傳送心跳訊息。傳送心跳首先是呼叫狀態機的step方法。(流程圖中2) 

2. step在leader狀態下為stepLeader(),當收到MsgBeat時,它會呼叫bcastHeartbeat()廣播MsgHeartbeat訊息。構造MsgHeartbeat型別訊息時,需要在Commit欄位填入當前已經可以commit的訊息index,如果該index大於peer中記錄的對端節點已經同步的日誌index,則採用對端已經同步的日誌index。Commit欄位的作用將在接收端處理訊息時詳細介紹。(流程圖中3)

3. send方法將訊息append到msgs陣列中。(流程圖中4)

4. node啟動的協程會收集msgs中的訊息,連同當前未持久化的日誌條目、已經確定可以commit的日誌條目、變化了的softState、變化了的hardState、readstate一起打包到Ready資料結構中。這些都是會引起狀態機變化的,所以都封裝在一個叫Ready的結構中,意思是這些東西都已經沒問題了,該持久化的持久化,該傳送的傳送。(流程圖中5)

5. 還是raftNode.start()啟動的那個協程,處理readyc通道。如果是leader,會在持久化日誌之前傳送訊息,如果不是leader,則會在持久化日誌完成以後傳送訊息。(流程圖中6)

 

6. transport的Send一般情況下都是呼叫其內部的peer的send()方法傳送訊息。peer的send()方法則是將訊息推送到streamWriter的msgc通道中。

7. streamWriter有一個協程處理msgc通道,呼叫encode,使用protobuf將Message序列化為bytes陣列,寫入到連線的IO通道中。(流程圖中7)

8. 對方的節點有streamReader會接收訊息,並反序列化為Message物件。然後將訊息推送到peer的recvc或者propc通道中。(流程圖中8)

9. peer啟動時啟動了兩個協程,分別處理recvc和propc通道。呼叫Raft.Process處理訊息。EtcdServer是這個介面的實現。(流程圖中9)

10. EtcdServer判斷訊息來源的節點是否被刪除,沒有的話呼叫Step方法,傳入訊息,執行狀態機的步進。而接收heartbeat的節點狀態機正常情況下都是follower狀態。因此就是呼叫stepFollower進行步進。(流程圖中10)

follower對heatbeat訊息的處理是:先將選舉過期時間計數(electionElapsed)歸零。這個時間會在每次tickElection呼叫時遞增。如果到了electionTimeout,就會重新選舉。另外,我們還可以看到這裡handleHeartbeat中,會將本地日誌的commit值設定為訊息中帶的Commit。這就是第2步說到設定Commit的目的,heartbeat訊息還會把leader的commit值同步到follower。同時,leader在設定訊息的Commit時,是取它對端已經同步的日誌最新index和它自己的commit值中間較小的那個,這樣可以保證如果有節點同步比較慢,也不會把commit值設定成了它還沒同步到的日誌。

最後,follower處理完以後會回覆一個MsgHeartbeatResp訊息。

11. 回覆訊息的中間處理流程和心跳訊息的處理一致,因此不再贅述。leader收到回覆訊息以後,最後會呼叫stepLeader處理回覆訊息。(流程圖中11)

12. stepLeader收到回覆訊息以後,會判斷是不是要繼續同步日誌,如果是,就傳送日誌同步資訊。另外會處理讀請求,這部分的處理將在linearizable讀請求的流程中詳細解讀。

3.3 選舉

檢測到選舉超時的follower,會觸發選舉流程,具體的流程如下:

1. 依然從tick開始,對於follower(或candidate)。tick就是tickElection,它的做法是,首先遞增選舉過期計數(electionElapsed),如果選舉過期計數超過了選舉超時計數。則開始發起選舉。發起選舉的話,實際是建立一個MsgHup訊息呼叫狀態機的Step方法。(流程圖中13)

2. Step方法處理MsgHup訊息,檢視當前本地訊息中有沒有沒有作用到狀態機的配置資訊日誌,如果有的話,是不能競選的,因為叢集的配置資訊有可能會出現增刪節點的情況,需要保證各節點都起作用以後才能進行選舉操作。從圖上可以看到,如果有PreVote的配置,會有一個PreElection的分支。這個放在最後我們介紹。我們直接看campaign()方法,它首先將自己變成candidate狀態,becomeCandidate會將自己Term+1。然後拿到自己當前最新的日誌Term和index值。把這些都包在一個MsgVote訊息中,廣播給所有的節點。最新的日誌Term和index值是非常重要的,它能保證新選出來的leader中一定包含之前已經commit的日誌,不會讓已經commit的日誌被新leader改寫。這個在後面的流程中還會講到。(流程圖中14)

3. 選舉訊息傳送的流程和所有訊息的流程一樣,不在贅述。(流程圖中15)

4. 心跳訊息到了對端節點以後,進行相應的處理,最終會調到Step方法,進行狀態機步進。Step處理MsgVote方法的流程是這樣的:

    - 首先,如果選舉過期時間還沒有超時,將拒絕這次選舉請求。這是為了防止有些follower自己的原因沒收到leader的心跳擅自發起選舉。

    - 如果r.Vote已經設定了,也就是說在一個任期中已經同意了某個節點的選舉請求,就會拒絕選舉

    - 如果根據訊息中的LogTerm和Index,也就是第2步傳進來的競選者的最新日誌的index和term,發現競選者比當前節點的日誌要舊,則拒絕選舉。

    - 其他情況則贊成選舉。回覆一個贊成的訊息。(流程圖中16)

5. 競選者收到MsgVoteResp訊息以後,stepCandidate處理該訊息,首先更新r.votes。r.votes是儲存了選票資訊。如果同意票超過半數,則升級為leader,否則如果已經獲得超過半數的反對票,則變成follower。(流程圖中18)

 

4、PreVote

PreVote是解決因為某個因為網路分割槽而失效的節點重新加入叢集以後,會導致叢集重新選舉的問題。問題出現的過程是這樣的,假設當前叢集的Term是1,其中一個節點,比如A,它因為網路分割槽,接收不到leader的心跳,當超過選舉超時時間以後,它會將自己變成Candidate,這時候它會把它的Term值變成2,然後開始競選。當然這時候是不可能競選成功的。可是當網路修復以後,無論是它的競選訊息,還是其他的回覆訊息,都會帶上它的Term,也就是2。而這時候整個叢集裡其他機器的Term還是1,這時候的leader發現已經有比自己Term高的節點存在,它就自己乖乖降級為follower,這樣就會導致一次重新選舉。這種現象本身布常見,而且出現了也只是出現一次重選舉,對整個叢集的影響並不大。但是如果希望避免這種情況發生,依然是有辦法的,辦法就是PreVote。

PreVote的做法是:當叢集中的某個follower發現自己已經在選舉超時時間內沒收到leader的心跳了,這時候它首先不是直接變成candidate,也就不會將Term自增1。而是引入一個新的環境叫PreVote,我們就將它稱為預選舉吧。它會先廣播傳送一個PreVote訊息,其他節點如果正常執行,就回復一個反對預選舉的訊息,其他節點如果也失去了leader,才會有回覆贊成的訊息。節點只有收到超過半數的預選舉選票,才會將自己變成candidate,發起選舉。這樣,如果是這個單個節點的網路有問題,它不會貿然自增Term,因此當它重新加入叢集時。也不會對現任leader地位有任何衝擊。保證了系統更穩定的方式執行。

 

5、如何保證已經commit的資料不會被改寫?

etcd叢集的leader會一直向follower同步自己的日誌,如果follower發現自己的日誌和leader不一致,會刪除它本地的不一致的日誌,保證和leader同步。

leader在執行過程中,會檢查同步日誌的回覆訊息,如果發現一條日誌已經被超過半數的節點同步,則把這條日誌記為committed。隨後會進行apply動作,持久化日誌,改變kv儲存。

我們現在設想這麼一個場景:一個叢集執行過程中,leader突然掛了,這時候就有新的follower競選leader。如果新上來的leader日誌是比較老的,那麼在同步日誌的時候,其他節點就會刪除比這個節點新的日誌。要命的是,如果這些新的日誌有的是已經提交了的。那麼就違反了已經提交的日誌不能被修改的原則了。

怎麼避免這種事情發生呢?這就涉及到剛才選舉流程中一個動作,candidate在發起選舉的時候會加上當前自己的最新的日誌index和term。follower收到選舉訊息時,會根據這兩個欄位的資訊,判斷這個競選者的日誌是不是比自己新,如果是,則贊成選舉,否則投反對票。

為什麼這樣可以保證已經commit的日誌不會被改寫呢?因為這個機制可以保證選舉出來的leader本地已經有已經commit的日誌了。

為什麼這樣就能保證新leader本地有已經commit的日誌呢?

因為我們剛才說到,只有超過半數節點同步的日誌,才會被leader commit,而candidate要想獲得半數以上的選票,日誌就一定要比半數以上的節點新。這樣兩個半數以上的群體裡交集中,一定至少存在一個節點。這個節點的日誌肯定被commit了。因此我們只要保證競選者的日誌被大多數節點新,就能保證新的leader不會改寫已經commit的日誌。

簡單來說,這種機制可以保證下圖的b和e肯定選不leader。

 

 

6、頻繁重選舉的問題

如果etcd頻繁出現重新選舉,會導致系統長時間處於不可用狀態,大大降低了系統的可用性。

什麼原因會導致系統重新選舉呢?

1. 網路時延和網路擁塞:從心跳傳送的流程可以看到,心跳訊息和其他訊息一樣都是先放到Ready結構的msgs陣列中。然後逐條傳送出去,對不同的節點,訊息傳送不會阻塞。但是對相同的節點,是一個協程來處理它的msgc通道的。也就是說如果有網路擁塞,是有可能出現其他的訊息擁塞通道,導致心跳訊息不能及時傳送的。即使只有心跳訊息,擁塞引起通道頻寬過小,也會導致這條心跳訊息長時間不能到達對端。也會導致心跳超時。另外網路延時會導致訊息傳送時間過程,也會引起心跳超時。另外,peer之間通訊建鏈的超時時間設定為1s+(選舉超時時間)*1/5 。也就是說如果選舉超時設定為5s,那麼建鏈時間必須小於2s。在網路擁塞的環境下,這也會影響訊息的正常傳送。

2. IO延時:從apply的流程可以看到,傳送msg以後,leader會開始持久化已經commit的日誌或者snapshot。這個過程會阻塞這個協程的呼叫。如果這個過程阻塞時間過長,就會導致後面的msgs堵在那裡不能及時傳送。根據官網的解釋,etcd是故意這麼做的,這樣可以讓那些io有問題的leader自動失去leader地位。讓io正常的節點選上leader。但是如果整個叢集的節點io都有問題,就會導致整個叢集不穩定。

 

相關文章