TiKV 源码解析系列 - Raft 的优化
這篇文章轉載TiDB大牛?唐劉 的博客:https://mp.weixin.qq.com/s?__biz=MzI3NDIxNTQyOQ==&mid=2247484544&idx=1&sn=7d8e412ecc5aaeb3f9b7cf391bdcf398&chksm=eb1623eadc61aafcefcfbdf36b388a5f96d3009d21641eb6ac67c57317d6c397ddeb58fc7d06&scene=21#wechat_redirect
由于工作原因,最近在學習TiDB中TiKV相關的知識,覺得寫得挺好的,并轉載留存。
在分布式領域,為了保證數據的一致性,通常都會使用 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。
可以看到,上面的流程是一個典型的順序操作,如果真的按照這樣的方式來寫,那性能是完全不行的。
?
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。
?
延展閱讀:
TiKV 源碼解析系列 - Lease Read
TiKV 源碼解析系列 - PD Scheduler
TiKV 源碼解析系列——Placement Driver
TiKV 源碼解析系列——multi-raft 設計與實現
TiKV 源碼解析系列——如何使用 Raft
總結
以上是生活随笔為你收集整理的TiKV 源码解析系列 - Raft 的优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: assets目录与res/raw目录下文
- 下一篇: 分解成质因数