十問 TiDB :關於架構設計的一些思考

做者:黃東旭web

「我但願可以把 TiDB 的設計的一些理念可以更好的傳達給你們,相信你們理解了背後緣由後,就可以把 TiDB 用的更好。」

作 TiDB 的緣起是從思考一個問題開始的:爲何在數據庫領域有這麼多永遠也躲不開的坑?從 2015 年咱們寫下第一行代碼,3 年以來咱們迎面遇到無數個問題,一邊思考一邊作,儘可能用最小的代價來快速奔跑。算法

做爲一個開源項目,TiDB 是咱們基礎架構工程師和社區一塊兒努力的結果,TiDB 已經發版到 2.0,有了一個比較穩定的形態,大量在生產環境使用的夥伴們。能夠負責任的說,咱們作的任何決定都通過了很是慎重的思考和實踐,是通過內部和社區一塊兒論證產生的結果。它未必是最好的,可是在這個階段應該是最適合咱們的,並且你們也能夠看到 TiDB 在快速迭代進化。數據庫

這篇文章是關於 TiDB 表明性「爲何」的 TOP 10,但願你們在瞭解了咱們這些背後的選擇以後,能更加純熟的使用 TiDB,讓它在適合的環境裏更好的發揮價值。編程

這個世界有不少人,感受大於思想,疑問多於答案。感恩你們保持疑問,咱們承諾回饋咱們的思考過程,畢竟有時候不少思考也頗有意思。緩存

1、爲何分佈式系統並非銀彈

其實並無什麼技術是完美和包治百病的,在存儲領域更是如此,若是你的數據可以在一個 MySQL 裝下而且服務器的壓力不大,或者對複雜查詢性能要求不高,其實分佈式數據庫並非一個特別好的選擇。 選用分佈式的架構就意味着引入額外的維護成本,並且這個成本對於特別小的業務來講是不太划算的,即便你說須要高可用的能力,那 MySQL 的主從複製 + GTID 的方案可能也基本夠用,這不夠的話,還有最近引入的 Group Replication。並且 MySQL 的社區足夠龐大,你能 Google 找到幾乎一切常見問題的答案。 安全

咱們作 TiDB 的初衷並非想要在小數據量下取代 MySQL,而是嘗試去解決基於單機數據庫解決不了的一些本質的問題。服務器

有不少朋友問我選擇分佈式數據庫的一個比較合適的時機是什麼?我以爲對於每一個公司或者每一個業務都不太同樣,我並不但願一刀切的給個普適的標準(也可能這個標準並不存在),可是有一些事件開始出現的時候:好比是當你發現你的數據庫已經到了你天天開始絞盡腦汁思考數據備份遷移擴容,開始隔三差五的想着優化存儲空間和複雜的慢查詢,或者你開始不自覺的調研數據庫中間件方案時,或者人肉在代碼裏面作 sharding 的時候,這時給本身提個醒,看看 TiDB 是否可以幫助你,我相信大多數時候應該是能夠的。 網絡

並且另外一方面,選擇 TiDB 和選擇 MySQL 並非一刀切的有你沒他的過程,咱們爲了能讓 MySQL 的用戶儘量減少遷移和改形成本,作了大量的工具能讓整個數據遷移和灰度上線變得平滑,甚至從 TiDB 無縫的遷移回來,並且有些小數據量的業務你仍然能夠繼續使用 MySQL。因此一開始若是你的業務和數據量還小,大膽放心的用 MySQL 吧,MySQL still rocks,TiDB 在將來等你。數據結構

2、爲何是 MySQL

和上面提到的同樣,並非 MySQL 很差咱們要取代他,而是選擇兼容 MySQL 的生態對咱們來講是最貼近用戶實際場景的選擇:架構

  1. MySQL 的社區足夠大,有着特別良好的羣衆基礎,做爲一個新的數據庫來講,若是須要用戶去學習一套新的語法,同時伴隨很重的業務遷移的話,是很不利於新項目冷啓動的。
  2. MySQL 那麼長時間積累下來大量的測試用例和各類依賴 MySQL 的第三方框架和工具的測試用例是咱們一個很重要的測試資源,特別是在早期,你如何證實你的數據庫是對的,MySQL 的測試就是咱們的一把尺子。
  3. 已經有大量的已有業務正在使用 MySQL,同時也遇到了擴展性的問題,若是放棄這部分有直接痛點的場景和用戶,也是不明智的。

另外一方面來看,MySQL 自從被 Oracle 收購後,無論是性能仍是穩定性這幾年都在穩步的提高,甚至在某些場景下,已經開始有替換 Oracle 的能力,從大的發展趨勢上來講,是很是健康的,因此跟隨着這個健康的社區一塊兒成長,對咱們來講也是一個商業上的策略。

3、爲何 TiDB 的設計中 SQL 層和存儲層是分開的

一個顯而易見的緣由是對運維的友好性。不少人以爲這個地方稍微有點反直覺,多一個組件不就會增長部署的複雜度嗎?

其實在實際生產環境中,運維並不只僅包含部署。舉個例子,若是在 SQL 層發現了一個 BUG 須要緊急的更新,若是全部部件都是耦合在一塊兒的話,你面臨的就是一次整個集羣的滾動更新,若是分層得當的話,你可能須要的只是更新無狀態的 SQL 層,反之亦然。

另一個更深層次的緣由是成本。存儲和 SQL 所依賴的計算資源是不同的,存儲會依賴 IO,而計算對 CPU 以及內存的要求會更高,無需配置 PCIe/NVMe/Optane 等磁盤,並且這二者是不必定對等的,若是所有耦合在一塊兒的話,對於資源調度是不友好的。 對於 TiDB 來講,目標定位是支持 HTAP,即 OLTP 和 OLAP 須要在同一個系統內部完成。顯然,不一樣的 workload 即便對於 SQL 層的物理資源需求也是不同的,OLAP 請求更多的是吞吐偏好型以及長 query,部分請求會佔用大量內存,而 OLTP 面向的是短平快的請求,優化的是延遲和 OPS (operation per second),在 TiDB 中 SQL 層是無狀態的,因此你能夠將不一樣的 workload 定向到不一樣的物理資源上作到隔離。仍是那句話,對調度器友好,同時調度期的升級也不須要把整個集羣所有升級一遍。

另外一方面,底層存儲使用 KV 對數據進行抽象,是一個更加靈活的選擇。

一個好處是簡單。對於 Scale-out 的需求,對 KV 鍵值對進行分片的難度遠小於對帶有複雜的表結構的結構化數據,另外,存儲層抽象出來後也能夠給計算帶來新的選擇,好比能夠對接其餘的計算引擎,和 TiDB SQL 層同時平行使用,TiSpark 就是一個很好的例子。

從開發角度來講,這個拆分帶來的靈活度還體如今能夠選擇不一樣的編程語言來開發。對於無狀態的計算層來講,咱們選擇了 Go 這樣開發效率極高的語言,而對於存儲層項目 TiKV 來講,是更貼近系統底層,對於性能更加敏感,因此咱們選擇了 Rust,若是全部組件都耦合在一塊兒很難進行這樣的按需多語言的開發,對於開發團隊而言,也能夠實現專業的人幹專業的事情,存儲引擎的開發者和 SQL 優化器的開發者可以並行的開發。 另外對於分佈式系統來講,全部的通訊幾乎都是 RPC,因此更明確的分層是一個很天然的並且代價不大的選擇。

4、爲何不復用 MySQL 的 SQL 層,而是選擇本身重寫

這點是咱們和不少友商很是不同的地方。 目前已有的不少方案,例如 Aurora 之類的,都是直接基於 MySQL 的源碼,保留 SQL 層,下面替換存儲引擎的方式實現擴展,這個方案有幾個好處:一是 SQL 層代碼直接複用,確實減輕了一開始的開發負擔,二是面向用戶這端確實能作到 100% 兼容 MySQL 應用。

可是缺點也很明顯,MySQL 已是一個 20 多年的老項目,設計之初也沒考慮分佈式的場景,整個 SQL 層並不能很好的利用數據分佈的特性生成更優的查詢計劃,雖然替換底層存儲的方案使得存儲層看上去能 Scale,可是計算層並無,在一些比較複雜的 Query 上就能看出來。另外,若是須要真正可以大範圍水平擴展的分佈式事務,依靠 MySQL 原生的事務機制仍是不夠的。

本身重寫整個 SQL 層一開始看上去很困難,但其實只要想清楚,有不少在現代的應用裏使用頻度很小的語法,例如存儲過程什麼的,不去支持就行了,至少從 Parser 這層,工做量並不會很大。 同時優化器這邊本身寫的好處就是可以更好的與底層的存儲配合,另外重寫能夠選擇一些更現代的編程語言和工具,使得開發效率也更高,從長遠來看,是個更加省事的選擇。

5、爲何用 RocksDB 和 Etcd Raft

不少工程師都有着一顆造輪子(玩具)的心,咱們也是,可是作一個工業級的產品就徹底不同了,目前的環境下,作一個新的數據庫,底層的存儲數據結構能選的大概就兩種:1. B+Tree, 2. LSM-Tree。

可是對於 B+Tree 來講每一個寫入,都至少要寫兩次磁盤: 1. 在日誌裏; 2. 刷新髒頁的時候,即便你的寫可能就只改動了一個 Byte,這個 Byte 也會放大成一個頁的寫 (在 MySQL 裏默認 InnoDB 的 Page size 是 16K),雖說 LSM-Tree 也有寫放大的問題,可是好處是 LSM-tree 將全部的隨機寫變成了順序寫(對應的 B+tree 在回刷髒頁的時候可能頁和頁之間並非連續的)。 另外一方面,LSMTree 對壓縮更友好,數據存儲的格式相比 B+Tree 緊湊得多,Facebook 發表了一些關於 MyRocks 的文章對比在他們的 MySQL 從 InnoDB 切換成 MyRocks (Facebook 基於 RocksDB 的 MySQL 存儲引擎)節省了不少的空間。因此 LSM-Tree 是咱們的選擇。

選擇 RocksDB 的出發點是 RocksDB 身後有個龐大且活躍的社區,同時 RocksDB 在 Facebook 已經有了大規模的應用,並且 RocksDB 的接口足夠通用,而且相比原始的 LevelDB 暴露了不少參數能夠進行鍼對性的調優。隨着對於 RocksDB 理解和使用的不斷深刻,咱們也已經成爲 RocksDB 社區最大的使用者和貢獻者之一,另外隨着 RocksDB 的用戶愈來愈多,這個項目也會變得愈來愈好,愈來愈穩定,能夠看到在學術界不少基於 LSM-Tree 的改進都是基於 RocksDB 開發的,另一些硬件廠商,特別是存儲設備廠商不少會針對特定存儲引擎進行優化,RocksDB 也是他們的首選之一。

反過來,本身開發存儲引擎的好處和問題一樣明顯,一是從開發到產品的週期會很長,並且要保證工業級的穩定性和質量不是一個簡單的事情,須要投入大量的人力物力。好處是能夠針對本身的 workload 進行定製的設計和優化,因爲分佈式系統自然的橫向擴展性,單機有限的性能提高對比整個集羣吞吐其實意義不大,把有限的精力投入到高可用和擴展性上是一個更加經濟的選擇。 另外一方面,RocksDB 做爲 LSM-Tree 其實現比工業級的 B+Tree 簡單不少(參考對比 InnoDB),從易於掌握和維護方面來講,也是一個更好的選擇。 固然,隨着咱們對存儲的理解愈來愈深入,發現不少專門針對數據庫的優化在 RocksDB 上實現比較困難,這個時候就須要從新設計新的專門的引擎,就像 CPU 也能作圖像處理,但遠不如 GPU,而 GPU 作機器學習又不如專用的 TPU。

選擇 Etcd Raft 的理由也相似。先說說爲何是 Raft,在 TiDB 項目啓動的時候,咱們其實有過在 MultiPaxos 和 Raft 之間的糾結,後來結論是選擇了 Raft。Raft 的算法總體實現起來更加工程化,從論文就能看出來,論文中甚至連 RPC 的結構都描述了出來,是一個對工業實現很友好的算法,並且當時工業界已經有一個通過大量用戶考驗的開源實現,就是 Etcd。並且 Etcd 更加吸引咱們的地方是它對測試的態度,Etcd 將狀態機的各個接口都抽象得很好,基本上能夠作到與操做系統的 API 分離,極大下降了寫單元測試的難度,同時設計了不少 hook 點可以作諸如錯誤注入等操做,看得出來設計者對於測試的重視程度。

與其本身從新實現一個 Raft,不如借力社區,互相成長。如今咱們也是 Etcd 社區的一個活躍的貢獻者,一些重大的 Features 例如 Learner 等新特性,都是由咱們設計和貢獻給 Etcd 的,同時咱們還在不斷的爲 Etcd 修復 Bug。

沒有徹底複用 Etcd 的主要的緣由是咱們存儲引擎的開發語言使用了 Rust,Etcd 是用 Go 寫的,咱們須要作的一個工做是將他們的 Raft 用 Rust 語言重寫,爲了完成這個事情,咱們第一步是將 Etcd 的單元測試和集成測試先移植過來了(沒錯,這個也是選擇 Etcd 的一個很重要的緣由,有一個測試集做爲參照),以避免移植過程破壞了正確性。另一方面,就如同前面所說,和 Etcd 不同,TiKV 的 Raft 使用的是 Multi-Raft 的模型,同一個集羣內會存在海量的互相獨立 Raft 組,真正複雜的地方在如何安全和動態的分裂,移動及合併多個 Raft 組,我在個人 這篇文章 裏面描述了這個過程。

6、爲何有這樣的硬件配置要求

咱們其實對生產環境硬件的要求仍是蠻高的,對於存儲節點來講,SSD 或者 NVMe 或者 Optane 是剛需,另外對 CPU 及內存的使用要求也很高,同時對大規模的集羣,網絡也會有一些要求 (詳見咱們的官方文檔推薦配置的 相關章節),其中一個很重要的緣由是咱們底層的選擇了 RocksDB 的實現,對於 LSM Tree 來講由於存在寫放大的自然特性,對磁盤吞吐需求會相應的更高,尤爲是 RocksDB 還有相似並行 Compaction 等特性。 並且大多數機械磁盤的機器配置傾向於一臺機器放更大容量的磁盤(相比 SSD),可是相應的內存卻通常來講不會更大,例如 24T 的機械磁盤 + 64G 內存,磁盤存儲的數據量看起來更大,可是大量的隨機讀會轉化爲磁盤的讀,這時候,機械磁盤很容易出現 IO 瓶頸,另外一方面,對於災難恢復和數據遷移來講,也是不太友好的。

另外,TiDB 的各個組件目前使用 gRPC 做爲 RPC 框架,gPRC 是依賴 HTTP2 做爲底層協議,相比不少樸素的 RPC 實現,會有一些額外的 CPU 開銷。TiKV 內部使用 RocksDB 的方式會伴隨大量的 Prefix Scan,這意味着大量的二分查找和字符串比較,這也是和不少傳統的離線數據倉庫很不同的 Pattern,這個會是一個 CPU 密集型的操做。在 TiDB 的 SQL 層這端,SQL 是計算密集型的應用這個天然不用說,另外對內存也有必定的需求。因爲 TiDB 的 SQL 是一個完整的 SQL 實現,表達力和衆多中間件根本不是一個量級,有些算子,好比 Hashjoin,就是會在內存裏開闢一塊大內存來執行 Join,因此若是你的查詢邏輯比較複雜,或者 Join 的一張子表比較大的狀況下(偏 OLAP 實時分析業務),對內存的需求也是比較高的,咱們並無像單機數據庫的優化器同樣,好比 Order by 內存放不下,就退化到磁盤上,咱們的哲學是儘量的使用內存。 若是硬件資源不足,及時的經過拒絕執行和失敗通知用戶,由於有時候半死不活的系統反而更加可怕。

另一方面,還有不少用戶使用 TiDB 的目的是用於替換線上 OLTP 業務,這類業務對於性能要求是比較高的。 一開始咱們並無在安裝階段嚴格檢查用戶的機器配置,結果不少用戶在硬件明顯沒有匹配業務壓力的狀況下上線,可能一開始沒什麼問題,可是峯值壓力一上來,可能就會形成故障,儘管 TiDB 和 TiKV 對這種狀況作了層層的內部限流,可是不少狀況也無濟於事。 因此咱們決定將配置檢查做爲部署腳本的強制檢查,一是減小了不少溝通成本,二是可讓用戶在上線時儘量的減小後顧之憂。

7、爲何用 Range-based 的分片策略,而不是 Hash-based

Hash-based 的問題是實現有序的 Scan API 會比較困難,咱們的目標是實現一個標準的關係型數據庫,因此會有大量的順序掃描的操做,好比 Table Scan,Index Scan 等。用 Hash 分片策略的一個問題就是,可能同一個表的數據是不連續的,一個順序掃描即便幾行均可能會跨越不一樣的機器,因此這個問題上沒得選,只能是 Range 分片。 可是 Range 分片可能會形成一些問題,好比頻繁讀寫小表問題以及單點順序寫入的問題。 在這裏首先澄清一下,靜態分片在咱們這樣的系統裏面是不存在的,例如傳統中間件方案那樣簡單的將數據分片和物理機一一對應的分片策略會形成:

  • 動態添加節點後,須要考慮數據從新分佈,這裏必然須要作動態的數據遷移;
  • 靜態分片對於根據 workload 實時調度是不友好的,例如若是數據存在訪問熱點,系統須要可以快速進行數據遷移以便於將熱點分散在不一樣的物理服務商上。

回到剛纔提到的基於 Range 分片的問題,剛纔我說過,對於順序寫入熱點的問題確實存在,但也不是不可解。對於大壓力的順序寫入的場景大多數是日誌或者相似的場景,這類場景的典型特色是讀寫比懸殊(讀 << 寫),幾乎沒有 Update 和隨機刪除,針對這種場景,寫入壓力其實能夠經過 Partition Table 解決,這個已經在 TiDB 的開發路線圖裏面,今年以內會和你們見面。

另外還有一個頻繁讀寫小表形成的熱點問題。這個緣由是,在底層,TiDB 的數據調度的最小單位是 Region,也就是一段段按字節序排序的鍵值 Key-Value Pairs (默認大小 96M),假設若是一個小表,總大小連 96M 都不到,訪問還特別頻繁,按照目前的機制,若是不強制的手動 Split,調度系統不管將這塊數據調度到什麼位置,新的位置都會出現熱點,因此這個問題本質上是無解的。所以建議若是有相似的訪問 pattern,儘量的將通用的小表放到 Redis 之類的內存緩存中,或者甚至直接放在業務服務的內存裏面(反正小)。

8、爲何性能(延遲)不是惟一的評價標準

不少朋友問過我,TiDB 能替換 Redis 嗎?你們對 Redis 和 TiDB 的喜好之情我也很能理解,可是很遺憾,TiDB 並非一個緩存服務,它支持跨行強一致事務,在非易失設備上實現持久化存儲,而這些都是有代價的。

簡單來講,寫磁盤的 IO 開銷 (WAL,持久化),多副本高可用和保證分佈式事務必然會犧牲延遲,更不用說作跨數據中心的同步了,在這點上,我認爲若是須要很低延遲的響應速度(亞毫秒級)就須要在業務端作緩存了。TiDB 的定位是給業務提供一個可擴展的 The Source of Truth (真相之源),即便業務層的緩存失效,也有一個地方可以提供強一致的數據,並且業務不用關心容量問題。另外一方面,衡量一個分佈式系統更有意義的指標是吞吐,這個觀點我在不少文章裏已經提到過,提升併發度,若是系統的吞吐可以隨着集羣機器數量線性提高,並且延遲是穩定的纔有意義,並且這樣纔能有無限的提高空間。在實際的環境中,單個 TiDB 集羣已經有一些用戶使用到了百萬級別的 QPS,這個在單機架構上是幾乎不可能實現的。另外,這幾年硬件的進步速度很是快,特別是 IO 相關的創新,好比 NVMe SSD 的普及,還有剛剛商用的持久化內存等新的存儲介質。不少時候咱們在軟件層面上絞盡腦汁甚至犧牲代碼的優雅換來一點點性能提高,極可能換塊磁盤就能帶來成倍的提高。

咱們公司內部有一句話:Make it right before making it fast。正確性和可靠性的位置是在性能以前的,畢竟在一個不穩定的系統上談性能是沒有意義的。

9、爲何彈性伸縮能力如此重要

在業務初期,數據量不大,業務流量和壓力不大的時候,基本隨便什麼數據庫都可以搞定,但不少時候業務的爆發性增加多是沒有辦法預期的,特別是一些 ToC 端的應用。早期的 Twitter 用戶必定對時不時的大鯨魚(服務不可用)深惡痛絕,近一點還有前兩年有一段時間爆紅的足記 App,很短的時間以內業務和數據量爆發性增加,數據庫幾乎是全部這些案例中的核心瓶頸。 不少互聯網的 DBA 和年輕的架構師會低估重構業務代碼帶來的隱造成本,在業務早期快速搞定功能和需求是最重要的。想象一下,業務快速增加,服務器天天都由於數據庫過載中止服務的時候,DBA 告訴你沒辦法,先讓你從新去把你的業務全改寫成分庫分表的形式,在代碼裏處處加 Sharding key,犧牲一切非 Sharding key 的多維關聯查詢和相關的跨 Shard 的強一致事務,而後數據複製好多份……這種時候是真正的時間等於金錢,決定這個公司生死存亡的時候不是去寫業務和功能代碼,而是由於基礎設施的限制被迫重構,實際上是很是很差的。 若是這個時候,有一個方案,可以讓你幾乎無成本的,不修改業務代碼的時候對數據庫吞吐進行線性擴展(無腦加機器實際上是最便宜的),最關鍵的是爲了業務的進化爭取了時間,我相信這個選擇其實一點都不難作。

其實作 TiDB 的初心正是如此,咱們過去見到了太多相似的血和淚,實在不能忍了,分庫分表用各類中間件什麼的炫技是很簡單,可是咱們想的是真正解決不少開發者和 DBA 眼前的燃眉之急。

最近這段時間,有兩個用戶的例子讓我印象很深,也很自豪,一個是 Mobike,一個是轉轉,前者是 TiDB 的早期用戶,他們本身也在數據增加很快的時候就開始使用 TiDB,在快速的發展過程當中沒有由於數據庫的問題掉鏈子;後者是典型的快速發展的互聯網公司,一個 All-in TiDB 的公司,這個早期的技術選型極大的解放了業務開發的生產力,讓業務可以更放開手腳去寫業務代碼,而不是陷入無休止的選擇 Sharding key,作讀寫分離等等和數據庫較勁的事情。

爲業務開發提供更加靈活便捷和低成本的智能基礎存儲服務,是咱們作 TiDB 的出發點和落腳點,分佈式/高可用/方便靈活的編程接口/智能省心,這些大的方向上也符合將來主流的技術發展趨勢。對於CEO 、 CTO 和架構師這類的管理者而言,在解決眼前問題的同時,跟隨大的技術方向,不給將來多變的業務埋坑,公司儘量快速發展,這個纔是核心要去思考的問題。

10、如何根據本身的實際狀況參考業內的使用案例

TiDB 是一個通用的數據庫,甚至但願比通常的數據庫更加通用,TiDB 是很早就嘗試融合 OLTP 和 OLAP 的邊界的數據庫產品,咱們是最先將 HTAP 這個概念從實驗室和論文裏帶到現實的產品之一。這類通用基礎軟件面臨的一個問題就是咱們在早期其實很難去指導垂直行業的用戶把 TiDB 用好,畢竟各自領域都有各自的使用場景和特色,TiDB 的開發團隊的背景大部分是互聯網行業,因此自然的會對互聯網領域的架構和場景更熟悉,可是好比在金融,遊戲,電商,傳統制造業這些行業裏其實咱們不是專家,不過如今都已經有不少的行業專家和開發者已經能將 TiDB 在各自領域用得很好。

咱們的 Blog,公衆號,官網等平臺會做爲一個案例分享的中心,歡迎各位正在使用 TiDB 的用戶,將大家的思考和使用經驗分享給咱們,就像如今已有案例背後的許多公司同樣,咱們會將大家的經驗變成一篇篇的用戶案例,經過咱們的平臺分享給其餘的正在選型的公司。

相關文章
相關標籤/搜索