1.TiKV框架圖和模塊說明
圖1 TiKV整體架構圖
1.1.各模塊說明
PD Cluster:它是由多個PD節點組成的etcd集群,PD是具有“上帝視角”的管理組件,負責存儲元數據和進行負載均衡,比如Region對應的range段信息、調度Region切分和合並等;
gRPC:開源遠程過程調用系統,客戶端服務端可基於該協議進行請求通信;
Placement Driver:管理TiKV集群,管理着整個集群的元數據信息,負責檢查數據一致性和數據自動平衡遷移;
TiKV node:用來存儲鍵值對的節點;
TxnKV API:支持事務操作的API;
RawKV API:不保證事務的的API;
Raft:一致性算法,TiKV集群使用了該算法來同步節點數據;
RocksDB:TiKV的真實后端存儲組件,RocksDB本身是個開源的鍵值對存儲系統;
Region:鍵值對數據移動的基本單位,每個region被復制到多個Nodes;
Raft group:多個同Region就組成一個Raft group,比如圖中不同顏色的Region,同顏色的就組成一個Raft group;
Leader:每個Raft gorup會有個Leader,負責處理客戶端的請求讀或寫,請求會先到Leader節點,再由Leader節點通知從節點修改。
1.2.功能特性
(1)多副本數據和數據自動均衡;
(2)容錯和數據恢復;
(3)支持設置key的過期時間;
(4)支持原子性的CAS(compare-and-swap);
(5)支持分布式事務;
2.TiKV工作流程原理
2.1. 分布式事務
TiKV的事務模型是使用Percolator Transaction model。
該事務模型依賴於一個時間戳服務,我們稱它為timestamp oracle,它會定時預先分配一個范圍時間戳,並且會將最大的那個時間戳保存到磁盤上,然后即可在內存中遞增產生范圍內的時間戳給請求,這樣即使該時間戳服務宕機了,下一次它預分配的也會從之前在磁盤上保存的那個最高時間戳后開始進行預分配,保證分配的時間戳永遠是不會回退的。該時間戳服務是嵌入到PD服務里的,由PD leader進行服務。
Percolator最先是應用在google的BigTable項目上的,它是一個支持單行事務的分布式存儲系統。
Percolator有CF(column family )的概念,類似於Rocksdb中的CF,每個CF會對應一個LSM Tree,但共享與一個WAL。
Percolator有5個CF,分別是lock、data、write、notify和ack;
Tikv里只涉及到前面3個,這里只講述前面3個。
當開啟一個事務寫入一個 key-value 的時候:
Prewrite階段(兩階段中的第一階段):
lock CF:將該key的lock放到lock CF;
data CF:將該key對應的value放到data CF;
Commit階段(兩階段中的第二階段):
write CF:將相對應的commit信息放到write CF;
寫數據過程:
在提交數據時采用兩階段提交。
Prewrite階段:
(1)獲取事務的開始時間戳start_ts;
(2)將事務涉及到的多個數據在lock CF中進行寫入,寫入時會檢查是否該數據是否已經被其它事務鎖住,如果是則進行回滾;並且從多個數據中選擇一個作為primary lock,其它的使用secondary lock,secondary lock里包含了primary對應數據的信息;
(3)將新數據寫入到data CF中,同時在寫入時也需要檢查該數據是否有大於start_ts時間戳的事務更新提交,如果有則表示有沖突,需要進行回滾
如果Prewrite階段沒有沖突,則Prewrite階段成功,進入Commit階段。
Commit階段:
(1)獲取commit時間戳commit_ts;
(2)對primary的數據進行寫入,將commit信息寫入到write CF中,從lock CF中移除該數據的primary lock;
(3)對Secondary的數據進行寫入,類似primary一樣的操作;
注意:commit階段當完成了第(2)步的primary數據的commit后就表示這個事務已經成功了,即使第(3)步的Secondary的數據commit失敗了也不影響整個事務表示成功,所以commit階段當完成了前兩步就向客戶端表示事務成功,第(3)是使用異步步的方式進行commit的。原因是Prewrite階段上鎖成功表示commit階段不會有沖突問題,所以一般都會成功,但有一種情況會失敗,那就是比如發生了類似宕機這樣的事情,那么此時Secondary鎖還會在lock CF里,所以其它事務在檢測該數據鎖時是判斷不了該數據事務是否已提交的,它可以通過Secondary lock獲取到之前Primary的信息,然后去查找對應的Primary數據是否commit成功,如果是則表示該事務已經提交成功了,繼續執行,如果Primary lock也還存在還未提交則上鎖失敗。
舉例:
假設Bob有10元,Joe有2元,現在Bob要轉7元給Joe。
圖2
如上圖,這里涉及到兩個key,Bob和Joe,它們目前key情況是最新提交是6,對應的data值是10和2。
現在進行轉賬操作,進行數據提交,那么在Prewrite階段,就要進行lock CF寫入和data CF寫入,寫入后如下圖:
圖3
可以看到Bob該key被選中為Primary,並且data里都寫上了經過轉賬后的值,分別為3和9,並且前面的7為申請的start_ts。
Prewrite階段順利,進入commit階段,先primary的key進行commit,commit后的情況如下圖:
圖4
可以看到Bob的primary lock已經移除,Joe的還沒commit。
Joe的也進行commit得到如下圖所示:
圖5
讀過程:
假設以圖5為例,讀取Bob key的值。
(1)首先也需要申請一個start_ts;
(2)然后在lock CF上搜索Bob key在[0, start_ts]上有沒有被上鎖,如果有則讀取失敗待會進行重試,可以看到Bob key上是沒有鎖的;
(3)從write中獲取[0, start_ts]最新的寫事務提交值,取到了該key最新寫事務的start_ts,從圖中可以看到是7;
(4)通過7去獲取data CF上的start_ts為7時的值,從圖中可知是3,獲取成功並返回。
TiKV中的Percolator跟上述講的類似,不過它的CF是defautl、lock和write,default對應的是上面所描述的data。同時也做了一些優化。
從上面過程中,我們看到在寫入data時,是將key和start_ts一起寫入的,start_ts是一個8bytes的值,會將它用大端序表示並進行取反(最新事務start_ts比舊事務start_ts大,取反后就會比舊start_ts小了),這樣做的目的是因為rocksdb存儲的LSM Tree的key是按序存放的,所以相同的key的不同版本會是相鄰的且最新的事務的key排在前面,這樣在查找最新事務提交時就會最先找到。
優化點:
(1)一個事務多個keys的Prewrite會分發到多個tikv節點進行並發預寫,當有一個失敗時,則進行回滾;
(2)對於比較小的value,在兩階段提交時,數據最后不放入data CF里,而是直接存放到write CF里,這樣就不用先在write CF找,然后再在data CF里找,只需要一個LSM Tree的查找;
(3)如果事務只讀取單個key,沒必要獲取start_ts,直接從write CF里讀取最新版本的提交;
(4)由於單個Region的多個key的寫入是原子方式寫入的,所以對於一個事務,如果涉及的寫入的key都是在同一個Region的話,就可以不使用兩階段提交方式寫入了,直接1階段提交寫入。
2.2. 寫入流程
在TiKV中,Region是保存key的基本單元,client端在讀寫數據時,都會先從pd中獲取指定key對應的Region信息,比如Region對應的leader tikv節點,然后向該節點發起請求,同時該Region的信息也會被緩存下來可用來加速后續的同樣在該Region的key的讀寫。
非事務的寫入流程圖:
非事務寫入流程:
(1)client端獲取操作的key所在的Region信息;
(2)PD返回該key所在Region的信息,包括Region對應的TiKV Leader節點信息等;
(3)向TiKV服務發起寫請求;
(4)由於Region是一個Raft group,這期間會進行一個Raft協議共識,會讓該Region的followers節點也收到該操作日志(把操作當做一個日志,進行日志復制,應用時解析該日志進行執行),收到半數以上回復時即Leader節點應用該日志並回復客戶端,且在下一次心跳時告訴客戶端應用該日志;
(5)回復處理結果。
非事務的讀取流程圖:
非事務讀取流程:
(1)向PD獲取Region的信息;
(2)返回Region信息;
(3)請求TiKV節點讀取key值;
(4)返回key值信息;
事務寫請求圖:
事務寫請求流程:
注意,這里圖中只寫了事務中只有一個key更改的情況,沒有代表性,流程里講時會加入b也更改,且與a在不同的Region。
(1)開啟事務獲取事務start_ts;
(2)client端獲取a和b的Region信息,假設a對應Region1,b對應Region2,那么在Prewrite階段,client端會並行分別向這兩個Region節點發送寫請求進行預寫,同時參數里會帶上start_ts和Primary或Secondary,預寫入的過程就類似於上面寫的Percolator的Prewrite的過程,這里以key a為例講述具體寫入CF的過程,假設申請的start_ts是10,key b也是一樣的,如果兩者有一個Prewrite階段失敗,那么就是失敗,進行回滾操作;
首先是寫lock CF:
lock CF:W a = Primary
然后是data CF:
data CF:a_10 = new_value
(3)當key a和b的Prewrite都成功的情況下進行Commit階段;
(4)申請commit_ts,假設是11;
(5)此時client端只會先向key a的Region1發起commit請求(因為它是Primary lock),然后就是Percolator的commit階段
寫write CF,它的值是start_ts:
write CF:a_11 = 10
(6)key a的commit成功則向用戶返回事務成功了,然后再異步提交key b的commit,這里key b就算失敗也無礙的原因在Percolator里說過了,本質就是其實CF里都已經記錄下最新值的修改了,只是Secondary的lock沒有移除掉。
事務的讀請求流程圖:
從Leader Region節點讀取值。
2.3.Range划分
tikv的range是一種按照key字節序進行排序的可看做是無限的sorted map,如果將該range按指定的點進行切分成多段range,那么每段range就是一個region;
tikv初始只有一個region,可以記為["", ""),region遵循左閉右開,比如如果使用key為abc1對該region進行切分則會得到兩個region:
region1:["", "abc")
region2:["abc", "")
默認配置下,一個Region的保存上限大小是96M,當Region保存的數據大於96M時,就會進行Region自動切分,分成均衡的兩個Region。
配置參數:region-split-size
TiKV在4.0版本引入了Load Base Split特性,該特性是用來解決Region熱點問題,當大量請求都打到一個Region時,由於一個Region的讀寫都是由一個節點上的Leader進行處理的,導致大部分請求由一個節點處理,會造成瓶頸,該功能特性的原理是基於統計的信息進行判斷,如果某個Region 10s內的qps或流量超過了配置文件中指定的值,則會對其進行Region拆分,並被調度分配到不同的節點上以打散熱點Region。
相關配置參數:
QPS閾值參數:split.qps-threshold
流量閾值參數:split.byte-threshold
除了分裂,Region也會進行合並操作,避免有大量的空Region存在,造成大量的通信和管理開銷。
系統會定時的去輪詢檢測所有Region,如果Region的大小大於max-merge-region-size配置值(默認20M),則不會與相鄰的Region進行合並;如果Region的key的數量大於max-merge-region-keys配置值(默認
200000個)則不會與相鄰的Region進行合並;否則其它情況都會與相鄰的Region進行合並。