作者 | 黃東旭
寫這篇文章的起因是有一次和朋友聊到一個很有趣的話題:如果我要為 1 億用戶提供免費的資料庫雲服務,這個系統應該如何設計?為了回答這個問題,我在之前的兩篇 Blog 中隱隱約約提到,我們在使用一些全新的視角和思路去看待和構建資料庫服務,並將這些思考變成了實際的產品:tidb.cloud/serverless,很多朋友很想了解 TiDB Cloud Serverless (下面會簡稱為 TiDB Serverless)的更多細節,所以便有了這篇博客。
有些朋友可能還不太了解 TiDB,請允許我先快速介紹一下 TiDB 本身。對 TiDB 比較熟悉的朋友可以跳過這段,主要是一些關鍵的名詞和定義:
2015 年初,我們啟動了一個非常有野心的計劃。受到 Google Spanner 和 F1 的啟發(我更願意稱之為鼓舞),我們希望構建一個理想的分布式資料庫:支持 SQL、使用 MySQL 協議和語法、支持 ACID 事務語義、不對一致性妥協;對業務透明且無上限的水平擴展能力,能夠根據而業務流量和存儲特徵動態調整數據的物理分布達到極高的硬體利用率;高可用及故障自愈能力,降低人工運維的負擔。
在這些目標的驅動下,從一開始我們就做出了如下的技術決定:
可以看得出,這個設計就是奔著極強的擴展性和超大規模的場景去的,早期也確實如此。最初,TiDB 的想法來自於替換一些超大規模的 MySQL 分庫分表方案,這個策略很成功,在無數的客戶那已經得到驗證,甚至還替換了一些以規模見長的 NoSQL 的系統,例如 Pinterest 在他們的 Blog 中提到他們將他們的 HBase 成功替換成了 TiDB( https://medium.com/pinterest-engineering/online-data-migration-from-hbase-to-tidb-with-zero-downtime-43f0fb474b84)
回顧當年做的一些決定,TiDB 是很正確的:
但是隨著時間的推移,我們漸漸發現了兩個趨勢:
上面的需求,在傳統的 Shared-nothing 架構下,會有以下幾點限制。
對於第一個趨勢來說,OLTP 和 OLAP 確實在融合,但是在傳統的 Shared-nothing 架構下其實是成本很高的,原因是對於 OLAP 應用來說,大多數時候 CPU 會成為瓶頸,CPU 是最昂貴且的資源。為了追求性能,我們只能增加更多的計算資源(也就是物理伺服器),而 OLAP 業務通常又不是在線服務,所以這些 workload 其實並不要求 7x24 小時獨占這些資源,但是為了運行這些 workload,我們不得不提前將這些資源準備好,即使 TiFlash 在存儲層已經能很好的彈性伸縮(可以按需對特定的表 Ad-hoc 的創建 TiFlash 副本)但很難在非雲的環境下對計算資源進行彈性伸縮。
對於第二個趨勢,傳統 shared-nothing 架構的分布式資料庫通常假設所有物理節點的硬體配置對等,否則會加大調度的難度;另外為了追求極致的性能,通常會放棄多租戶的設計,因為在雲下大多數的資料庫部署對於應用來說都是獨占式的。而要降低資料庫的使用成本不外乎兩條路:
另外就是業務開始上雲,這點提的比較多,就不贅述了。
基於以上的假設,我們思考一個問題:如果今天重寫 TiDB,應該有哪些新的假設、作出哪些選擇,去覆蓋更多應用場景和更高的穩定性。
假設:
選擇:
更重要的選擇是我在之前文章提到的:與其把資料庫放到雲上運維,更應該將雲資料庫作為一個完整的雲服務設計。在這個指導思想下,我們開始了 TiDB Cloud Serverless,因為細節太多,我不可能在這篇文章內一一照顧到,思來想去挑了兩個重點在這篇文章中介紹一下:存儲引擎和多租戶。
存儲引擎設計
在傳統的上下文中,我們提到存儲引擎時一般都想到 LSM-Tree、B-Tree、Bitcask 什麼的,另外在傳統的語境中,這些存儲引擎都是單機的,對於一個分布式資料庫來說,需要在本地存儲引擎之上再構建一層 Sharding 邏輯決定數據在不同物理節點上的分布。
但是在雲上,尤其對於構建一個 Serverless 的存儲服務,數據的物理分布不再重要,因為 S3 (對象存儲,這裡包含但也並不特指 AWS S3) 已經將擴展性和分布式可用性的問題解決得足夠好(而且在開源社區也有高質量的對象存儲實現,如 MinIO 等)。如果我們將在 S3 的基礎之上構建的一個新的「存儲引擎「看成整體的話,我們會發現整個分布式資料庫的設計邏輯被大大簡化了。
為什麼強調存儲引擎?因為計算層抽象得好的話是可以做到無狀態的(參考 TiDB 的 SQL 層),無狀態系統的彈性伸縮相對容易做到,真正的挑戰在於存儲層。從 AWS S3 在 2022 年的論文中我們可以看到,AWS 在解決 S3 的彈性和穩定性問題上花了極大的功夫(其實 S3 才是 AWS 真正的 Serverless 產品!)。
但是 S3 的問題在於小 I/O 的延遲,在 OLTP workload 的主要讀寫鏈路上不能直接穿透到 S3,所以對於寫入而言,仍然需要寫入本地磁碟,但是好在只要確保日誌寫入成功就好。實現一個分布式、低延遲、高可用的日誌存儲(append-only)比起實現一個完整的存儲引擎簡單太多了。所以日誌的複製仍然是需要單獨處理的,可以選擇 Raft 或者 Multi-Paxos 之類的 RSM 算法,偷懶一點也可以直接依賴 EBS。
對於讀請求來說,極低的延遲(<10ms) 基本上只有依靠內存緩存,所以優化的方向也只是做幾級的緩存,或者是否需要用本地磁碟、是按照 page 還是按照 row 來做緩存的問題。另外在分布式環境下還有一個好處是,由於可以在邏輯上將數據分片,所以緩存也可以分散在多台機器上。
對於這個新的存儲引擎,有以下的幾個設計目標:
支持讀寫分離
對於 OLTP 業務來說,通常讀是遠大於寫的,這裡的一個隱含假設是對於強一致讀(read after write)的需求可能只占全部讀請求的很小部分,大多數讀場景可以容忍 100ms 左右 inconsistent gap,但是寫請求對於硬體資源的消耗是更大的(不考慮 cache miss 的場景)。在單機的環境下,這其實是無解的,因為就那麼一畝三分地,讀 / 寫 /compaction 都要消耗本地資源,所以能做的只能有平衡。
但是在雲的環境下,完全可以將讀寫的節點分開:本地通過 Raft 協議複製到多個 Peer 上,同時在後台持續異步同步日誌數據到 S3(遠端的 compact 節點進行 compaction)。如果用戶需要強一致讀,那麼可以在 Raft Leader 上讀,如果沒有強一致的需求,那麼可以在其他的 Peer 節點上讀(考慮到即使異步複製日誌的 gap 也不會太長,畢竟是 Raft),即使在 Non-Leader Peer 上想實現強一致讀,也很簡單,只需要向 Leader 請求一下最新的 Log Index,然後在本地等待這個 Index 的 log 同步過來即可;新增的讀節點,只需要考慮從 S3 上加載 Snapshot 然後緩存預熱後即可對外提供服務。
另外,通過 S3 傳輸 snapshot 數據的額外好處是,在同一個 region 內部,跨 az 的流量是不計的。而且對於 HTAP 場景中的分析請求,因為數據都已經在 S3 上,且大多數分析的場景大約只需要 Read-committed 的隔離級別 + Stale Read 就能滿足需求,因此只需要按照上面描述的讀節點來處理即可,ETL 的反向數據回填也可以通過 S3 完成。
Before
After
支持冷熱分離
對於 OLTP 場景來說,我們觀察到儘管總數據量可能很大,有時 OLTP 單個業務上百 TB 我們也經常見到,但是絕大多數場景下都有冷熱之分,而且對於冷數據來說,只需要提供「可接受」的體驗即可,例如讀寫冷數據的延遲容忍度通常會比較高。
但是就像開始提到的,我們多數時候並不能二元地劃分冷熱,但是我們又希望能通過冷熱分離將冷數據使用更便宜的硬體資源存儲,同時不犧牲熱數據的訪問體驗。在 Serverless TiDB 中,很自然的,冷數據將只存儲在 S3 上,因為分層的存儲設計會很自然地將熱數據保留在本地磁碟和內存中,冷數據在最下層也就是 S3 上。這個設計的壞處是犧牲了冷數據緩存預熱時的延遲,因為要從 S3 上加載。但是就像上面說到的,在現實的場景中,這個延遲通常是可以被容忍的,想要提升這種情況的用戶體驗也很容易,再加入一級緩存即可。
支持極快的彈性伸縮(Scale-out / in),並支持 Scale-to-Zero
在提到冷熱分離的時候,有兩個隱含的話題沒有 cover:
要回答這兩個問題,需要討論:數據分片,請求路由以及數據調度,這三個話題也是支持彈性伸縮的關鍵。
我們先說數據分片。傳統 Shared Nothing 架構的數據分片方式在邏輯上和物理上是一一對應的,例如 TiKV 的一個 region 對應的就是某台 TiKV 節點上的 RocksDB 一段物理數據。另一個更好理解的例子是 MySQL 分庫分表,每個分片就是一台具體的 MySQL。
但是在雲上,如果有這樣一個雲原生的存儲引擎,那我們其實已經不太需要關心數據的物理分布,但是邏輯分片仍然是重要的,因為涉及到計算資源的分配(以及緩存)。過去 Shared-nothing 架構的分布式資料庫的問題是:如果要調整分片就會涉及到數據移動和搬遷,例如添加 / 刪除存儲節點後的 Rebalance。這個過程是最容易出問題的,因為只要涉及到物理數據的移動,保證一致性和高可用都將是複雜的工作,TiKV 的 Multi-Raft 花了無數的精力在這塊。但是對於基於 S3 的存儲引擎來說,數據在分片之間的移動其實只是一個元信息修改,所謂的物理的移動也只是在新的節點上的緩存預熱問題。
有數據的邏輯分片就意味著需要有對應的請求路由層,需要根據規則將請求定位到具體某台機器上。目前我仍然認為就像 TiKV 一樣,KV Range 是一個很好分片方式。像字典一樣,當你拿到一個單詞,通過字典序就能很精準的定位。在 TiDB Serverless 中我們仍然保留了 KV 的分片和邏輯路由,但是會在原本的 TiDB 之上增加一層 Proxy 用於保持不同租戶的 Session ,我們姑且叫它 Gateway。
Gateway 是租戶隔離及流量感知的重要一環(後邊會提到), Gateway 感知到某個租戶的連接被創建後,會在一個緩存的計算資源池中(tidb-server 的 container pool)撈出一個 idle 狀態的 tidb-server pod 來接管客戶端的請求,然後當連接斷開或者超時一段時間後自動歸還給 pool。這個設計對於計算資源的快速伸縮是非常有效的,而且 tidb-server 的無狀態設計,也讓我們在給 TiDB Serverless 實現這部分能力的時候簡單了很多,而且在 Gateway 感知到熱點出現後可以很快的彈性擴容。計算的擴容很好理解,存儲層的擴容也很簡單,只需修改一下元信息 + 在新的節點上掛載遠端存儲並預熱緩存即可。
經濟性
在雲上,一切資源都是需要付費的,而且不同的服務定價差別巨大,對比一下 S3 / RDS / DynamoDB 的定價即可看到很大的區別,而且相比服務費用和存儲成本,大家很容易忽略流量的費用(有時候你看帳單會發現其實比起存儲成本,流量會是更大的開銷)。節省成本有幾個關鍵:
能夠重用原來 TiDB 的大部分代碼
TiKV 對於存儲引擎有著很好的抽象,其實主要修改的地方大概只有原來 RocksDB 的部分。關於 Raft 和分片的邏輯基本是能夠復用的。至於 SQL 引擎和事務的部分,因為原本就和存儲的耦合比較低,所以主要修改的部分就是添加租戶相關的邏輯。這也是為什麼我們能使用一個小團隊,大概一年時間就能推出產品的原因。
多租戶
想像一下,如果你需要支撐一個龐大規模的用戶資料庫,集群中有成千上萬的用戶,每個用戶基本上只訪問自己的數據,並且有明顯的冷熱數據區分。而且就像一開始提到的那樣,可能有 90% 的用戶是小型用戶,但你無法確定這些用戶何時會突然增長。你需要設計一個這樣的系統,以應對這種情況。
最原始的方法很簡單,就是直接將一批機器提供給一個用戶,將另一批機器提供給另一個用戶。這樣的物理隔離方法非常簡單,但缺點是資源分配不夠靈活,並且有許多共享成本。例如,每個租戶都有自己管控系統和日誌存儲等方面的成本。顯然,這是最笨重的物理隔離方法。
第二種方法是在容器平台上進行部署,利用虛擬化,儘可能利用底層雲平台的資源。這種方法可能比第一種方案稍好一些,至少可以更充分地利用底層硬體資源。然而,仍然存在前面提到的共享成本問題。
以上兩種方案的核心思想是:隔離。其實包括所謂的在 SQL 引擎內部做虛擬機之類的方式本質上都是在強調隔離。但如果要實現用戶數量越多、單位成本會越低的效果,光強調隔離是不夠的,還需要共享:在共享的基礎上進行調度,以實現隔離的效果。
TiDB Serverless 允許多個租戶共享一個物理 Cluster 但是從不同用戶的視角來看是相互隔離的。這是降低單位成本的關鍵所在,因為多個用戶共享同一資源池。不過,這種基於共享的方案也存在一些問題,例如資源共享不足以滿足大租戶的需要、租戶之間的數據可能會混淆、無法對每個租戶的需求進行個性化的定製等。此外,如果一個租戶需要定製化的功能,整個系統需要被更改以適應這個租戶的需要,這會導致系統的可維護性下降。因此,需要一種更靈活且可定製化的多租戶實現方式,以滿足不同租戶的不同需求。
我們看看在 TiDB Serverless 中是如何解決這些問題的。在介紹設計方案前,我們先看看一些重要的設計目標:
對於第一點,不同租戶的數據如何隔離而且互相不可見,要回答這個問題需要從兩個方面考慮:物理存儲和元信息。
物理存儲的隔離反而是容易的。在上面提到了 TiKV 內部是會對數據進行分片的,即使數據存儲在 S3 上,我們也可以利用不同的 Key 的編碼很好地區分不同租戶的數據。TiDB 5.0 中也引入了一套名為 Placement Rules 的機制在語意層提供用戶控制數據物理分布的操作介面(目前還沒有在 TiDB 的雲產品產品線中直接暴露給用戶)( https://docs.pingcap.com/tidb/stable/placement-rules-in-sql)。TiDB 在設計之初為了性能考慮儘可能精簡存儲層對 key 的編碼,並沒有引入 namespace 的概念也沒有對用戶的數據進行額外的前綴編碼,例如:不同用戶的數據加上租戶 id,為了實現租戶的區分,這些工作現在都需要做,在存儲層這部分並不算困難,更多要考慮的是兼容性和遷移方案。
傳統的分布式資料庫中,元信息存儲是系統的一部分,例如在傳統的 TiDB 中,PD 組件存儲的元信息的主要部分是 TiKV 的 Key 和 Value 與具體的某個存儲節點的對應關係。但是在一個 DBaaS 中,元信息遠不止如此,更好的設計是將源信息存儲單獨抽離出來作為一個公共服務為多個集群服務,理由如下:
在連接建立之初就知道了租戶 id 後,所有的邏輯隔離都可以通過這個 id 實現。
Gateway 會幹比起普通 Proxy 更多的事情,事實上在 TiDB Serverless 中,我們直接將原本 TiDB 代碼中的 Session 管理模塊的代碼單獨抽出來,用於實現 Gateway。這個設計帶來的好處很多:
Gateway 作為常駐模塊,本身也是無狀態的。增加的延遲和與帶來的好處相比,我們認為是可以接受的。
我們通過 Gateway 和元信息的修改實現了多租戶在一個物理集群上的邏輯層的隔離,但是如何避免 Noisy Neiberhood 的問題?在上面提到我們引入了 RU 的概念,這裡就不得不提一下我們在 TiDB 7.0 中引入的名為 Resource Control 的新框架( https://docs.pingcap.com/tidb/dev/tidb-resource-control)。和 Dynamo 類似,TiDB 的 Resource Control 也是使用了 Token bucket 的算法( https://en.wikipedia.org/wiki/Token_bucket)。將不同類型的物理資源和 RU 對應起來,然後通過 Token bucket 進行全局的資源控制:
比起硬性的物理隔離,這樣實現資源隔離方式的好處是很明顯的:
其實還有更多好處也在 Dynamo 那篇論文里提到,這和我們實際的感受是一致的。
但是,基於共享大集群的多租戶服務實現,會帶來一個挑戰:爆炸半徑的控制。例如當某一個服務出現故障或者發現嚴重的 Bug,可能影響的范不是一個租戶,而是一片。目前我們的解法是簡單的 sharding,至少一個 region 故障不會影響到另一個 region。另外對於大租戶,我們也提供傳統的 dedicated cluster 服務,也算是在業務層對這個問題提供了解決方案。當然,這方面我們也仍然在持續的探索中,有些工作很快會有一些可以看到的效果,到時候再與大家分享。
還有很多很有趣的話題,例如上面提到整個系統都是通過 Kubernetes 串聯在一起的,那麼為這樣一個複雜的 DBaaS 提供 Devops,如何池化雲上資源和多 region 支持?另外設計思路的改變不僅影響了上面提到的內容,對於資料庫的周邊工具的設計也會有重大的影響,例如 CDC、備份恢復什麼的。因為篇幅關係就不在這裡寫了,以後有機會再說。
對話賈揚清、關濤、張伯翰:AI 平民化趨勢下,數據架構將被徹底顛覆?
一場馬斯克的反爬鬧劇:Twitter一夜回到五年前?
對話開源泰斗陸首群教授:中國開源發展應追求0到1的爆發性創新,而不是0到0的假創新
離職員工竊取原始碼,半年狂賺1.5 億;美團「1元現金」火速收購光年之外;53歲周鴻禕清華讀博:重新學習做一個工程師|Q 資訊