1. 程式人生 > >TiKV 是如何存取資料的(上)

TiKV 是如何存取資料的(上)

作者:唐劉 siddontang

本文會詳細的介紹 TiKV 是如何處理讀寫請求的,通過該文件,同學們會知道 TiKV 是如何將一個寫請求包含的資料更改儲存到系統,並且能讀出對應的資料的。

本文分為上下兩篇,在上篇中,我們將介紹一些基礎知識,便於大家去理解後面的流程。

基礎知識

Raft

Raft

TiKV 使用 Raft 一致性演算法來保證資料的安全,預設提供的是三個副本支援,這三個副本形成了一個 Raft Group。

當 Client 需要寫入某個資料的時候,Client 會將操作傳送給 Raft Leader,這個在 TiKV 裡面我們叫做 Propose,Leader 會將操作編碼成一個 entry,寫入到自己的 Raft Log 裡面,這個我們叫做 Append。

Leader 也會通過 Raft 演算法將 entry 複製到其他的 Follower 上面,這個我們叫做 Replicate。Follower 收到這個 entry 之後也會同樣進行 Append 操作,順帶告訴 Leader Append 成功。

當 Leader 發現這個 entry 已經被大多數節點 Append,就認為這個 entry 已經是 Committed 的了,然後就可以將 entry 裡面的操作解碼出來,執行並且應用到狀態機裡面,這個我們叫做 Apply。

在 TiKV 裡面,我們提供了 Lease Read,對於 Read 請求,會直接發給 Leader,如果 Leader 確定自己的 lease 沒有過期,那麼就會直接提供 Read 服務,這樣就不用走一次 Raft 了。如果 Leader 發現 lease 過期了,就會強制走一次 Raft 進行續租,然後在提供 Read 服務。

Multi Raft

Multi Raft

因為一個 Raft Group 處理的資料量有限,所以我們會將資料切分成多個 Raft Group,我們叫做 Region。切分的方式是按照 range 進行切分,也就是我們會將資料的 key 按照位元組序進行排序,也就是一個無限的 sorted map,然後將其切分成一段一段(連續)的 key range,每個 key range 當成一個 Region。

兩個相鄰的 Region 之間不允許出現空洞,也就是前面一個 Region 的 end key 就是後一個 Region 的 start key。Region 的 range 使用的是前閉後開的模式 [start, end),對於 key start 來說,它就屬於這個 Region,但對於 end 來說,它其實屬於下一個 Region。

TiKV 的 Region 會有最大 size 的限制,當超過這個閾值之後,就會分裂成兩個 Region,譬如 [a, b) -> [a, ab) + [ab, b),當然,如果 Region 裡面沒有資料,或者只有很少的資料,也會跟相鄰的 Region 進行合併,變成一個更大的 Region,譬如 [a, ab) + [ab, b) -> [a, b)

Percolator

對於同一個 Region 來說,通過 Raft 一致性協議,我們能保證裡面的 key 操作的一致性,但如果我們要同時操作多個數據,而這些資料落在不同的 Region 上面,為了保證操作的一致性,我們就需要分散式事務。

譬如我們需要同時將 a = 1,b = 2 修改成功,而 a 和 b 屬於不同的 Region,那麼當操作結束之後,一定只能出現 a 和 b 要麼都修改成功,要麼都沒有修改成功,不能出現 a 修改了,但 b 沒有修改,或者 b 修改了,a 沒有修改這樣的情況。

最通常的分散式事務的做法就是使用 two-phase commit,也就是俗稱的 2PC,但傳統的 2PC 需要有一個協調者,而我們也需要有機制來保證協調者的高可用。這裡,TiKV 參考了 Google 的 Percolator,對 2PC 進行了優化,來提供分散式事務支援。

Percolator 的原理是比較複雜的,需要關注幾點:

首先,Percolator 需要一個服務 timestamp oracle (TSO) 來分配全域性的 timestamp,這個 timestamp 是按照時間單調遞增的,而且全域性唯一。任何事務在開始的時候會先拿一個 start timestamp (startTS),然後在事務提交的時候會拿一個 commit timestamp (commitTS)。

Percolator 提供三個 column family (CF),Lock,Data 和 Write,當寫入一個 key-value 的時候,會將這個 key 的 lock 放到 Lock CF 裡面,會將實際的 value 放到 Data CF 裡面,如果這次寫入 commit 成功,則會將對應的 commit 資訊放到入 Write CF 裡面。

Key 在 Data CF 和 Write CF 裡面存放的時候,會把對應的時間戳給加到 Key 的後面。在 Data CF 裡面,新增的是 startTS,而在 Write CF 裡面,則是 commitCF。

假設我們需要寫入 a = 1,首先從 TSO 上面拿到一個 startTS,譬如 10,然後我們進入 Percolator 的 PreWrite 階段,在 Lock 和 Data CF 上面寫入資料,如下:

Lock CF: W a = lock

Data CF: W a_10 = value

後面我們會用 W 表示 Write,R 表示 Read, D 表示 Delete,S 表示 Seek。

當 PreWrite 成功之後,就會進入 Commit 階段,會從 TSO 拿一個 commitTS,譬如 11,然後寫入:

Lock CF: D a

Write CF: W a_11 = 10

當 Commit 成功之後,對於一個 key-value 來說,它就會在 Data CF 和 Write CF 裡面都有記錄,在 Data CF 裡面會記錄實際的資料, Write CF 裡面則會記錄對應的 startTS。

當我們要讀取資料的時候,也會先從 TSO 拿到一個 startTS,譬如 12,然後進行讀:

Lock CF: R a

Write CF: S a_12 -> a_11 = 10

Data CF: R a_10

在 Read 流程裡面,首先我們看 Lock CF 裡面是否有 lock,如果有,那麼讀取就失敗了。如果沒有,我們就會在 Write CF 裡面 seek 最新的一個提交版本,這裡我們會找到 11,然後拿到對應的 startTS,這裡就是 10,然後將 key 和 startTS 組合在 Data CF 裡面讀取對應的資料。

上面只是簡單的介紹了下 Percolator 的讀寫流程,實際會比這個複雜的多。

RocksDB

TiKV 會將資料儲存到 RocksDB,RocksDB 是一個 key-value 儲存系統,所以對於 TiKV 來說,任何的資料都最終會轉換成一個或者多個 key-value 存放到 RocksDB 裡面。

每個 TiKV 包含兩個 RocksDB 例項,一個用於儲存 Raft Log,我們後面稱為 Raft RocksDB,而另一個則是存放使用者實際的資料,我們稱為 KV RocksDB。

一個 TiKV 會有多個 Regions,我們在 Raft RocksDB 裡面會使用 Region 的 ID 作為 key 的字首,然後再帶上 Raft Log ID 來唯一標識一條 Raft Log。譬如,假設現在有兩個 Region,ID 分別為 1,2,那麼 Raft Log 在 RocksDB 裡面類似如下存放:

1_1 -> Log {a = 1}

1_2 -> Log {a = 2}

…

1_N -> Log {a = N}

2_1 -> Log {b = 2}

2_2 -> Log {b = 3}

…

2_N -> Log {b = N}

因為我們是按照 range 對 key 進行的切分,那麼在 KV RocksDB 裡面,我們直接使用 key 來進行儲存,類似如下:

a -> N

b -> N

裡面存放了兩個 key,a 和 b,但並沒有使用任何字首進行區分。

RocksDB 支援 Column Family,所以能直接跟 Percolator 裡面的 CF 對應,在 TiKV 裡面,我們在 RocksDB 使用 Default CF 直接對應 Percolator 的 Data CF,另外使用了相同名字的 Lock 和 Write。

PD

TiKV 會將自己所有的 Region 資訊彙報給 PD,這樣 PD 就有了整個叢集的 Region 資訊,當然就有了一張 Region 的路由表,如下:

Router

當 Client 需要操作某一個 key 的資料的時候,它首先會向 PD 問一下這個 key 屬於哪一個 Region,譬如對於 key a 來說,PD 知道它屬於 Region 1,就會給 Client 返回 Region 1 的相關資訊,包括有多少個副本,現在 Leader 是哪一個副本,這個 Leader 副本在哪一個 TiKV 上面。

Client 會將相關的 Region 資訊快取到本地,加速後續的操作,但有可能 Region 的 Raft Leader 變更,或者 Region 出現了分裂,合併,Client 會知道快取失效,然後重新去 PD 獲取最新的資訊。

PD 同時也提供全域性的授時服務,在 Percolator 事務模型裡面,我們知道事務開始以及提交都需要有一個時間戳,這個就是 PD 統一分配的。

基礎知識就介紹到這裡,下篇我們將詳細的介紹 TiKV 的讀寫流程~ 敬請期待!