做者:周昱行git
上篇文章 中,咱們介紹了數據讀寫過程當中 tikv-client 須要解決的幾個具體問題,本文將繼續介紹 tikv-client 裏的兩個主要的模塊——負責處理分佈式計算的 copIterator 和執行二階段提交的 twoPhaseCommitter。github
在介紹 copIterator 的概念以前,咱們須要簡單回顧一下前面 TiDB 源碼閱讀系列文章(六)中講過的 distsql 和 coprocessor 的概念以及它們和 SQL 語句的關係。算法
tikv-server 經過 coprocessor 接口,支持部分 SQL 層的計算能力,大部分只涉及單表數據的經常使用的算子均可如下推到 tikv-server 上計算,計算下推之後,從存儲引擎讀取的數據雖然是同樣的多,可是經過網絡返回的數據會少不少,能夠大幅節省序列化和網絡傳輸的開銷。sql
distsql 是位於 SQL 層和 coprocessor 之間的一層抽象,它把下層的 coprocessor 請求封裝起來對上層提供一個簡單的 Select
方法。執行一個單表的計算任務。最上層的 SQL 語句可能會包含 JOIN
,SUBQUERY
等複雜算子,涉及不少的表,而 distsql 只涉及到單個表的數據。一個 distsql 請求會涉及到多個 region,咱們要對涉及到的每個 region 執行一次 coprocessor 請求。網絡
因此它們的關係是這樣的,一個 SQL 語句包含多個 distsql 請求,一個 distsql 請求包含多個 coprocessor 請求。併發
copIterator 的任務就是實現 distsql 請求,執行全部涉及到的 coprocessor 請求,並依次返回結果。oracle
一個 distsql 請求須要處理的數據是一個單表上的 index scan 或 table scan,在 Request 包含了轉換好的 KeyRange list。接下來,經過 region cache 提供的 LocateKey 方法,咱們能夠找到有哪些 region 包含了一個 key range 範圍內的數據。異步
找到全部 KeyRange 包含的全部的 region 之後,咱們須要按照 region 的 range 把 key range list 進行切分,讓每一個 coprocessor task 裏的 key range list 不會超過 region 的範圍。分佈式
構造出了全部 coprocessor task 以後,下一步就是執行這些 task 了。函數
爲了更容易理解 copIterator 的執行模式,咱們先從最簡單的實現方式開始, 逐步推導到如今的設計。
copIterator 是 kv.Response
接口的實現,須要實現對應 Next 方法,在上層調用 Next 的時候,返回一個 coprocessor response,上層經過屢次調用 Next
方法,獲取多個 coprocessor response,直到全部結果獲取完。
最簡單的實現方式,是在 Next
方法裏,執行一個 coprocessor task,返回這個 task 的執行結果。
這個執行方式的一個很大的問題,大量時間耗費在等待 coprocessor 請求返回結果,咱們須要改進一下。
coprocessor 請求若是是由 Next
觸發的,每次調用 Next
就必須等待一個 RPC round trip
的延遲。咱們能夠改形成請求在 Next
被調用以前觸發,這樣就能在 Next 被調用的時候,更早拿到結果返回,省掉了阻塞等待的過程。
在 copIterator 建立的時候,咱們啓動一個後臺 worker goroutine 來依次執行全部的 coprocessor task,並把執行結果發送到一個 response channel,這樣前臺 Next
方法只須要從這個 channel 裏 receive 一個 coprocessor response 就能夠了。若是這個 task 已經執行完成,Next
方法能夠直接獲取到結果,當即返回。
當全部 coprocessor task 被 work 執行完成的時候,worker 把這個 response channel 關閉,Next
方法在 receive channel 的時候發現 channel 已經關閉,就能夠返回 nil response
,表示全部結果都處理完成了。
以上的執行方案仍是存在一個問題,就是 coprocessor task 只有一個 worker 在執行,沒有並行,性能仍是不理想。
爲了增大並行度,咱們能夠構造多個 worker 來執行 task,把全部的 task 發送到一個 task channel,多個 worker 從這一個 channel 讀取 task,執行完成後,把結果發到 response channel,經過設置 worker 的數量控制併發度。
這樣改造之後,就能夠充分的並行執行了,可是這樣帶來一個新的問題,task 是有序的,可是因爲多個 worker 並行執行,返回的 response 順序是亂序的。對於不要求結果有序的 distsql 請求,這個執行模式是可行的,咱們使用這個模式來執行。對於要求結果有序的 distsql 請求,就不能知足要求了,咱們須要另外一種執行模式。
當 worker 執行完一個 task 以後,當前的作法是把 response 發送到一個全局的 channel 裏,若是咱們給每個 task 建立一個 channel,把 response 發送到這個 task 本身的 response channel 裏,Next 的時候,就能夠按照 task 的順序獲取 response,保證結果的有序。
以上就是 copIterator 最終的執行模式。
理解執行模式以後,咱們從源碼的角度,分析一遍完整的執行流程。
前臺的執行的第一步是 CopClient 的 Send 方法。先根據 distsql 請求裏的 KeyRanges
構造 coprocessor task,用構造好的 task 建立 copIterator,而後調用 copIterator 的 open 方法,啓動多個後臺 worker goroutine,而後啓動一個 sender 用來把 task 丟進 task channel,最後 copIterator 作爲 kv.Reponse
返回。
前臺執行的第二步是屢次調用 kv.Response
的 Next
方法,直到獲取全部的 response。
copIterator 在 Next
裏會根據結果是否有序,選擇相應的執行模式,無序的請求會從 全局 channel 裏獲取結果,有序的請求會在每個 task 的 response channel 裏獲取結果。
從 task channel 獲取到一個 task 以後,worker 會執行 handleTask 來發送 RPC 請求,並處理請求的異常,當 region 分裂的時候,咱們須要從新構造 新的 task,並從新發送。對於有序的 distsql 請求,分裂後的多個 task 的執行結果須要發送到舊的 task 的 response channel 裏,因此一個 task 的 response channel 可能會返回多個 response,發送完成後須要 關閉 task 的 response channel。
2PC 是實現分佈式事務的一種方式,保證跨越多個網絡節點的事務的原子性,不會出現事務只提交一半的問題。
在 TiDB,使用的 2PC 模型是 Google percolator 模型,簡單的理解,percolator 模型和傳統的 2PC 的區別主要在於消除了事務管理器的單點,把事務狀態信息保存在每一個 key 上,大幅提升了分佈式事務的線性 scale 能力,雖然仍然存在一個 timestamp oracle 的單點,可是由於邏輯很是簡單,並且能夠 batch 執行,因此並不會成爲系統的瓶頸。
關於 percolator 模型的細節,能夠參考這篇文章的介紹 https://pingcap.com/blog-cn/percolator-and-txn/
當一個事務準備提交的時候,會建立一個 twoPhaseCommiter,用來執行分佈式的事務。
構造的時候,須要作如下幾件事情
從 memBuffer
和 lockedKeys
裏收集全部的 key 和 mutation
memBuffer
裏的 key 是有序排列的,咱們從頭遍歷 memBuffer
能夠順序的收集到事務裏須要修改的 key,value 長度爲 0 的 entry 表示 DELETE
操做,value 長度大於 0 表示 PUT
操做,memBuffer
裏的第一個 key 作爲事務的 primary key。lockKeys
裏保存的是不須要修改,但須要加讀鎖的 key,也會作爲 mutation 的 LOCK
操做,寫到 TiKV 上。
在收集 mutation 的時候,會統計整個事務的大小,若是超過了最大事務限制,會返回報錯。
太大的事務可能會讓 TiKV 集羣壓力過大,執行失敗並致使集羣不可用,因此要對事務的大小作出硬性的限制。
若是一個事務的 key 經過 prewrite
加鎖後,事務沒有執行完,tidb-server 就掛掉了,這時候集羣內其餘 tidb-server 是沒法讀取這個 key 的,若是沒有 TTL,就會死鎖。設置了 TTL 以後,讀請求就能夠在 TTL 超時以後執行清鎖,而後讀取到數據。
咱們計算一個事務的超時時間須要考慮正常執行一個事務須要花費的時間,若是過短會出現大的事務沒法正常執行完的問題,若是太長,會有異常退出致使某個 key 長時間沒法訪問的問題。因此使用了這樣一個算法,TTL 和事務的大小的平方根成正比,並控制在一個最小值和一個最大值之間。
在 twoPhaseCommiter 建立好之後,下一步就是執行 execute 函數。
在 execute
函數裏,須要在 defer
函數裏執行 cleanupKeys,在事務沒有成功執行的時候,清理掉多餘的鎖,若是不作這一步操做,殘留的鎖會讓讀請求阻塞,直到 TTL 過時纔會被清理。第一步會執行 prewriteKeys,若是成功,會從 PD 獲取一個 commitTS
用來執行 commit
操做。取到了 commitTS
以後,還須要作如下驗證:
commitTS
比 startTS
大
schema 沒有過時
事務的執行時間沒有過長
若是沒有經過檢查,事務會失敗報錯。
經過檢查以後,執行最後一步 commitKeys,若是沒有錯誤,事務就提交完成了。
當 commitKeys
請求遇到了網絡超時,那麼這個事務是否已經提交是不肯定的,這時候不能執行 cleanupKeys
操做,不然就破壞了事務的一致性。咱們對這種狀況返回一個特殊的 undetermined error,讓上層來處理。上層會在遇到這種 error 的時候,把鏈接斷開,而不是返回給用一個執行失敗的錯誤。
prewriteKeys, commitKeys 和 cleanupKeys 有不少相同的邏輯,須要把 keys 根據 region 分紅 batch,而後對每一個 batch 執行一次 RPC。
當 RPC 返回 region 過時的錯誤時,咱們須要把這個 region 上的 keys 從新分紅 batch,發送 RPC 請求。
這部分邏輯咱們把它抽出來,放在 doActionOnKeys 和 doActionOnBatches 裏,並實現 prewriteSinlgeBatch,commitSingleBatch,cleanupSingleBatch 函數,用來執行單個 batch 的 RPC 請求。
雖大部分邏輯是相同的,可是不一樣的請求在執行順序上有一些不一樣,在 doActionOnKeys
裏須要特殊的判斷和處理。
prewrite
分紅的多個 batch 須要同步並行的執行。
commit
分紅的多個 batch 須要先執行第一個 batch,成功後再異步並行執行其餘的 batch。
cleanup
分紅的多個 batch 須要異步並行執行。
doActionOnBatches 會開啓多個 goroutines 並行的執行多個 batch,若是遇到了 error,會把其餘正在執行的 context cancel
掉,而後返回第一個遇到的 error。
執行 prewriteSingleBatch
的時候,有可能會遇到 region 分裂錯誤,這時候 batch 裏的 key 就再也不是一個 region 上的 key 了,咱們會在這裏遞歸的調用 prewriteKeys,從新走一遍拆分 batch 而後執行 doActionOnBatch
和 prewriteSingleBatch
的流程。這部分邏輯在 commitSingleBatch
和 cleanupSingleBatch
裏也都有。
twoPhaseCommitter 包含的邏輯只是事務模型的一小部分,主要的邏輯在 tikv-server 端,超出了這篇文章的範圍,就不在這裏詳細討論了。