您好,登錄后才能下訂單哦!
數據庫、操作系統和編譯器并稱為三大系統,可以說是整個計算機軟件的基石。其中數據庫更靠近應用層,是很多業務的支撐。這一領域經過了幾十年的發展,不斷的有新的進展。
很多人用過數據庫,但是很少有人實現過一個數據庫,特別是實現一個分布式數據庫。了解數據庫的實現原理和細節,一方面可以提高個人技術,對構建其他系統有幫助,另一方面也有利于用好數據庫。
研究一門技術最好的方法是研究其中一個開源項目,數據庫也不例外。單機數據庫領域有很多很好的開源項目,其中 MySQL 和 PostgreSQL 是其中知名度最高的兩個,不少同學都看過這兩個項目的代碼。但是分布式數據庫方面,好的開源項目并不多。 TiDB 目前獲得了廣泛的關注,特別是一些技術愛好者,希望能夠參與這個項目。由于分布式數據庫自身的復雜性,很多人并不能很好的理解整個項目,所以我希望能寫一些文章,自頂向上,由淺入深,講述 TiDB 的一些技術原理,包括用戶可見的技術以及大量隱藏在 SQL 界面后用戶不可見的技術點。
數據庫最根本的功能是能把數據存下來,所以我們從這里開始。
保存數據的方法很多,最簡單的方法是直接在內存中建一個數據結構,保存用戶發來的數據。比如用一個數組,每當收到一條數據就向數組中追加一條記錄。這個方案十分簡單,能滿足最基本,并且性能肯定會很好,但是除此之外卻是漏洞百出,其中最大的問題是數據完全在內存中,一旦停機或者是服務重啟,數據就會永久丟失。
為了解決數據丟失問題,我們可以把數據放在非易失存儲介質(比如硬盤)中。改進的方案是在磁盤上創建一個文件,收到一條數據,就在文件中 Append 一行。OK,我們現在有了一個能持久化存儲數據的方案。但是還不夠好,假設這塊磁盤出現了壞道呢?我們可以做 RAID (Redundant Array of Independent Disks),提供單機冗余存儲。如果整臺機器都掛了呢?比如出現了火災,RAID 也保不住這些數據。我們還可以將存儲改用網絡存儲,或者是通過硬件或者軟件進行存儲復制。到這里似乎我們已經解決了數據安全問題,可以松一口氣了。But,做復制過程中是否能保證副本之間的一致性?也就是在保證數據不丟的前提下,還要保證數據不錯。保證數據不丟不錯只是一項最基本的要求,還有更多令人頭疼的問題等待解決:
這些問題每一項都非常難,但是要做一個優秀的數據存儲系統,必須要解決上述的每一個難題。 為了解決數據存儲問題,我們開發了 TiKV 這個項目。接下來我向大家介紹一下 TiKV 的一些設計思想和基本概念。
作為保存數據的系統,首先要決定的是數據的存儲模型,也就是數據以什么樣的形式保存下來。TiKV 的選擇是 Key-Value 模型,并且提供有序遍歷方法。簡單來講,可以將 TiKV 看做一個巨大的 Map,其中 Key 和 Value 都是原始的 Byte 數組,在這個 Map 中,Key 按照 Byte 數組總的原始二進制比特位比較順序排列。 大家這里需要對 TiKV 記住兩點:
講了這么多,有人可能會問了,這里講的存儲模型和 SQL 中表是什么關系?在這里有一件重要的事情要說四遍:
這里的存儲模型和 SQL 中的 Table 無關! 這里的存儲模型和 SQL 中的 Table 無關! 這里的存儲模型和 SQL 中的 Table 無關! 這里的存儲模型和 SQL 中的 Table 無關!
現在讓我們忘記 SQL 中的任何概念,專注于討論如何實現 TiKV 這樣一個高性能高可靠性的巨大的(分布式的) Map。
任何持久化的存儲引擎,數據終歸要保存在磁盤上,TiKV 也不例外。但是 TiKV 沒有選擇直接向磁盤上寫數據,而是把數據保存在 RocksDB 中,具體的數據落地由 RocksDB 負責。這個選擇的原因是開發一個單機存儲引擎工作量很大,特別是要做一個高性能的單機引擎,需要做各種細致的優化,而 RocksDB 是一個非常優秀的開源的單機存儲引擎,可以滿足我們對單機引擎的各種要求,而且還有 Facebook 的團隊在做持續的優化,這樣我們只投入很少的精力,就能享受到一個十分強大且在不斷進步的單機引擎。當然,我們也為 RocksDB 貢獻了一些代碼,希望這個項目能越做越好。這里可以簡單的認為 RocksDB 是一個單機的 Key-Value Map。
好了,萬里長征第一步已經邁出去了,我們已經為數據找到一個高效可靠的本地存儲方案。俗話說,萬事開頭難,然后中間難,最后結尾難。接下來我們面臨一件更難的事情:如何保證單機失效的情況下,數據不丟失,不出錯?簡單來說,我們需要想辦法把數據復制到多臺機器上,這樣一臺機器掛了,我們還有其他的機器上的副本;復雜來說,我們還需要這個復制方案是可靠、高效并且能處理副本失效的情況。聽上去比較難,但是好在我們有 Raft 協議。Raft 是一個一致性算法,它和 Paxos 等價,但是更加易于理解。這里是 Raft 的論文,感興趣的可以看一下。本文只會對 Raft 做一個簡要的介紹,細節問題可以參考論文。另外提一點,Raft 論文只是一個基本方案,嚴格按照論文實現,性能會很差,我們對 Raft 協議的實現做了大量的優化,具體的優化細節可參考我司首席架構師 tangliu 同學的這篇文章。
Raft 是一個一致性協議,提供幾個重要的功能:
TiKV 利用 Raft 來做數據復制,每個數據變更都會落地為一條 Raft 日志,通過 Raft 的日志復制功能,將數據安全可靠地同步到 Group 的多數節點中。
到這里我們總結一下,通過單機的 RocksDB,我們可以將數據快速地存儲在磁盤上;通過 Raft,我們可以將數據復制到多臺機器上,以防單機失效。數據的寫入是通過 Raft 這一層的接口寫入,而不是直接寫 RocksDB。通過實現 Raft,我們擁有了一個分布式的 KV,現在再也不用擔心某臺機器掛掉了。
講到這里,我們可以提到一個 非常重要的概念:Region。這個概念是理解后續一系列機制的基礎,請仔細閱讀這一節。
前面提到,我們將 TiKV 看做一個巨大的有序的 KV Map,那么為了實現存儲的水平擴展,我們需要將數據分散在多臺機器上。這里提到的數據分散在多臺機器上和 Raft 的數據復制不是一個概念,在這一節我們先忘記 Raft,假設所有的數據都只有一個副本,這樣更容易理解。
對于一個 KV 系統,將數據分散在多臺機器上有兩種比較典型的方案:一種是按照 Key 做 Hash,根據 Hash 值選擇對應的存儲節點;另一種是分 Range,某一段連續的 Key 都保存在一個存儲節點上。TiKV 選擇了第二種方式,將整個 Key-Value 空間分成很多段,每一段是一系列連續的 Key,我們將每一段叫做一個 Region,并且我們會盡量保持每個 Region 中保存的數據不超過一定的大小(這個大小可以配置,目前默認是 64mb)。每一個 Region 都可以用 StartKey 到 EndKey 這樣一個左閉右開區間來描述。
注意,這里的 Region 還是和 SQL 中的表沒什么關系! 請各位繼續忘記 SQL,只談 KV。 將數據劃分成 Region 后,我們將會做 兩件重要的事情:
這兩點非常重要,我們一點一點來說。
先看第一點,數據按照 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),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 層。
-----------------------------------End[Tony.Tang]2018.3.8------------------------------------------------------------
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。