天天看點

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

“我希望能夠把 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 的生态對我們來說是最貼近使用者實際場景的選擇:

MySQL 的社群足夠大,有着特别良好的群衆基礎,作為一個新的資料庫來說,如果需要使用者去學習一套新的文法,同時伴随很重的業務遷移的話,是很不利于新項目冷啟動的。

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

已經有大量的已有業務正在使用 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 的

QQ靓号購買平台

文章對比在他們的 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 組,我在我的 《基于 Raft 建構彈性伸縮的存儲系統的一些實踐》 這篇文章裡面描述了這個過程。

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

我們其實對生産環境硬體的要求還是蠻高的,對于存儲節點來說,SSD 或者 NVMe 或者 Optane 是剛需,另外對 CPU 及記憶體的使用要求也很高,同時對大規模的叢集,網絡也會有一些要求 (詳見我們的官方文檔推薦配置的 相關章節),其中一個很重要的原因是我們底層的選擇了 RocksDB 的實作,對于 LSM Tree 來說因為存在寫放大的天然特性,對磁盤吞吐需求會相應的更高,尤其是 RocksDB 還有類似并行 Compaction 等特性。 而且大多數機械磁盤的機器配置傾向于一台機器放更大容量的磁盤(相比 SSD),但是相應的記憶體卻一般來說不會更大,例如 24T 的機械磁盤 + 64G 記憶體,磁盤存儲的資料量看起來更大,但是大量的随機讀會轉化為磁盤的讀,這時候,機械磁盤很容易出現 IO 瓶頸,另一方面,對于災難恢複和資料遷移來說,也是不太友好的。

另外,TiDB 的各個元件目前使用 gRPC 作為 RPC 架構,gRPC 是依賴 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 在各自領域用得很好。