1. 程式人生 > >CONSENSUS:BRIDGING THEORY AND PRACTICE(第6章)

CONSENSUS:BRIDGING THEORY AND PRACTICE(第6章)

客戶端互動

本章介紹了幾個客戶端和Raft-based的複製狀態機互動的問題:

  • 6.1節講述了客戶端如何發現叢集,即使叢集的成員會隨著時間變化;
  • 6.2節講述了客戶端的請求是怎麼路由到leader處理的;
  • 6.3節介紹了Raft如何提供線性一致性的;
  • 6.4節介紹了Raft如何更有效地處理只讀請求的。

圖6.1客戶端與複製狀態機互動使用的RPCs,這些會在通篇裡討論。所有一致性基礎的系統都涉及這些問題,Raft的解決方案和其他系統類似。

本章假定Raft-based的複製狀態機直接以網路服務的形式暴露給客戶端。Raft也可以直接整合到客戶端應用程式中。這種情況下,客戶端互動的一些問題可能被推高一個層級到嵌入在應用程式裡的網路客戶端。例如,嵌入到應用程式的網路客戶端會有和Raft作為網路服務時其客戶端發現叢集類似的問題。


這裡寫圖片描述

6.1 叢集發現

當Raft作為一個網路服務暴露的時候,客戶端為了和複製狀態機互動必須能定位Raft叢集。對於固定成員的叢集,這是直截了當的;比如可以把網路地址靜態地儲存在客戶端配置檔案裡。然而,如何發現一個隨著時間動態改變成員的叢集是一個大的挑戰。有兩個一般的方法:

  1. 客戶端使用廣播或者多播去發現所有的叢集成員。然而,這樣就依賴了特定的網路環境。
  2. 客戶端可以通過外部的目錄服務發現叢集servers,比如有著眾所周知地址的DNS。servers沒有必要和外部系統保持一致,但是必須包含:客戶端一直能夠發現所有叢集servers,但是其實包含一些額外的不屬於叢集的servers是無害的。因此,在成員變更期間,外部的目錄服務需要在成員變更前更新,等成員變更完成之後再移除不屬於cluster的servers。
  3. 譯者注:諸如具有服務發現能力的etcd這樣的服務。

LogCabin客戶端當前使用DNS發現叢集。LogCabin目前不會在成員變更前後自動地更新DNS記錄(通過管理員指令碼)。

6.2 請求的路由

客戶端請求是通過Raft leader處理的,因此客戶端需要一個發現leader的方法。當一個客戶端啟動的時候,隨機選擇一個server連線。如果客戶端第一次選擇的不是leader,這個server就會拒絕服務。這種情況下,一個非常簡單的方法是客戶端隨機地重試下一個直到發現leader。如果客戶端純隨機地選擇servers,這種清純的方法平均情況下會在經過(n + 1) / 2次嘗試下發現leader,這對於小的叢集已經足夠快了。

通過簡單的優化也可以把路由到leader的請求優化的更快。Servers通常知道當前leader的地址,由於AppendEntries請求包含leader的身份資訊。當一個不是leader的server收到了客戶端的請求時,它可以做下面兩件事中的一個:

  1. 第一個選項,是我們建議的而且也是LogCabin中實現的,即非leader的server拒絕請求並返回leader的地址給客戶端,如果它知道。這允許客戶端直接重連到leader,因此以後的請求都可以直接定向處理了。這也需要很少量的額外程式碼去實現,因為客戶端已經需要有當leader故障時重連到一個不同server的能力了。
  2. 可供選擇地,server可以代理客戶端的請求到leader。

Raft還必須防止過期的領導資訊不定期地延遲客戶端請求。領導資訊可能在leaders、followers和clients中都會過期:

  • Leaders:一個server可能處於leader狀態,但是它並不是現在的leader,這就會不必要地延遲客戶端請求。例如,假如一個leader和叢集網路隔離了,但是它仍然可以和某個客戶端通訊。沒有額外的機制它就會一直延遲那個客戶端的請求,它沒法把log複製給其他servers。期間可能有另一個新的term的leader能和叢集中的大多數通訊並且能提交客戶端的請求。因此,如果一個選舉超時週期內還沒能和叢集中的大多數做一輪成功的心跳,那麼Raft leader會下臺;這允許客戶端把請求重試到其他server上。
  • Followers:Followers保持跟蹤leader的身份資訊以便它們能重定向或者代理客戶端的請求。它們必須在開始新的選舉或者term變更時丟棄這個資訊。否則它們可能帶給客戶端不必要的延遲(例如,兩個服務互相重定向,是客戶端陷入無線迴圈)。
  • Clients:如果一個客戶端丟失了與leader的連線(或者和某個server的),它將簡單地隨機重試一個server。堅持和最後瞭解到的leader聯絡的話一旦那個server故障了就會導致客戶端不必要的延遲。

6.3 實現線性化語義

按照目前的描述,Raft對客戶端提供了at-least-once語義;複製狀態機是可能應用一個命令多次的。例如,假設客戶端提交了一個命令到leader並且leader把命令新增到了日誌並且提交了日誌entry,但是在響應客戶端之前就崩潰了。由於客戶端沒有收到確認,就會重新提交這個命令到新的leader上,就會導致把這個命令作為一個新的entry寫入日誌中並且也會提交這個entry。儘管客戶端期望這個命令被執行一次,但是實際上執行了兩次。

如果網路導致客戶端請求重複即使在沒有客戶端牽涉的情況下也會導致請求被應用多次。

這個問題不是Raft獨有的;絕大部分有狀態的分散式系統都會發生。然而,作為一個一致性為基礎的系統僅提供at-least-once的語義是特別不合適的,客戶端典型的需要的是更強的保證。重複命令的問題可能一微妙的方式出現,客戶端很難從這種情況下回復過來。這些問題會引起或者不正確的結果,或者不正確的狀態,或者都有。圖6.2顯示了一個不正確結果的例子:一個狀態機正在提供一個鎖,並且一個客戶端發現它不能獲取到這個鎖因為它原始的請求,因為它沒有收到上鎖的確認。一個狀態不正確的例子,比如一個增量操作,客戶端打算把一個值增加1但反而增加了2或者更多。網路層級的亂序和客戶端的併發能導致更加驚奇的結果。


這裡寫圖片描述

我們在Raft中的目標是實現線性化語義,可以避免這類問題。線上性化語義中,每一個操作都會在呼叫和響應間的某個點立即並且準確地一次地執行。這是一個很強的一致性,客戶端對行為的推理會變得簡單,並且不允許命令被執行多次。

Raft為了實現線性化語義,servers必須過濾掉重複的請求。最基本的想法就是servers儲存客戶端的操作並且使用他們跳過重複的相同請求。為了實現這個,每一個客戶端自己需要有一個獨一無二的標識,並且客戶端對每一個命令都賦予一個獨一無二的序號。每一個server的狀態機對客戶端維護一個session。session最近客戶端被處理的序號,同時也關聯著響應。如果一個server收到一個序號已經被執行過了的命令,它直接作出響應而不是重執行這個請求。

給定了過濾重複請求的能力,Raft提供了線性一致性。Raft日誌提供每個server應用命令的序列化機制。命令根據在Raft日誌中第一次出現而立即、精確一次地生效,由於任何後來出現的都會被狀態機過濾掉。

這個方法還推廣到允許來自單個客戶端的併發請求。客戶端的session不只跟蹤客戶端最近的序號和響應,而是包含了一組序列號和響應對。對於每個請求,客戶端包含了它尚未收到的相應的最低序號,狀態機就會丟棄所有較低序號的響應。

不幸的是,sessions沒辦法永遠被保持,由於空間是有限的。servers必須最終決定過期一個客戶端的session,但是這引來了兩個問題:servers怎麼一致同意什麼時候過期一個客戶端的session,他們如何處理一個不幸地被過早過期的活動的客戶端?

Servers必須在什麼時候決定客戶端session過期意見一致;否則servers的狀態機會互相偏離。例如,假如一個server過期了某個客戶端的session,然後再應用了這個客戶端的重複命令;與此同時,其他servers保持了這個session並且不會應用重複命令。複製狀態機將會不一致。為了避免這個問題,session過期必須是確定的,就像正常的狀態機操作那樣。一個想法是設定一個sessions數量的上限,並且依照LRU協議移除session條目。另一個想法是基於一個意見一致的時間源。在LogCabin中,leader對沒一個新增到Raft日誌的命令增加一個當前時間的屬性。作為提交日誌的一部分,servers在這個時間上達成一致;然後狀態機使用這個確定的時間去過期不活躍的sessions。活著的客戶端在不活動期間發起keep-alive請求,同樣會附加leader的時間戳並且提交到Raft日誌中以便維護它們的sessions。
譯者注:這裡的時間是另一套servers間的時間,是一個邏輯時間,時間的更新是通過leader日誌中帶的時間更新的,session的deadline時間也是這個邏輯時間,這樣servers就可以在執行到同一條日誌時作出同樣的session過期行為保保證狀態機狀態一致。

另一個問題是如何處理session過期的客戶端的請求。我們認為這是一個異常的情況;然而總會有一些風險,因為通常沒有辦法知道客戶端何時退出。一個想法是當沒有session記錄時給客戶端分配一個新的session,但是這將導致執行重複命令的風險。為了提供可靠的保證,servers需要辨別一個新的client是不是之前有過session過期。當一個客戶端初次啟動的時候,它可以通過RegisterClient RPC向叢集註冊自己。這會分配一個session並返回給客戶端作為標識,客戶端隨後的所有命令都要帶上這個標識。如果一個狀態機遇到一個沒有記錄的session的命令,它不會處理這個命令而是返回一個錯誤給客戶端。LogCabin目前在這種情況下會讓客戶端掛掉(大部分客戶端可能沒辦法正確優雅地處理session過期這種錯誤,但是系統典型地已經能處理客戶端掛掉)。

譯者注:譯者理解client掛掉不一定就是程序掛掉了,完全可以僅僅是這個client物件本身壞掉了,銷燬並重新建立一個物件就是了。之後假如有必要重新執行可以通過狀態查詢定位之前未完成的命令重新執行。

6.4 更有效地處理只讀請求

只讀的客戶端請求僅僅是查詢,它們不會改變複製狀態機。因此,一個很自然的想法是查詢是否可以繞過Raft日誌,Raft日誌的目的是以同樣的順序複製改變到server的狀態機上。繞過日誌提供了吸引人的效能優勢:只讀查詢在很多應用中都很普遍,並且增加entries到日誌會需要同步寫到disk,會消耗時間。

然而,如果沒有額外的預防措施,繞過日誌可能導致只讀請求得到陳舊的資料。例如,一個leader可能和叢集中剩餘部分網路分割槽了,叢集中剩餘部分選出了一個新的leader並且提交了新的entries到Raft日誌中。如果分割槽的leader沒有和其他server商議就返回了只讀查詢的響應,可能返回舊的結果,這就不是線性化了。線性化語義要求讀的結果反映系統在讀開始後的某個時間的狀態;每個讀必須至少返回最近提交寫的結果(系統允許stale reads僅是提供了序列化語義,這是弱的一致性形式)。Stale reads的問題已經在兩個三方Raft實現中被發現了,所以這個問題應當小心留意。

幸運地是,只讀請求是可能繞過Raft日誌並仍然提供線性化語義的。為了達到這樣的目的,leader需要執行以下步驟:

  1. 如果leader還沒有標記的當前任期內被提交的entry,它需要等待直到已經做到了。領導人完整性保證了leader有著所有已提交的entries,但是在它自己的任期開始的時候,他可能不知道哪些是已提交的條目。為了發現已提交的條目,需要在自己的任期提交一個條目。Raft通過leader在自己的任期開始的時候提交一個空的no-op條目到日誌中。一旦這個no-op天木被提交了,leader的commit index在這個任期內將至少和其他server的一樣大。
  2. Leader把自己當前的commit index儲存到一個本地變數readIndex中。這將用作查詢操作所針對的狀態版本的下界。
  3. Leader需要確信它沒有在不知道的情況下被其他新的leader取代。它發起一輪新的心跳並且等待majority的確認。一旦收到了這些確認,leader就知道在它傳送心跳的時刻不存在leader比它的term更大。因此,readIndex在當時是叢集中任何伺服器所見過的最大的提交索引。
  4. Leader等待它的狀態機至少前進到readIndex;這足夠能滿足線性化語義。
  5. 最後,leader對狀態機發起查詢並且回覆客戶端結果。

這個方法比提交一個制度請求的條目到日誌中更有效,由於避免了disk寫同步。為了進一步地提高只讀查詢的效能,leader可以分攤需要確認leadership的開銷:可以通過一輪心跳確認累積的只讀查詢。

Follower也可以幫助分擔只讀查詢的處理。這將提升系統的讀吞吐,並且也將轉移leader的負載,允許leader能夠處理更多的請求。然而,如果沒有額外的預防機制這些讀將導致返回舊資料的風險。例如,follower網路分割槽或者即便能收到leader的心跳而leader本身也網路分割槽了自己還不知道。為了提供安全地讀,follower可以發起一個請求到leader問一下當前的readIndex(leader將執行上面的1-3步);follower之後可以在自己的狀態機執行4和5步。

LogCabin在leader上實現了上述演算法,並且在高負載的情況下通過積攢多個只讀查詢來分攤開銷。Follower目前在LogCabin中不提供只讀請求。

6.4.1 使用時鐘減少只讀請求的訊息

截止到目前,只讀查詢的提供線性化語義是通過同步模型的方法實現的(clocks、processors和messages能在任意的速度運作)。這個安全等級要求通訊去實現:每一批只讀的查詢都需要一輪和叢集的一半的心跳,增加了查詢的延時。剩下的章節探索一個可選擇的通過依賴時鐘進而避免和大家傳送訊息的機制。LogCabin當前沒有實現這個選擇,並且我們不建議使用,除非有效能需求。

為了對只讀查詢使用時鐘代替資訊互動,正常的心跳機制需要提供一個租約的形式。一旦leader的心跳收到了majority的確認,leader將假定在選舉超時週期內沒有其他server將成為leader,並且它能相應地擴大它的租約(見圖6.3)。Leader將不用任何額外機制,在租約有效期間直接回復只讀查詢(領導人轉換機制允許leader被更早地替換;leader需要能在轉換領導人時過期自己的租約)。

租約的方法假定了server間的時鐘漂移是有界的(在給定的時間段內,沒有任何server的時鐘增長超過這個限制的時間)。發現和維護這個界限可能是非常大的操作挑戰(比如,由於排程和垃圾回收中斷,虛擬機器整合,或者為了時間同步的時鐘速率調整)。如果假設被違反了,系統可返回任意的舊資訊。

幸運地是,一個簡單的擴充套件可以提高對於客戶端的保證,因此即使在非同步假設下(即使時鐘會出錯),每個客戶端都將看到複製狀態機的單調進度(順序一致性)。例如,一個客戶端將不會看到了log n的狀態,然後換到別的server並且只看到了log n - 1的狀態。為了實現這個保證,servers將把狀態機狀態相應的log index包含到回覆客戶端的響應中。客戶端將會跟蹤最它們看到的結果的相應的最近的index,並且它們將在每個請求中提供這個資訊。如果一個server收到了客戶端的請求的index大於server最後應用的log index,它將不會對這個請求提供服務。


這裡寫圖片描述

6.5 討論

這章討論了幾個客戶端和Raft互動的問題。提供線性化語義和優化只讀查詢的問題在正確性方面尤為微妙。不幸地是,當前的一致性文獻都是講叢集servers間的溝通,沒有涉及這些重要的問題。我們認為這是一個錯誤。一個完整的系統必須正確地和客戶端互動,否則由核心共識演算法提供能一致性能力將被浪費。正如我們已經在真正的基於Raft的系統中看到的那樣,客戶端互動可能是bug的主要來源,但是我們希望更好地理解這些問題以幫助防止將來的問題。