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

PingCAP發表於2019-03-02

作者:黃東旭

“我希望能夠把 TiDB 的設計的一些理念能夠更好的傳達給大家,相信大家理解了背後原因後,就能夠把 TiDB 用的更好。”

做 TiDB 的緣起是從思考一個問題開始的:為什麼在資料庫領域有這麼多永遠也躲不開的坑?從 2015 年我們寫下第一行程式碼,3 年以來我們迎面遇到無數個問題,一邊思考一邊做,儘量用最小的代價來快速奔跑。

作為一個開源專案,TiDB 是我們基礎架構工程師和社群一起努力的結果,TiDB 已經發版到 2.0,有了一個比較穩定的形態,大量在生產環境使用的夥伴們。可以負責任的說,我們做的任何決定都經過了非常慎重的思考和實踐,是經過內部和社群一起論證產生的結果。它未必是最好的,但是在這個階段應該是最適合我們的,而且大家也可以看到 TiDB 在快速迭代進化。

這篇文章是關於 TiDB 代表性“為什麼”的 TOP 10,希望大家在瞭解了我們這些背後的選擇之後,能更加純熟的使用 TiDB,讓它在適合的環境裡更好的發揮價值。

這個世界有很多人,感覺大於思想,疑問多於答案。感恩大家保持疑問,我們承諾回饋我們的思考過程,畢竟有時候很多思考也很有意思。

一、為什麼分散式系統並不是銀彈

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

我們做 TiDB 的初衷並不是想要在小資料量下取代 MySQL,而是嘗試去解決基於單機資料庫解決不了的一些本質的問題。

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

而且另一方面,選擇 TiDB 和選擇 MySQL 並不是一刀切的有你沒他的過程,我們為了能讓 MySQL 的使用者儘可能減小遷移和改造成本,做了大量的工具能讓整個資料遷移和灰度上線變得平滑,甚至從 TiDB 無縫的遷移回來,而且有些小資料量的業務你仍然可以繼續使用 MySQL。所以一開始如果你的業務和資料量還小,大膽放心的用 MySQL 吧,MySQL still rocks,TiDB 在未來等你。

二、為什麼是 MySQL

和上面提到的一樣,並不是 MySQL 不好我們要取代他,而是選擇相容 MySQL 的生態對我們來說是最貼近使用者實際場景的選擇:

  1. MySQL 的社群足夠大,有著特別良好的群眾基礎,作為一個新的資料庫來說,如果需要使用者去學習一套新的語法,同時伴隨很重的業務遷移的話,是很不利於新專案冷啟動的。

  2. MySQL 那麼長時間積累下來大量的測試用例和各種依賴 MySQL 的第三方框架和工具的測試用例是我們一個很重要的測試資源,特別是在早期,你如何證明你的資料庫是對的,MySQL 的測試就是我們的一把尺子。

  3. 已經有大量的已有業務正在使用 MySQL,同時也遇到了擴充套件性的問題,如果放棄這部分有直接痛點的場景和使用者,也是不明智的。

另一方面來看,MySQL 自從被 Oracle 收購後,不管是效能還是穩定性這幾年都在穩步的提升,甚至在某些場景下,已經開始有替換 Oracle 的能力,從大的發展趨勢上來說,是非常健康的,所以跟隨著這個健康的社群一起成長,對我們來說也是一個商業上的策略。

三、為什麼 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,所以更明確的分層是一個很自然的而且代價不大的選擇。

四、為什麼不復用 MySQL 的 SQL 層,而是選擇自己重寫

這點是我們和很多友商非常不一樣的地方。 目前已有的很多方案,例如 Aurora 之類的,都是直接基於 MySQL 的原始碼,保留 SQL 層,下面替換儲存引擎的方式實現擴充套件,這個方案有幾個好處:一是 SQL 層程式碼直接複用,確實減輕了一開始的開發負擔,二是面向使用者這端確實能做到 100% 相容 MySQL 應用。

但是缺點也很明顯,MySQL 已經是一個 20 多年的老專案,設計之初也沒考慮分散式的場景,整個 SQL 層並不能很好的利用資料分佈的特性生成更優的查詢計劃,雖然替換底層儲存的方案使得儲存層看上去能 Scale,但是計算層並沒有,在一些比較複雜的 Query 上就能看出來。另外,如果需要真正能夠大範圍水平擴充套件的分散式事務,依靠 MySQL 原生的事務機制還是不夠的。

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

五、為什麼用 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 組,我在我的 這篇文章 裡面描述了這個過程。

六、為什麼有這樣的硬體配置要求

我們其實對生產環境硬體的要求還是蠻高的,對於儲存節點來說,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 對這種情況做了層層的內部限流,但是很多情況也無濟於事。 所以我們決定將配置檢查作為部署指令碼的強制檢查,一是減少了很多溝通成本,二是可以讓使用者在上線時儘可能的減少後顧之憂。

七、為什麼用 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 之類的記憶體快取中,或者甚至直接放在業務服務的記憶體裡面(反正小)。

八、為什麼效能(延遲)不是唯一的評價標準

很多朋友問過我,TiDB 能替換 Redis 嗎?大家對 Redis 和 TiDB 的喜愛之情我也很能理解,但是很遺憾,TiDB 並不是一個快取服務,它支援跨行強一致事務,在非易失裝置上實現持久化儲存,而這些都是有代價的。

簡單來說,寫磁碟的 IO 開銷 (WAL,持久化),多副本高可用和保證分散式事務必然會犧牲延遲,更不用說做跨資料中心的同步了,在這點上,我認為如果需要很低延遲的響應速度(亞毫秒級)就需要在業務端做快取了。TiDB 的定位是給業務提供一個可擴充套件的 The Source of Truth (真相之源),即使業務層的快取失效,也有一個地方能夠提供強一致的資料,而且業務不用關心容量問題。**另一方面,衡量一個分散式系統更有意義的指標是吞吐,這個觀點我在很多文章裡已經提到過,提高併發度,如果系統的吞吐能夠隨著叢集機器數量線性提升,而且延遲是穩定的才有意義,而且這樣才能有無限的提升空間。**在實際的環境中,單個 TiDB 叢集已經有一些使用者使用到了百萬級別的 QPS,這個在單機架構上是幾乎不可能實現的。另外,這幾年硬體的進步速度非常快,特別是 IO 相關的創新,比如 NVMe SSD 的普及,還有剛剛商用的持久化記憶體等新的儲存介質。很多時候我們在軟體層面上絞盡腦汁甚至犧牲程式碼的優雅換來一點點效能提升,很可能換塊磁碟就能帶來成倍的提升。

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

九、為什麼彈性伸縮能力如此重要

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

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

最近這段時間,有兩個使用者的例子讓我印象很深,也很自豪,一個是 Mobike,一個是轉轉,前者是 TiDB 的早期使用者,他們自己也在資料增長很快的時候就開始使用 TiDB,在快速的發展過程中沒有因為資料庫的問題掉鏈子;後者是典型的快速發展的網際網路公司,一個 All-in TiDB 的公司,這個早期的技術選型極大的解放了業務開發的生產力,讓業務能夠更放開手腳去寫業務程式碼,而不是陷入無休止的選擇 Sharding key,做讀寫分離等等和資料庫較勁的事情。

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

十、如何根據自己的實際情況參考業內的使用案例

TiDB 是一個通用的資料庫,甚至希望比一般的資料庫更加通用,TiDB 是很早就嘗試融合 OLTP 和 OLAP 的邊界的資料庫產品,我們是最早將 HTAP 這個概念從實驗室和論文裡帶到現實的產品之一。這類通用基礎軟體面臨的一個問題就是我們在早期其實很難去指導垂直行業的使用者把 TiDB 用好,畢竟各自領域都有各自的使用場景和特點,TiDB 的開發團隊的背景大部分是網際網路行業,所以天然的會對網際網路領域的架構和場景更熟悉,但是比如在金融,遊戲,電商,傳統制造業這些行業裡其實我們不是專家,不過現在都已經有很多的行業專家和開發者已經能將 TiDB 在各自領域用得很好。

我們的 Blog,公眾號,官網等平臺會作為一個案例分享的中心,歡迎各位正在使用 TiDB 的使用者,將你們的思考和使用經驗分享給我們,就像現在已有案例背後的許多公司一樣,我們會將你們的經驗變成一篇篇的使用者案例,通過我們的平臺分享給其他的正在選型的公司。

相關文章