標籤:

三篇文章了解 TiDB 技術內幕——說存儲

資料庫、操作系統和編譯器並稱為三大系統,可以說是整個計算機軟體的基石。其中資料庫更靠近應用層,是很多業務的支撐。這一領域經過了幾十年的發展,不斷的有新的進展。

很多人用過資料庫,但是很少有人實現過一個資料庫,特別是實現一個分散式資料庫。了解資料庫的實現原理和細節,一方面可以提高個人技術,對構建其他系統有幫助,另一方面也有利於用好資料庫。

研究一門技術最好的方法是研究其中一個開源項目,資料庫也不例外。單機資料庫領域有很多很好的開源項目,其中n MySQL 和 PostgreSQL 是其中知名度最高的兩個,不少同學都看過這兩個項目的代碼。但是分散式資料庫方面,好的開源項目並不多。 nTiDB n目前獲得了廣泛的關注,特別是一些技術愛好者,希望能夠參與這個項目。由於分散式資料庫自身的複雜性,很多人並不能很好的理解整個項目,所以我們希望能通過一系列文章,自頂向上,由淺入深,講述n TiDB 的一些技術原理,包括用戶可見的技術以及大量隱藏在 SQL 界面後用戶不可見的技術點。

本文為本系列文章第一篇。

「保存數據」

資料庫最根本的功能是能把數據存下來,所以我們從這裡開始。

保存數據的方法很多,最簡單的方法是直接在內存中建一個數據結構,保存用戶發來的數據。比如用一個數組,每當收到一條數據就向數組中追加一條記錄。這個方案十分簡單,能滿足最基本,並且性能肯定會很好,但是除此之外卻是漏洞百出,其中最大的問題是數據完全在內存中,一旦停機或者是服務重啟,數據就會永久丟失。

為了解決數據丟失問題,我們可以把數據放在非易失存儲介質(比如硬碟)中。改進的方案是在磁碟上創建一個文件,收到一條數據,就在文件中n Append 一行。OK,我們現在有了一個能持久化存儲數據的方案。但是還不夠好,假設這塊磁碟出現了壞道呢?我們可以做 RAID n(Redundant Array of Independent Disks),提供單機冗餘存儲。如果整台機器都掛了呢?比如出現了火災,RAID n也保不住這些數據。我們還可以將存儲改用網路存儲,或者是通過硬體或者軟體進行存儲複製。到這裡似乎我們已經解決了數據安全問題,可以鬆一口氣了。But,做複製過程中是否能保證副本之間的一致性?也就是在保證數據不丟的前提下,還要保證數據不錯。保證數據不丟不錯只是一項最基本的要求,還有更多令人頭疼的問題等待解決:

  • 能否支持跨數據中心的容災?

  • 寫入速度是否夠快?

  • 數據保存下來後,是否方便讀取?

  • 保存的數據如何修改?如何支持並發的修改?

  • 如何原子地修改多條記錄?

這些問題每一項都非常難,但是要做一個優秀的數據存儲系統,必須要解決上述的每一個難題。

為了解決數據存儲問題,我們開發了 TiKV 這個項目。接下來向大家介紹一下 TiKV 的一些設計思想和基本概念。

Key-Value

作為保存數據的系統,首先要決定的是數據的存儲模型,也就是數據以什麼樣的形式保存下來。TiKVn 的選擇是 Key-Value 模型,並且提供有序遍歷方法。簡單來講,可以將 TiKV 看做一個巨大的 Map,其中 Key 和 Value n都是原始的 Byte 數組,在這個 Map 中,Key 按照 Byte 數組總的原始二進位比特位比較順序排列。

大家這裡需要對 TiKV 記住兩點:

1. 這是一個巨大的 Map,也就是存儲的是 Key-Value pair;

2. 這個 Map 中的 Key-Value pair 按照 Key 的二進位順序有序,也就是我們可以 Seek 到某一個 Key 的位置,然後不斷的調用 Next 方法以遞增的順序獲取比這個 Key 大的 Key-Value。

講了這麼多,有人可能會問了,這裡講的存儲模型和 SQL 中表是什麼關係?在這裡有一件重要的事情要說四遍:

這裡的存儲模型和 SQL 中的 Table 無關!

這裡的存儲模型和 SQL 中的 Table 無關!

這裡的存儲模型和 SQL 中的 Table 無關!

這裡的存儲模型和 SQL 中的 Table 無關!

現在讓我們忘記 SQL 中的任何概念,專註於討論如何實現 TiKV 這樣一個高性能高可靠性的巨大的(分散式的) Map。

RocksDB

任何持久化的存儲引擎,數據終歸要保存在磁碟上,TiKVn 也不例外。但是 TiKV 沒有選擇直接向磁碟上寫數據,而是把數據保存在 RocksDB 中,具體的數據落地由 RocksDB n負責。這個選擇的原因是開發一個單機存儲引擎工作量很大,特別是要做一個高性能的單機引擎,需要做各種細緻的優化,而 RocksDB n是一個非常優秀的開源的單機存儲引擎,可以滿足我們對單機引擎的各種要求,而且還有 Facebook n的團隊在做持續的優化,這樣我們只投入很少的精力,就能享受到一個十分強大且在不斷進步的單機引擎。當然,我們也為 RocksDB n貢獻了一些代碼,希望這個項目能越做越好。這裡可以簡單的認為 RocksDB 是一個單機的 Key-Value Map。

Raft

好了,萬里長征第一步已經邁出去了,我們已經為數據找到一個高效可靠的本地存儲方案。俗話說,萬事開頭難,然後中間難,最後結尾難。接下來我們面臨一件更難的事情:如何保證單機失效的情況下,數據不丟失,不出錯?簡單來說,我們需要想辦法把數據複製到多台機器上,這樣一台機器掛了,我們還有其他的機器上的副本;複雜來說,我們還需要這個複製方案是可靠、高效並且能處理副本失效的情況。聽上去比較難,但是好在我們有n Raft 協議。Raft 是一個一致性演算法,它和 Paxos n等價,但是更加易於理解。這裡[raft.github.io/raft.pdf]是n Raft 的論文,感興趣的可以看一下。本文只會對 Raft 做一個簡要的介紹,細節問題可以參考論文。另外提一點,Raft n論文只是一個基本方案,嚴格按照論文實現,性能會很差,我們對 Raft n協議的實現做了大量的優化,具體的優化細節可參考我司首席架構師唐劉同學的這篇文章。

Raft 是一個一致性協議,提供幾個重要的功能:

1. Leader 選舉

2. 成員變更

3. 日誌複製

TiKV 利用 Raft 來做數據複製,每個數據變更都會落地為一條 Raft 日誌,通過 Raft 的日誌複製功能,將數據安全可靠地同步到 Group 的多數節點中。

到這裡我們總結一下,通過單機的n RocksDB,我們可以將數據快速地存儲在磁碟上;通過 Raft,我們可以將數據複製到多台機器上,以防單機失效。數據的寫入是通過 Raft n這一層的介面寫入,而不是直接寫 RocksDB。通過實現 Raft,我們擁有了一個分散式的 KV,現在再也不用擔心某台機器掛掉了。

Region

講到這裡,我們可以提到一個非常重要的概念:Region。這個概念是理解後續一系列機制的基礎,請仔細閱讀這一節。

前面提到,我們將n TiKV 看做一個巨大的有序的 KV Map,那麼為了實現存儲的水平擴展,我們需要將數據分散在多台機器上。這裡提到的數據分散在多台機器上和 nRaft 的數據複製不是一個概念,在這一節我們先忘記 Raft,假設所有的數據都只有一個副本,這樣更容易理解。

對於一個n KV 系統,將數據分散在多台機器上有兩種比較典型的方案:一種是按照 Key 做 Hash,根據 Hash 值選擇對應的存儲節點;另一種是分 nRange,某一段連續的 Key 都保存在一個存儲節點上。TiKV 選擇了第二種方式,將整個 Key-Value n空間分成很多段,每一段是一系列連續的 Key,我們將每一段叫做一個 Region,並且我們會盡量保持每個 Region n中保存的數據不超過一定的大小(這個大小可以配置,目前默認是 64MB)。每一個 Region 都可以用 StartKey 到 EndKey n這樣一個左閉右開區間來描述。

注意,這裡的 Region 還是和 SQL 中的表沒什麼關係! 請各位繼續忘記 SQL,只談 KV。

將數據劃分成 Region 後,我們將會做兩件重要的事情:

  • 以 Region 為單位,將數據分散在集群中所有的節點上,並且盡量保證每個節點上服務的 Region 數量差不多

  • 以 Region 為單位做 Raft 的複製和成員管理

這兩點非常重要,我們一點一點來說。

先看第一點,數據按照n Key 切分成很多 Region,每個 Region 的數據只會保存在一個節點上面。我們的系統會有一個組件來負責將 Region n儘可能均勻的散布在集群中所有的節點上,這樣一方面實現了存儲容量的水平擴展(增加新的節點後,會自動將其他節點上的 Region n調度過來),另一方面也實現了負載均衡(不會出現某個節點有很多數據,其他節點上沒什麼數據的情況)。同時為了保證上層客戶端能夠訪問所需要的數據,我們的系統中也會有一個組件記錄n Region 在節點上面的分布情況,也就是通過任意一個 Key 就能查詢到這個 Key 在哪個 Region 中,以及這個 Region n目前在哪個節點上。至於是哪個組件負責這兩項工作,會在後續介紹。

對於第二點,TiKVn 是以 Region 為單位做數據的複製,也就是一個 Region 的數據會保存多個副本,我們將每一個副本叫做一個 Replica。Repican 之間是通過 Raft 來保持數據的一致(終於提到了 Raft),一個 Region 的多個 Replica 會保存在不同的節點上,構成一個 nRaft Group。其中一個 Replica 會作為這個 Group 的 Leader,其他的 Replica 作為 nFollower。所有的讀和寫都是通過 Leader 進行,再由 Leader 複製給 Follower。

大家理解了 Region 之後,應該可以理解下面這張圖:

我們以 Region 為單位做數據的分散和複製,就有了一個分散式的具備一定容災能力的 KeyValue 系統,不用再擔心數據存不下,或者是磁碟故障丟失數據的問題。這已經很 Cool,但是還不夠完美,我們需要更多的功能。

MVCC

很多資料庫都會實現多版本控制(MVCC),TiKV 也不例外。設想這樣的場景,兩個 Client 同時去修改一個 Key 的 Value,如果沒有 MVCC,就需要對數據上鎖,在分散式場景下,可能會帶來性能以及死鎖問題。

TiKV 的 MVCC 實現是通過在 Key 後面添加 Version 來實現,簡單來說,沒有 MVCC 之前,可以把 TiKV 看做這樣的:

Key1 -> Value

Key2 -> Value

……

KeyN -> Value

有了 MVCC 之後,TiKV 的 Key 排列是這樣的:

Key1-Version3 -> Value

Key1-Version2 -> Value

Key1-Version1 -> Value

……

Key2-Version4 -> Value

Key2-Version3 -> Value

Key2-Version2 -> Value

Key2-Version1 -> Value

……

KeyN-Version2 -> Value

KeyN-Version1 -> Value

……

注意,對於同一個n Key 的多個版本,我們把版本號較大的放在前面,版本號小的放在後面(回憶一下 Key-Value 一節我們介紹過的 Key n是有序的排列),這樣當用戶通過一個 Key + Version 來獲取 Value 的時候,可以將 Key 和 Version 構造出 MVCCn 的 Key,也就是 Key-Version。然後可以直接 Seek(Key-Version),定位到第一個大於等於這個 Key-Versionn 的位置。

這裡有一篇更詳細的文檔,可以進一步閱讀。

事務

TiKV n的事務採用的是 Percolator n【research.google.com/pub】模型,並且做了大量的優化。事務的細節這裡不詳述,大家可以參考論文以及我們的其他文章。這裡只提一點,TiKVn n的事務採用樂觀鎖,事務的執行過程中,不會檢測寫衝突,只有在提交過程中,才會做衝突檢測,衝突的雙方中比較早完成提交的會寫入成功,另一方會嘗試重新執行整個事務。當業務的寫入衝突不嚴重的情況下,這種模型性能會很好,比如隨機更新表中某一行的數據,並且表很大。但是如果業務的寫入衝突嚴重,性能就會很差,舉一個極端的例子,就是計數器,多個客戶端同時修改少量行,導致衝突嚴重的,造成大量的無效重試。

其他

到這裡,我們已經了解了 TiKV 的基本概念和一些細節,理解了這個分散式帶事務的 KV 引擎的分層結構以及如何實現多副本容錯。下一篇文章,將會介紹如何在 KV 的存儲模型之上,構建 SQL 層。(作者:申礫)

推薦閱讀:

GopherChina 2017 演講實錄|申礫:Go in TiDB
TiDB 在 360 金融貸款實時風控場景應用
gRPC-rs:從 C 到 Rust
基於 Tile 連接 Row-Store 和 Column-Store

TAG:TiDB |