分散式共識協議Paxos本質是一次寫入暫存器? - maheshba

banq發表於2021-11-17

在系統中,我們透過抽象來處理複雜性。對於任何系統,都存在三個關鍵問題:
  • 它實現了什麼抽象?
  • 這種抽象的設計空間是什麼?
  • 為什麼這個抽象有用?

在這篇文章中,我們將回答 Paxos 的前兩個問題。
本文件並不打算取代 Paxos 論文。
 

Paxos 實現了一次寫入暫存器:
Paxos 是一個實現共識的協議。這意味著它實現了一個稱為一次寫入暫存器(或 WOR)的邏輯物件。WOR 有一個簡單的 API:您可以寫入一次;你可以多次閱讀它。字面上虛擬碼如下:

class WOR{
    public:
        //success means some write succeeded;
        //read after a write to see what was written.
        void write(std::string payload);
        //throw an exception if unwritten
        std::string read();
}

WOR 對於併發訪問是安全的。它是可線性化的,或者可交換地,是強一致的。換句話說你的實現必須表現得好像你有一個很大的鎖(不管它是如何實現的)。
WOR 是一個用於達成共識的 API。共識可以隱藏在許多其他 API 之後,但 WOR 是它的最小 API。
理解共識只是一種物件對系統構建者有一些有用的含義。
  • 只需檢查它們的 API,您就可以立即發現其他系統中是否存在共識。例如,具有簡單 put/get API 的鍵值儲存不一定實現共識(您不能在頂部實現 WOR);而有條件的 put/get API 確實實現了共識(您可以在頂部實現 WOR)。
  • 當構建一個需要共識的真實系統時,你可以依賴一些現有系統作為 WOR 的臨時實現(例如,一些現有的帶有條件 puts 的鍵值儲存);使整個堆疊執行;然後用共識協議乾淨地替換它。
  • 您可以在 WOR 實現中出現故障(接下來出現)時,將與分散式系統中的共識相關的複雜性與系統的所有其他複雜性徹底隔離開來。

 

Paxos使用法定仲裁quorums機制實現了一個一次寫入暫存器以實現容錯:
共識的全部意義在於在出現故障的分散式系統中做有用的事情。通常讓人們失望的是確切的故障模型。
這是 Paxos 使用的模型。機器可以任意重啟;但它們具有持久儲存,並且會恢復。更令人擔憂的是,它們可能會因爆炸而失敗(丟失它們儲存的所有位)。
我們將共識建模為一個邏輯物件:WOR。考慮一個系統,其中客戶端正在訪問 WOR,而 WOR 又儲存在儲存伺服器叢集上。
實現一次寫入暫存器(我們稱之為WOR-server)的一種簡單方法是讓單個伺服器將值儲存在 RAM 中並透過 RPC 公開 WOR API。但這並不持久;如果伺服器重新啟動,該值將丟失。一個稍微好一點的解決方案是將資料儲存在磁碟上,這樣它就不會在重新啟動時丟失。但是如果伺服器爆炸,你就會丟失資料。
因此,如果機器爆炸,WOR-server——單伺服器設計——是不耐用的。為了獲得對爆炸機器的永續性,我們需要跨伺服器複製資料。但是很明顯,如果所有的伺服器都可以爆炸,就沒有辦法獲得永續性。所以我們需要對爆炸伺服器的數量做一個假設。
Paxos 假設只有少數伺服器會爆炸;等價地,為了容忍 F 個爆炸的伺服器,Paxos 需要 2F+1 個伺服器。這直接導致了基於仲裁的協議。為了持久地寫入資料,我們只需要將其儲存在大多數法定人數的伺服器上。我們假設只有少數人會爆炸;所以至少一個未爆炸的伺服器將包含資料。
所以我們到達了WOR-quorum:寫入 WOR 的客戶端可以簡單地將資料寫入法定伺服器(每個伺服器都執行單伺服器WOR-伺服器設計)。從 WOR 讀取需要客戶端轉到法定伺服器。這樣的解決方案是持久的。
在 Paxos 中,伺服器被稱為接受者;寫入值的客戶端稱為提議者。
在只有一個客戶端不會失敗的系統中,我們就完成了。但在實際系統中,我們通常希望多個客戶端同時訪問 WOR;每個客戶端都可以自己重新啟動或爆炸。
這將我們帶到了 Paxos 協議。
 

Paxos 實現了一個一次寫入暫存器,使用法定仲裁quorums機制進行容錯和兩階段鎖定進行併發控制:
在WOR-quorum 中,當兩個以上的客戶端同時寫入一個仲裁時,我們最終會在不同少數接受者上得到不同的值。防止這種情況的一種方法是首先鎖定接受者,然後寫入它們(並解鎖它們)。
鎖定很棘手有兩個原因。首先,您可能會遇到死鎖;以嚴格的順序獲取鎖以防止死鎖會增加延遲。其次,分散式系統中的鎖定有一種新的故障模式:客戶端在獲取鎖定後可能會崩潰。
Paxos 透過一種鎖竊取形式為這兩個問題提供瞭解決方案。鎖有版本號或編號;編號較高的鎖可以覆蓋編號較低的鎖。客戶將選擇一個唯一的鎖號;然後嘗試用它鎖定法定人數。
如果接受者被解鎖,或者用一個較小的數字鎖定,則鎖獲取成功;如果接受者被更高的數字鎖定,則失敗(在這種情況下,鎖定客戶端可以使用更高的鎖定數字重試)。鎖不是建議性的;寫入基於鎖號,如果鎖被盜,則在接受器處將失敗。
現在進入了微妙之處:

  • 完成中的寫入:回想一下,我們假設少數伺服器會爆炸。如果客戶端鎖定了大多數伺服器;無法訪問剩餘的少數;並且發現一個值已經寫在那個多數的單個接受者上,它必須假設這個值也被寫在了不可訪問的少數並被確認回了一些老客戶。因此,唯一的前進道路是新客戶端將該值作為自己的值並將其寫入可訪問的大多數。如果存在多個這樣的值,客戶端必須選擇具有最高關聯鎖號的值。
  • 活鎖Livelock:顯然上面的協議可以活鎖,如果兩個客戶端不斷互相竊取鎖。這個問題在理論上是不可能解決的:FLP 結果(早於 Paxos 協議)表明,對於容錯共識,你不能同時擁有活性和安全性。Paxos 中的 Livelock 是 FLP 結果在行動中的真實示例。
  • 不同的鎖定/寫入仲裁:事實證明,您可以鎖定一些多數仲裁併寫入另一個多數仲裁;兩個階段的法定人數不必相同。(但是對未鎖定接受器的寫入必須被解釋為先鎖定再寫入,否則您會遇到此錯誤)。靈活的 Paxos 進一步指出,如果較低的永續性是可以接受的,則寫入仲裁不一定必須是多數。
  • 預鎖定:客戶端可以在寫入值時預鎖定仲裁以避免往返。為了提供對鎖定的細粒度控制,我們可以顯式地向 WOR 公開一個名為 lock() 的額外 API。如果客戶端與恰好位於同一組接受器上的多個 WOR 互動,我們可以預先鎖定整批 WOR。預鎖定涵蓋了 MultiPaxos 中的關鍵最佳化,我們將在稍後討論。

所以這是最終的 WOR API:

class WOR{
    public:
        //lock a quorum
        int lock();
        //success means some write succeeded;
        //read after a write to see what was written.
        //throws an exception if you lost the lock.
        void write(std::string payload, int lockId);
        //throw an exception if unwritten
        std::string read();
}


  
作者mahesh是 Facebook 的一名軟體工程師;之前是耶魯大學的副教授,以及 VMware Research 和 Microsoft Research Silicon Valley 的研究員。
 

相關文章