Hermes: a Fast, Fault-Tolerant and Linearizable Replication Protocol

提供高性能的讀寫服務是分佈式研發工程師一直追求的目標,譬如在 TiDB 中,我們就基於原生的一致性算法 Raft 做了非常多的改進和性能優化。當然,在分佈式領域,複製協議不光只有 Raft 這麼一種,譬如這段時間,我就看到了另一個不錯的實現,叫做 Hermes,來自於 Paper Hermes: a Fast, Fault-Tolerant and Linearizable Replication Protocol

通常我們說分佈式系統的高性能,無非就是關注兩點:低延遲和高吞吐,對於讀寫請求來說,無非就是:

    • 能從所有副本讀取數據
    • 更少的網絡交互
    • 去中心化,不需要有單獨的地方進行 serialization 處理
    • 完全併發,任何 replica 都能進行寫入

對於 TiDB 來說,雖然我們在 Raft 這邊支持了 Follower Read,也就是能從任意副本讀取數據,但在寫入上面,還是隻有 Leader 能提供寫服務。但是 Hermes 卻能做到任何副本寫入,所以讓我對這篇 Paper 非常有興趣,讀下來之後,其實會發現,原來 Hermes 的原理非常的簡單。

在介紹原理之前,先階段的說下,Hermes 使用的是非常通用的 logical timestamp 機制,後面用 LT 來表示,一個 LT 是一個 [V, Cid] 的 tuple,V 就是通常的 Lamport clock,而 Cid 則是 replica 的 Node ID。如果對於一個 Key 的併發寫入 V 是相同的,則按照 Cid 進行排序。

Hermes 的 replica 有 Coordinator 和 Follower 兩種,Coordinator 接受 client 的寫入請求,參考論文的圖,一次寫入如下:

  • Coordinator 接受 client 的 write 請求,將 Key 分配一個新的 LT,給其他 followers 也廣播一個 Invalidation(INV) 消息。並且將 Key 變成 Write 狀態。
  • 其他 followers 收到 INV 之後,會比較 LT,如果收到消息的 LT 比本地的大,就會將 Key 變成 Invalid 狀態(如果這個 Key 之前是 Write 或者 Replay,則會變成 Trans 狀態),並且回覆 ACK 消息。無論之前比較的結果怎樣,ACK 裏面的 LT 都是收到的 INV 的 LT。
  • 如果所有的 ACK 都收到了,write 就認爲完成,這個 Key 就變成 Valid 狀態(如果之前處於 Trans 狀態,則會變成 Invalid 狀態)
  • Coordinator 再次廣播 Validation(VAL) 消息
  • 當 Follower 收到 VAL 消息,只有 VAL 的 LT 等於之前本地的 local timestamp,纔會將 Key 轉成 Valid 狀態,否則一律丟棄這個 VAL 消息

可以看到,流程非常的簡單,那麼 Hermes 是如何支持 fault tolerance, Linearizability 這些特性的呢?

Hermes 保證,如果讀取的 Key 處於 Invalid 狀態,那麼 read 則會一直等待,直到 Key 處於 Valid 狀態。所以這裏可以看到,Hermes 採用 read wait 的方式滿足了 Linearizability。

而對於 fault tolerance,則是使用的 replayable write 的方式,如果 Coordinator 在發送 VAL 消息之前跪了,那麼就可能導致一個 key 處於 invalid 狀態。這裏可以直接 replay write,因爲 Hermes 會做如下事情:

  • INV 消息裏面會帶上寫入的新值,這樣收到 INV 的 replica 都能知道這個值,並且將其設置爲 Invalid 狀態
  • Logical timestamp 能保證在每個 replica 上面的 write 順序
  • 基於上述兩點,任何一個節點如果發現一個 key 處於 Invalid 狀態超過一段時間,就可以認爲自己是 coordinator,使用之前的 LT,重新傳遞之前的 INV 消息,安全的 replay 這個 write。

Membership 變更

再來說說另一個需要關注的 membership 變更,Hermes 採用的是比較常用的做法,叫做 m-update。一個 m-update 包括:

  • 一個新的 lease
  • 一批新的 live nodes
  • 一個遞增的 epoch ID

一個接受了 m-update 的 replica 就可以當成 coordinator 了,這裏需要注意:

  • 如果 read 處於 Valid 的 key,仍然是按照之前的方式處理
  • 如果 write 或者 read 處於 Invalid 的 key,則需要等 m-update 裏面的 live nodes 接受到 m-update
  • 如果一個 follower 還沒接受到 m-update,則會丟棄後續的消息,因爲這些消息的 epoch ID 會比 follower 當前的 epoch ID 要大

Example

下面是 Paper 實際列舉的一個例子:

  1. Node 1 開始 write A = 1,同樣,node 3 開始 write A = 3。
  2. Node 2 收到 Node 1 的 INV 消息,回覆了 ACK,並且將 Key A 變成了 Invalid 狀態
  3. Node 3 給 Node 1 也回覆了 ACK,但 Node 3 並沒有改變 A 或者它的狀態,因爲 A 的 LT 是比 Node 1 發過來的 INV 要大的。
  4. Node 2 收到了 Node 3 的 INV,因爲這個 INV 的 LT 比之前的要大,所以 Node 2 更新了本地 A 的 LT 和 value
  5. Node 1 收到了 Node 3 的 INV,同理也更新了本地 A 的狀態
  6. Node 2 開始一次 read,因爲 A 的狀態是 invalid,所以 read 會 stalled,然後收到 VAL 的消息,就可以讀取了,這時候讀到的值是 3
  7. Node 1 收到了所有的 ACKs,但仍然將 A 變成了 Invalid 狀態,因爲之前收到從 Node 3 發來的帶有更大 LT 的 INV 消息,但還沒收到 Node 3 發來的 VAL 消息
  8. 如果 Node 3 跪掉了,Node 1 還沒收到 VAL 消息
    1. Lease 過期,Node 3 仍然是 failed,membership 更新
    2. 從 Node 1 讀取 A,會發現處於 Invalid 狀態,Node 1 對 A 進行 replay 操作
    3. Node 2 收到消息不用處理,因爲有同樣的 LT
    4. Node 1 收到 Node 2 的 ACK,完成 replay,並且廣播 VAL

寫在最後

上面只是簡單的介紹了 Hermes 常用的讀寫流程以及成員變更方式,其實 Hermes 還提供了 CAS 支持,不過我也懶得研究了,總得來說,Hermes 還是非常簡單的,而且非常容易實現,更難得可貴的是,Hermes 提供了 TLA+ 的證明,我現在也佩服我自己,竟然還能看得懂,之前的辛苦學習沒白費。。。

關於更多的信息,大家可以直接用官網 https://hermes-protocol.com/ 看看。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章