作者:寶爺 校對: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都有問題,就會導致整個集群不穩定。