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

PingCAP發表於2017-05-30

資料庫、作業系統和編譯器並稱為三大系統,可以說是整個計算機軟體的基石。其中資料庫更靠近應用層,是很多業務的支撐。這一領域經過了幾十年的發展,不斷的有新的進展。 很多人用過資料庫,但是很少有人實現過一個資料庫,特別是實現一個分散式資料庫。瞭解資料庫的實現原理和細節,一方面可以提高個人技術,對構建其他系統有幫助,另一方面也有利於用好資料庫。 研究一門技術最好的方法是研究其中一個開源專案,資料庫也不例外。單機資料庫領域有很多很好的開源專案,其中 MySQL 和 PostgreSQL 是其中知名度最高的兩個,不少同學都看過這兩個專案的程式碼。但是分散式資料庫方面,好的開源專案並不多。 TiDB 目前獲得了廣泛的關注,特別是一些技術愛好者,希望能夠參與這個專案。由於分散式資料庫自身的複雜性,很多人並不能很好的理解整個專案,所以我希望能寫一些文章,自頂向上,由淺入深,講述 TiDB 的一些技術原理,包括使用者可見的技術以及大量隱藏在 SQL 介面後使用者不可見的技術點。

本篇為本系列文章第一章

儲存資料

213f0002867b0bfc15bc.jpg-24kB

資料庫最根本的功能是能把資料存下來,所以我們從這裡開始。 儲存資料的方法很多,最簡單的方法是直接在記憶體中建一個資料結構,儲存使用者發來的資料。比如用一個陣列,每當收到一條資料就向陣列中追加一條記錄。這個方案十分簡單,能滿足最基本,並且效能肯定會很好,但是除此之外卻是漏洞百出,其中最大的問題是資料完全在記憶體中,一旦停機或者是服務重啟,資料就會永久丟失。 為了解決資料丟失問題,我們可以把資料放在非易失儲存介質(比如硬碟)中。改進的方案是在磁碟上建立一個檔案,收到一條資料,就在檔案中 Append 一行。OK,我們現在有了一個能持久化儲存資料的方案。但是還不夠好,假設這塊磁碟出現了壞道呢?我們可以做 RAID (Redundant Array of Independent Disks),提供單機冗餘儲存。如果整臺機器都掛了呢?比如出現了火災,RAID 也保不住這些資料。我們還可以將儲存改用網路儲存,或者是通過硬體或者軟體進行儲存複製。到這裡似乎我們已經解決了資料安全問題,可以鬆一口氣了。But,做複製過程中是否能保證副本之間的一致性?也就是在保證資料不丟的前提下,還要保證資料不錯。保證資料不丟不錯只是一項最基本的要求,還有更多令人頭疼的問題等待解決:

  • 能否支援跨資料中心的容災?
  • 寫入速度是否夠快?
  • 資料儲存下來後,是否方便讀取?
  • 儲存的資料如何修改?如何支援併發的修改?
  • 如何原子地修改多條記錄? 這些問題每一項都非常難,但是要做一個優秀的資料儲存系統,必須要解決上述的每一個難題。 為了解決資料儲存問題,我們開發了 TiKV 這個專案。接下來我向大家介紹一下 TiKV 的一些設計思想和基本概念。

Key-Value

作為儲存資料的系統,首先要決定的是資料的儲存模型,也就是資料以什麼樣的形式儲存下來。TiKV 的選擇是 Key-Value 模型,並且提供有序遍歷方法。簡單來講,可以將 TiKV 看做一個巨大的 Map,其中 Key 和 Value 都是原始的 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

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

Raft

好了,萬里長征第一步已經邁出去了,我們已經為資料找到一個高效可靠的本地儲存方案。俗話說,萬事開頭難,然後中間難,最後結尾難。接下來我們面臨一件更難的事情:如何保證單機失效的情況下,資料不丟失,不出錯?簡單來說,我們需要想辦法把資料複製到多臺機器上,這樣一臺機器掛了,我們還有其他的機器上的副本;複雜來說,我們還需要這個複製方案是可靠、高效並且能處理副本失效的情況。聽上去比較難,但是好在我們有 Raft 協議。Raft 是一個一致性演算法,它和 Paxos 等價,但是更加易於理解。這裡是 Raft 的論文,感興趣的可以看一下。本文只會對 Raft 做一個簡要的介紹,細節問題可以參考論文。另外提一點,Raft 論文只是一個基本方案,嚴格按照論文實現,效能會很差,我們對 Raft 協議的實現做了大量的優化。 Raft 是一個一致性協議,提供幾個重要的功能: 1. Leader 選舉 2. 成員變更 3. 日誌複製 TiKV 利用 Raft 來做資料複製,每個資料變更都會落地為一條 Raft 日誌,通過 Raft 的日誌複製功能,將資料安全可靠地同步到 Group 的多數節點中。 212f0006616c28f6576f.jpg-15.6kB

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

Region

講到這裡,我們可以提到一個非常重要的概念:Region。這個概念是理解後續一系列機制的基礎,請仔細閱讀這一節。 前面提到,我們將 TiKV 看做一個巨大的有序的 KV Map,那麼為了實現儲存的水平擴充套件,我們需要將資料分散在多臺機器上。這裡提到的資料分散在多臺機器上和 Raft 的資料複製不是一個概念,在這一節我們先忘記 Raft,假設所有的資料都只有一個副本,這樣更容易理解。 對於一個 KV 系統,將資料分散在多臺機器上有兩種比較典型的方案:一種是按照 Key 做 Hash,根據 Hash 值選擇對應的儲存節點;另一種是分 Range,某一段連續的 Key 都儲存在一個儲存節點上。TiKV 選擇了第二種方式,將整個 Key-Value 空間分成很多段,每一段是一系列連續的 Key,我們將每一段叫做一個 Region,並且我們會盡量保持每個 Region 中儲存的資料不超過一定的大小(這個大小可以配置,目前預設是 64MB)。每一個 Region 都可以用 StartKey 到 EndKey 這樣一個左閉右開區間來描述。 2134000662c8b6c66367.jpg-7.6kB

注意,這裡的 Region 還是和 SQL 中的表沒什麼關係!請各位繼續忘記 SQL,只談 KV。 將資料劃分成 Region 後,我們將會做兩件重要的事情: - 以 Region 為單位,將資料分散在叢集中所有的節點上,並且儘量保證每個節點上服務的 Region 數量差不多 - 以 Region 為單位做 Raft 的複製和成員管理

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

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

對於第二點,TiKV 是以 Region 為單位做資料的複製,也就是一個 Region 的資料會儲存多個副本,我們將每一個副本叫做一個 Replica。Repica 之間是通過 Raft 來保持資料的一致(終於提到了 Raft),一個 Region 的多個 Replica 會儲存在不同的節點上,構成一個 Raft Group。其中一個 Replica 會作為這個 Group 的 Leader,其他的 Replica 作為 Follower。所有的讀和寫都是通過 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 ……

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

事務

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

其他

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

相關文章