TiKV 源码解析系列
本系列文章主要面向 TiKV 社區開發者,重點介紹 TiKV 的系統架構,源碼結構,流程解析。目的是使得開發者閱讀之后,能對 TiKV 項目有一個初步了解,更好的參與進入 TiKV 的開發中。本文是本系列文章的第六章節。重點介紹 TiKV 中 Raft 的優化。
在分布式領域,為了保證數據的一致性,通常都會使用 Paxos 或者 Raft 來實現。但 Paxos 以其復雜難懂著稱,相反 Raft 則是非常簡單易懂,所以現在很多新興的數據庫都采用 Raft 作為其底層一致性算法,包括我們的 TiKV。
當然,Raft 雖然簡單,但如果單純的按照 Paper 的方式去實現,性能是不夠的。所以還需要做很多的優化措施。本文假定用戶已經熟悉并了解過 Raft 算法,所以對 Raft 不會做過多說明。(還不熟悉Raft,點這里:)TiKV 源碼解析系列——如何使用 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。
可以看到,上面的流程是一個典型的順序操作,如果真的按照這樣的方式來寫,那性能是完全不行的。:P
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…
→ST 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 源碼解析系列 - 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
延展閱讀:
TiKV 源碼解析系列 - Lease Read
TiKV 源碼解析系列 - PD Scheduler
TiKV 源碼解析系列——Placement Driver
TiKV 源碼解析系列——multi-raft 設計與實現
TiKV 源碼解析系列——如何使用 Raft
總結
以上是生活随笔為你收集整理的TiKV 源码解析系列的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 教学思路C#之入门一 认识简单的C#结构
- 下一篇: 归并排序的分析与Java实现