TiKV 源碼解析系列 - Raft 的優化

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

在分佈式領域,爲了保證數據的一致性,一般都會使用 Paxos 或者 Raft 來實現。但 Paxos 以其複雜難懂著稱,相反 Raft 則是很是簡單易懂,因此如今不少新興的數據庫都採用 Raft 做爲其底層一致性算法,包括咱們的 TiKV。github

固然,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

相關文章
相關標籤/搜索