分布式一致性算法概要
隨著各種高并發訪問、海量數據處理等應用場景越來越多,為了應對這些使用場景,分布式系統應運而生。分布式系統得以發展,得益于諸多優點,比如:可以避免 單點故障 ,容易 橫向擴展 等。所謂單點故障指的是:單個組件發生故障會導致整個系統的癱瘓,而容易橫向擴展的意思是我們可以通過增加機器來提高整個系統的性能。分布式系統在帶來諸多優點的同時,也帶來了一些挑戰,我們下面來重點描述清楚其中的一個核心挑戰: 在分布式系統中如何保證數據的一致性 。關于分布式系統的基本概念,可以參考相關的理論書籍。
在眾多分布式一致性協議中, paxos 算法經過了嚴格的數學證明。然而由于該算法非常難以理解,尤其是在各種系統的實現時,一般采用 paxos 的簡化版本或者 paxos 的一些變種協議,比如 Raft 和 Zab 等。為此,本文選取 Raft 算法來進行介紹。我們可以通過構造一個如下的應用場景來幫助我們理解一個算法。
以一個數據存儲系統為例,如果 client 想要在 server 上設置一個值,假如一開始有一個 server 和 一個 client,此時只要一個server 完成值的設置即可。可是如果有多個 server 存在的話,要如何保證所有的 server 節點上存儲的值是一致的呢?如果各節點的初始狀態一致,每個節點執行相同的操作序列,那么他們最后能得到一個一致的狀態。為保證每個節點執行相同的命令序列,需要在每一條指令上執行一個“一致性算法”以保證每個節點看到的指令一致。
Raft 算法介紹
在一個由 Raft 協議組織的集群中有三類角色:
- Leader(領袖)
- Follower(群眾)
- Candidate(候選人)
每個節點都會處于以上三種角色中的一種。
算法的主體大致可分為2個過程:
- 選舉(Leader Election)
- 日志同步(Log Replication)
下面我們分別介紹 Raft 算法中的選舉流程和日志同步流程,隨后會介紹 Raft 算法對一些異常情況的處理。
選舉
選舉和現實社會中的民主選舉制度很像,剛開始沒有 Leader,所有集群中的參與者都是 Follower, 當 Follower 超過選舉超時時間 ( election timeout ) 沒收到來自 Leader 的心跳報文,則成為 Candidate,增加任期 ( Term ) 并向其他節點發起新的選舉請求。接收到請求的節點如果還沒投票,則投票給該節點,并重置自己的超時時間。如果某個 Candidate 節點接收到的選票的個數占大多數(超過 1/2), 它就會成為 Leader 節點,這就是所謂的 Leader election 的過程。每隔一定時間向 Follower 發送心跳報文來維持自己的 『統治』地位。
集群中的每個節點都處于以上三種角色中的一種,其狀態轉換圖可以總結如下:
下面通過幾個例子來幫助我們理解這個選舉過程:
1. 可以正常選舉出 Leader 的例子 :
在此例子中,一共有三個節點,復現流程如下:
- 節點 B 首先出現心跳超時,然后變成 Candidate 角色。
- 節點 B 首先把自己寶貴的一票投給自己,然后請求其他節點也將贊同票投給自己。
- 節點 A 和 C 此時還未出現超時,仍然處于 Follower 角色,接收到請求后,投票給了 節點 B。
- 節點 B 接收到的贊同票超過半數,成為 Leader 節點。
2. 平分選票的情況
由于 Raft 協議并不要求集群中的節點個數為奇數,于是在四個節點的集群中,可能出現如下的情況:
復現流程如下:
- 節點 B 和 C 幾乎同時心跳超時,成為 Candidate 角色。
- 節點 B 和 C 均將自己的贊同票投給自己。
- 節點 A 投給了節點 B,而 節點 D 則投給了節點 C。
- 出現兩個 Candidate 的選票相同的情況。
當出現這種情況的時候,沒有任何一個節點接收到的贊同票超過半數,于是此選舉過程不會有 Leader 角色出現。大家各自等待超時,然后開啟下一輪的選舉流程。
疑問:那有沒有可能在下一輪的選舉過程依然出現平分選票的情況?答案是有的,可是 Raft 有一個機制可以避免此種情況持續發生: 每個節點的超時時間都是隨機的 ,這樣就可以盡量避免多次出現以上的情況。
3. 選舉流程要點總結
「1」Leader 角色通過發送心跳報文來維護自己的統治地位。「2」Follower 角色接收不到心跳報文,則超時開始下一輪的選舉。「3」每個節點的超時時間都是隨機的?!?」成為 Leader 節點需要贊同票超過半數。「5」選舉結束,所有除 Leader 的候選人又變回 Follower 服從 Leader。
日志同步
當選舉完成后,客戶端進行的操作都要通過 Leader 來進行。Leader 接受到客戶端發來的操作請求后,首先記錄到日志中,此時為 uncommitted 狀態。然后在下一個心跳報文中通知所有 Follower,當大多數 Follower 響應 ACK 后,Leader 響應客戶端,進入 committed 階段,并向 Follower 發送 commit 通知應用( Apply )操作。
日志同步流程如下:
上圖中的序號對應一個存儲的步驟, 日志同步 流程總結如下:「1」client 給 Leader 發送一個操作請求,假設為 SET X=8?!?」Leader 會把 SET X=8 這個操作記錄到本地 log,此時為 uncommited 狀態,同時將這個操作發送給所有的 Follower。「3」Follower 接收到操作請求后,也將 SET X=8 這個操作記錄到本地 log(uncommited 狀態),然后給 Leader 回復 ACK 信息?!?」Leader 節點當接收到的 ACK 超過半數后,給 client 回復 ACK 信息,進入 commited 階段。「5」Leader 節點首先應用自己的 log,然后將 commit 消息發送給 Follower, 讓 Follower 也 應用自己存儲的日志信息。
Raft 中的異常處理
我們已經介紹了 Raft 算法的正常處理流程,然而現實總是很骨感,會出現各種異常的情況。client 和 Leader 之間的通信異常,不會影響到集群內部狀態的一致性,不再贅述;如果在一個任期內,Follower 角色宕機,處理流程比較簡單。其呈現出來的現象就是 步驟「3」異常,然而只要 Leader 能夠接收到超過半數的 ACK,就不會影響到正常的存儲流程。但是 Leader 角色會針對該 Follower 節點,持續進行步驟 「2」 的動作,直到接收到步驟 「3」的回應信息或者我們把該 Follower 節點從集群中拿掉;Raft 協議強依賴 Leader 節點的可用性來確保集群數據的一致性。下面重點介紹在各種情況下 Leader 節點出現故障時,Raft 協議是如何保障數據一致性的。
1. 寫操作 到達 Leader 節點,但尚未同步到 Follower 節點
此刻集群的狀態如下,用方框中的灰色表示當前處于 uncommited 狀態。
如果集群在此狀態,Leader 節點宕機,則 Raft 的處理流程如下:「1」Follower 節點超時,然后選舉出新一任期的 Leader,然后它并沒有處于 uncommited 狀態的日志?!?」Client 節點由于 Ack 超時,可安全發起重試?!?」原來的 Leader 節點恢復后以 Follower 角色重新加入集群。并從當前任期的新 Leader 處同步數據,可能會覆蓋原來處于 uncommited 狀態的日志。
2. 寫操作 到達 Leader 節點,且將寫操作同步到 Follower 節點,但還未向 Leader 回復 ACK 信息。
此刻集群的狀態如下,用方框中的灰色表示當前處于 uncommited 狀態。
如果集群在此狀態,Leader 節點宕機,則 Raft 的處理流程如下:「1」Follower 節點超時,然后選舉出新一任期的 Leader,并且它存在處于 uncommited 狀態的日志。「2」新的 Leader 節點假裝沒有剛才的選舉過程,繼續從步驟(2)開始執行,以完成數據的提交?!?」Client 節點由于 ACK 超時,可安全發起重試。「4」原來的 Leader 節點恢復后以 Follower 角色重新加入集群。并從當前任期的新 Leader 處同步數據。
此時,在上面步驟「1」中選舉新的 Leader 節點時,需要滿足一個限制條件,即新的 Leader 節點需要具有最新的日志信息,所謂的最新是通過任期和日志的偏移量兩個參數來判定的,這個限制是為了解決只有部分 Follower 節點接收到寫操作請求的情況。
3. Leader 節點接收到寫操作的 ACK 信息,處于 commited 狀態,然而 Follower 處于 uncommited 狀態。
此種情況是由于,在步驟(5)執行過程中,Leader 宕機導致的,此刻集群的狀態如下,用方框中的灰色表示當前處于 uncommited 狀態,用方框中的黑色表示當前處于 commited 狀態,同時圓圈中的值表示操作已經生效。
對于此種情況,Raft 協議的處理流程和上面情況2的處理流程是一樣的。
4. 腦裂
由于網絡故障,產生分區將原先的 Leader 節點和 Follower 節點分隔開,Follower 收不到 Leader 的心跳將發起選舉,產生新的 Leader,這時就產生了雙 Leader,這就是所謂的腦裂。這種情況下某些 Leader 由于獲取不到大多數的投票,所以數據永遠不會提交成功。當網絡故障恢復后,舊的 Leader 發現有 Term 更新的 Leader 存在,則自動降級為 Follower 并從最新的 Leader 同步數據達成集群一致。
總結
本文以 Raft 協議為契機來介紹分布式環境中的一致性。首先介紹了 Leader 選舉和日志同步的過程,然后介紹了 Raft 協議是如何處理各種異常情況的。通過 Raft 協議的學習,不僅可以對分布式一致性有一個概括性的了解,同時也會有助于對其他一致性協議(比如 paxox)的學習。在此基礎上,可以嘗試閱讀一些 Raft 開源的實現, 以加深進一步的理解。