TiKV 原始碼解析系列 - Raft 的優化

PingCAP發表於2017-03-13

摘要: 本系列文章主要面向 TiKV 社群開發者,重點介紹 TiKV 的系統架構,原始碼結構,流程解析。目的是使得開發者閱讀之後,能對 TiKV 專案有一個初步瞭解,更好的參與進入 TiKV 的開發中。本文是本系列文章的第六章節。重點介紹 TiKV 中 Raft 的優化。(作者:唐劉)

在分散式領域,為了保證資料的一致性,通常都會使用 Paxos 或者 Raft 來實現。但 Paxos 以其複雜難懂著稱,相反 Raft 則是非常簡單易懂,所以現在很多新興的資料庫都採用 Raft 作為其底層一致性演算法,包括我們的 TiKV。

當然,Raft 雖然簡單,但如果單純的按照 Paper 的方式去實現,效能是不夠的。所以還需要做很多的優化措施。本文假定使用者已經熟悉並瞭解過 Raft 演算法,所以對 Raft 不會做過多說明。

Simple Request Flow

這裡首先介紹一下一次簡單的 Raft 流程:

  1. Leader 收到 client 傳送的 request。
  2. Leader 將 request append 到自己的 log。
  3. Leader 將對應的 log entry 傳送給其他的 follower。
  4. Leader 等待 follower 的結果,如果大多數節點提交了這個 log,則 apply。
  5. Leader 將結果返回給 client。
  6. Leader 繼續處理下一次 request。

可以看到,上面的流程是一個典型的順序操作,如果真的按照這樣的方式來寫,那效能是完全不行的。

Batch and Pipeline

首先可以做的就是 batch,大家知道,在很多情況下面,使用 batch 能明顯提升效能,譬如對於 RocksDB 的寫入來說,我們通常不會每次寫入一個值,而是會用一個 WriteBatch 快取一批修改,然後在整個寫入。 對於 Raft 來說,Leader 可以一次收集多個 requests,然後一批傳送給 Follower。當然,我們也需要有一個最大傳送 size 來限制每次最多可以傳送多少資料。

如果只是用 batch,Leader 還是需要等待 Follower 返回才能繼續後面的流程,我們這裡還可以使用 Pipeline 來進行加速。大家知道,Leader 會維護一個 NextIndex 的變數來表示下一個給 Follower 傳送的 log 位置,通常情況下面,只要 Leader 跟 Follower 建立起了連線,我們都會認為網路是穩定互通的。所以當 Leader 給 Follower 傳送了一批 log 之後,它可以直接更新 NextIndex,並且立刻傳送後面的 log,不需要等待 Follower 的返回。如果網路出現了錯誤,或者 Follower 返回一些錯誤,Leader 就需要重新調整 NextIndex,然後重新傳送 log 了。

Append Log Parallelly

對於上面提到的一次 request 簡易 Raft 流程來說,我們可以將 2 和 3 並行處理,也就是 Leader 可以先並行的將 log 傳送給 Followers,然後再將 log append。為什麼可以這麼做,主要是因為在 Raft 裡面,如果一個 log 被大多數的節點append,我們就可以認為這個 log 是被 committed 了,所以即使 Leader 再給 Follower 傳送 log 之後,自己 append log 失敗 panic 了,只要 N / 2 + 1 個 Follower 能接收到這個 log 併成功 append,我們仍然可以認為這個 log 是被 committed 了,被 committed 的 log 後續就一定能被成功 apply。

那為什麼我們要這麼做呢?主要是因為 append log 會涉及到落盤,有開銷,所以我們完全可以在 Leader 落盤的同時讓 Follower 也儘快的收到 log 並 append。

這裡我們還需要注意,雖然 Leader 能在 append log 之前給 Follower 發 log,但是 Follower 卻不能在 append log 之前告訴 Leader 已經成功 append 這個 log。如果 Follower 提前告訴 Leader 說已經成功 append,但實際後面 append log 的時候失敗了,Leader 仍然會認為這個 log 是被 committed 了,這樣系統就有丟失資料的風險了。

Asynchronous Apply

上面提到,當一個 log 被大部分節點 append 之後,我們就可以認為這個 log 被 committed 了,被 committed 的 log 在什麼時候被 apply 都不會再影響資料的一致性。所以當一個 log 被 committed 之後,我們可以用另一個執行緒去非同步的 apply 這個 log。

所以整個 Raft 流程就可以變成:

  1. Leader 接受一個 client 傳送的 request。
  2. Leader 將對應的 log 傳送給其他 follower 並本地 append。
  3. Leader 繼續接受其他 client 的 requests,持續進行步驟 2。
  4. Leader 發現 log 已經被 committed,在另一個執行緒 apply。
  5. Leader 非同步 apply log 之後,返回結果給對應的 client。

使用 asychronous apply 的好處在於我們現在可以完全的並行處理 append log 和 apply log,雖然對於一個 client 來說,它的一次 request 仍然要走完完整的 Raft 流程,但對於多個 clients 來說,整體的併發和吞吐量是上去了。

Now Doing…

SST Snapshot

在 Raft 裡面,如果 Follower 落後 Leader 太多,Leader 就可能會給 Follower 直接傳送 snapshot。在 TiKV,PD 也有時候會直接將一個 Raft Group 裡面的一些副本排程到其他機器上面。上面這些都會涉及到 Snapshot 的處理。

在現在的實現中,一個 Snapshot 流程是這樣的:

  1. Leader scan 一個 region 的所有資料,生成一個 snapshot file
  2. Leader 傳送 snapshot file 給 Follower
  3. Follower 接受到 snapshot file,讀取,並且分批次的寫入到 RocksDB

如果一個節點上面同時有多個 Raft Group 的 Follower 在處理 snapshot file,RocksDB 的寫入壓力會非常的大,然後極易引起 RocksDB 因為 compaction 處理不過來導致的整體寫入 slow 或者 stall。

幸運的是,RocksDB 提供了 SST 機制,我們可以直接生成一個 SST 的 snapshot file,然後 Follower 通過 injest 介面直接將 SST file load 進入 RocksDB。

Asynchronous Lease Read

在之前的 Lease Read 文章中,我提到過 TiKV 使用 ReadIndex 和 Lease Read 優化了 Raft Read 操作,但這兩個操作現在仍然是在 Raft 自己執行緒裡面處理的,也就是跟 Raft 的 append log 流程在一個執行緒。無論 append log 寫入 RocksDB 有多麼的快,這個流程仍然會 delay Lease Read 操作。

所以現階段我們正在做的一個比較大的優化就是在另一個執行緒非同步實現 Lease Read。也就是我們會將 Leader Lease 的判斷移到另一個執行緒非同步進行,Raft 這邊的執行緒會定期的通過訊息去更新 Lease,這樣我們就能保證 Raft 的 write 流程不會影響到 read。

總結

雖然外面有聲音說 Raft 效能不好,但既然我們選擇了 Raft,所以就需要對它持續的進行優化。而且現階段看起來,成果還是很不錯的。相比於 RC1,最近釋出的 RC2 無論在讀寫效能上面,效能都有了極大的提升。但我們知道,後面還有很多困難和挑戰在等著我們,同時我們也急需在效能優化上面有經驗的大牛過來幫助
我們一起改進。
如果你對我們做的東西感興趣,想讓 Raft 快的飛起,歡迎聯絡我們,郵箱:info@pingcap.com,作者個人微信:siddontang

相關文章