本系列文章主要面向 TiKV 社區開發者,重點介紹 TiKV 的系統架構,源碼結構,流程解析。目的是使得開發者閱讀以後,能對 TiKV 項目有一個初步瞭解,更好的參與進入 TiKV 的開發中。本文是本系列文章的第五章節。做者:唐劉 git
TiKV 是一個要保證線性一致性的分佈式 KV 系統,所謂線性一致性,一個簡單的例子就是在 t1 的時間咱們寫入了一個值,那麼在 t1 以後,咱們的讀必定能讀到這個值,不可能讀到 t1 以前的值。github
由於 Raft 原本就是一個爲了實現分佈式環境下面線性一致性的算法,因此咱們能夠經過 Raft 很是方便的實現線性 read,也就是將任何的讀請求走一次 Raft log,等這個 log 提交以後,在 apply 的時候從狀態機裏面讀取值,咱們就必定可以保證這個讀取到的值是知足線性要求的。算法
固然,你們知道,由於每次 read 都須要走 Raft 流程,因此性能是很是的低效的,因此你們一般都不會使用。安全
咱們知道,在 Raft 裏面,節點有三個狀態,leader,candidate 和 follower,任何 Raft 的寫入操做都必須通過 leader,只有 leader 將對應的 raft log 複製到 majority 的節點上面,咱們纔會認爲這一次寫入是成功的。因此咱們能夠認爲,若是當前 leader 能肯定必定是 leader,那麼咱們就能夠直接在這個 leader 上面讀取數據,由於對於 leader 來講,若是確認一個 log 已經提交到了大多數節點,在 t1 的時候 apply 寫入到狀態機,那麼在 t1 以後後面的 read 就必定能讀取到這個新寫入的數據。服務器
那麼如何確認 leader 在處理此次 read 的時候必定是 leader 呢?在 Raft 論文裏面,提到了兩種方法。網絡
第一種就是 ReadIndex,當 leader 要處理一個讀請求的時候:架構
將當前本身的 commit index 記錄到一個 local 變量 ReadIndex 裏面。app
向其餘節點發起一次 heartbeat,若是大多數節點返回了對應的 heartbeat response,那麼 leader 就可以肯定如今本身仍然是 leader。分佈式
Leader 等待本身的狀態機執行,直到 apply index 超過了 ReadIndex,這樣就可以安全的提供 linearizable read 了。性能
Leader 執行 read 請求,將結果返回給 client。
能夠看到,不一樣於最開始的經過 Raft log 的 read,ReadIndex read 使用了 heartbeat 的方式來讓 leader 確認本身是 leader,省去了 Raft log 那一套流程。雖然仍然會有網絡開銷,但 heartbeat 原本就很小,因此性能仍是很是好的。
但這裏,須要注意,實現 ReadIndex 的時候有一個 corner case,在 etcd 和 TiKV 最初實現的時候,咱們都沒有注意到。也就是 leader 剛經過選舉成爲 leader 的時候,這時候的 commit index 並不可以保證是當前整個系統最新的 commit index,因此 Raft 要求當 leader 選舉成功以後,首先提交一個 no-op 的 entry,保證 leader 的 commit index 成爲最新的。
因此,若是在 no-op 的 entry 還沒提交成功以前,leader 是不可以處理 ReadIndex 的。但以前 etcd 和 TiKV 的實現都沒有注意到這個狀況,也就是有 bug。解決的方法也很簡單,由於 leader 在選舉成功以後,term 必定會增長,在處理 ReadIndex 的時候,若是當前最新的 commit log 的 term 還沒到新的 term,就會一直等待跟新的 term 一致,也就是 no-op entry 提交以後,才能夠對外處理 ReadIndex。
使用 ReadIndex,咱們也能夠很是方便的提供 follower read 的功能,follower 收到 read 請求以後,直接給 leader 發送一個獲取 ReadIndex 的命令,leader 仍然走一遍以前的流程,而後將 ReadIndex 返回給 follower,follower 等到當前的狀態機的 apply index 超過 ReadIndex 以後,就能夠 read 而後將結果返回給 client 了。
雖然 ReadIndex 比原來的 Raft log read 快了不少,但畢竟仍是有 Heartbeat 的開銷,因此咱們能夠考慮作更進一步的優化。
在 Raft 論文裏面,提到了一種經過 clock + heartbeat 的 lease read 優化方法。也就是 leader 發送 heartbeat 的時候,會首先記錄一個時間點 start,當系統大部分節點都回復了 heartbeat response,那麼咱們就能夠認爲 leader 的 lease 有效期能夠到 start + election timeout / clock drift bound
這個時間點。
爲何可以這麼認爲呢?主要是在於 Raft 的選舉機制,由於 follower 會在至少 election timeout 的時間以後,纔會從新發生選舉,因此下一個 leader 選出來的時間必定能夠保證大於 start + election timeout / clock drift bound
。
雖然採用 lease 的作法很高效,但仍然會面臨風險問題,也就是咱們有了一個預設的前提,各個服務器的 CPU clock 的時間是準的,即便有偏差,也會在一個很是小的 bound 範圍裏面,若是各個服務器之間 clock 走的頻率不同,有些太快,有些太慢,這套 lease 機制就可能出問題。
TiKV 使用了 lease read 機制,主要是咱們以爲在大多數狀況下面 CPU 時鐘都是正確的,固然這裏會有隱患,因此咱們也仍然提供了 ReadIndex 的方案。
TiKV 的 lease read 實如今原理上面跟 Raft 論文上面的同樣,但實現細節上面有些差異,咱們並無經過 heartbeat 來更新 lease,而是經過寫操做。對於任何的寫入操做,都會走一次 Raft log,因此咱們在 propose 此次 write 請求的時候,記錄下當前的時間戳 start,而後等到對應的請求 apply 以後,咱們就能夠續約 leader 的 lease。固然實際實現還有不少細節須要考慮的,譬如:
咱們使用的 monotonic raw clock,而 不是 monotonic clock,由於 monotonic clock 雖然不會出現 time jump back 的狀況,但它的速率仍然會受到 NTP 等的影響。
咱們默認的 election timeout 是 10s,而咱們會用 9s 的一個固定 max time 值來續約 lease,這樣一個是爲了處理 clock drift bound 的問題,而另外一個則是爲了保證在滾動升級 TiKV 的時候,若是用戶調整了 election timeout,lease read 仍然是正確的。由於有了 max lease time,用戶的 election timeout 只能設置的比這個值大,也就是 election timeout 只能調大,這樣的好處在於滾動升級的時候即便出現了 leader 腦裂,咱們也必定可以保證下一個 leader 選舉出來的時候,老的 leader lease 已通過期了。
固然,使用 Raft log 來更新 lease 還有一個問題,就是若是用戶長時間沒有寫入操做,這時候來的讀取操做由於早就已經沒有 lease 了,因此只能強制走一次上面的 ReadIndex 機制來 read,但上面已經說了,這套機制性能也是有保證的。至於爲何咱們不在 heartbeat 那邊更新 lease,緣由就是咱們 TiKV 的 Raft 代碼想跟 etcd 保持一致,但 etcd 沒這個需求,因此咱們就作到了外面。
在 TiKV 裏面,從最開始的 Raft log read,到後面的 Lease Read,咱們一步一步的在保證線性一致性的狀況下面改進着性能。後面,咱們會引入更多的一致性測試 case 來驗證整個系統的安全性,固然,也會持續的提高性能。