直播回顧 | 困擾多年的分庫分表問題終於解決了
- 2020 年 4 月 2 日
- 筆記
騰訊雲數據庫國產數據庫專題線上技術沙龍正在火熱進行中,3月17日鄭寒的分享已經結束,沒來得及參與的小夥伴不用擔心,以下就是直播的視頻和文字回顧。
關注「騰訊雲數據庫」公眾號,回復「0317鄭寒」,即可下載直播分享PPT。
話不多說,我們正式進入今天的分享。今天分享的主題是「億級流量場景下的平滑擴容:TDSQL水平拓展方案實踐」。
今天的分享我會主要包含這四部分:
第一部分首先介紹水平擴容的背景,主要介紹為什麼要水平擴容,主要跟垂直擴容進行對比,以及講一下一般我們水平擴容會碰到的問題。
第二部分會簡單介紹TDSQL如何做水平擴容,讓大家有一個直觀的印象。
第三部分會詳細介紹TDSQL水平擴容背後的設計原理,主要會跟第一部分進行對應,看一下TDSQL如何解決一般水平擴容碰到的問題。
第四部分會介紹實踐中的案例。
一、數據庫水平擴容的背景和挑戰
首先我們看水平擴容的背景。擴容的原因其實非常直觀,一般來說主要是隨着業務的訪問量,或者是需要的規模擴大,而現有的容量或者性能滿足不了業務的需求,主要表現在TPS、QPS不夠或者時延超過了業務的容忍範圍,或者是現有的容量不能滿足要求了,後者主要是指磁盤或者網絡帶寬。一般碰到這種問題,我們就要擴容。擴容來說,其實比較常見的就是兩種方式,一種是垂直擴容,一種是水平擴容。這兩種有不同的特點,優缺點其實也非常明顯。
1.1 水平擴容 VS 垂直擴容
首先我們看一下垂直擴容。垂直擴容,主要是提高機器的配置,或者提高實例的配置。因為,我們知道,大家在雲上購買一個數據庫或者購買一個實例,其實是按需分配的,就是說對用戶而言,可能當前的業務量不大,只需要兩個CPU或者是幾G的內存;而隨着業務的增長,他可能需要對這個實例進行擴容,那麼他可能當前就需要20個CPU,或者是40G的內存。
這個時候,在雲上我們是可以通過對資源的控制來動態地調整,讓它滿足業務的需求——就是說可以在同一台機器上動態增加CPU。這個擴容的極限就是——當整台機器的CPU和內存都給它,如果發現還不夠的話,就需要準備更好的機器來進行擴容。這個在MySQL裏面可以通過主備切換:通過先選好一台備機,然後進行數據同步;等數據同步完成以後,進行主備切換,這樣就能利用到現在比較好的那台機器。
大家可以看到,這整個過程當中,對業務來說基本上沒有什麼影響——進行主備切換,如果換IP的話,其實是通過前端的或者VIP的方式,對業務來說基本上沒有什麼影響。那麼它一個最大的不好的地方就是,它依賴於單機資源:你可以給它提供一個更好的機器,從而滿足一定量的要求。而隨着業務更加快速的發展,你會發現現在能提供的最好的機器,可能還是滿足不了,相當於擴不下去了。因此,垂直擴容最大的缺點就是,它依賴於單機的資源。

1.2 水平擴容
跟垂直擴容對比,另外一種方式我們叫水平擴容。水平擴容最大的優點是解決了垂直擴容的問題——理論上水平擴容可以進行無限擴容,它可以通過增加機器的方式來動態適應業務的需求。
水平擴容和垂直擴容相比,它可以解決垂直擴容的問題,但是會引入一些其他的問題。因為水平擴容比垂直擴容更加複雜,下面我們分析下可能遇見的問題,以及後面我們會介紹TDSQL的解決方案:

首先,在垂直擴容裏面,系統經過擴容以後,其實數據總體來說還是存在一個節點,一主多備架構中,備機上也存儲着所有數據。而水平擴容過程中數據會進行拆分,面臨的第一個問題是,數據如何進行拆分?因為如果拆分不好,當出現熱點數據時,可能結果就是,即使已經把數據拆分成很多份了,但是存儲熱點數據的單獨節點會成為性能瓶頸。
第二點,在整個水平擴容過程中,會涉及到數據的搬遷、路由的改變。那麼整個過程中能否做到對業務沒有感知?或者是它對業務的侵入性大概有多少?
第三,在整個擴過程中,因為剛才有這麼多步驟,如果其中一步失敗了,如何能夠進行回滾?同時,在整個擴容過程中,如何能保證切換過程中數據高一致性?
再者,在擴容以後,由於數據拆分到了各個節點,如何能保證擴容後的性能?因為理論上來說,我們是希望我隨着機器的增加,性能也能做到線性提升,這是理想的狀態。實際上在整個水平擴容的過程中,不同的架構或者不同的方式,對性能影響是比較大的。有時候會發現,可能擴容了很多,機器已經增加了,但是性能卻很難做到線性擴展。
同樣的,當數據已經拆分成多份,我們如何繼續保證數據庫分佈式的特性?在單機架構下,數據存儲一份,類似MySQL支持本地做到原子性——可以保證在一個事物中數據要麼全部成功,要麼全部失敗。在分佈式架構里,原子性則只能保證在單點裏面數據是一致性的。因此,從全局來說,由於數據現在跨節點了,那麼在跨節點過程中怎麼保證全局的一致性,怎麼保證在多個節點上數據要麼全部寫成功,要麼全部回滾?這個就會涉及到分佈式事務。
所以大家可以看到,水平擴容的優點很明顯,它解決了垂直擴容機器的限制。但是它更複雜,引入了更多的問題。接下來大家帶着這些問題,下面我會介紹TDSQL如何進行水平擴容,它又是如何解決剛才說的這些問題的。
二、TDSQL水平擴容實踐
2.1 TDSQL架構
首先我們看一下TDSQL的架構。TDSQL簡單來說包含幾部分:

第一部分是SQL引擎層:主要是作為接入端,屏蔽整個TDSQL後端的數據存儲細節。對業務來說,業務訪問的是SQL引擎層。
接下來是由多個SET組成的數據存儲層:分佈式數據庫中,數據存儲在各個節點上,每個SET我們當做一個數據單元。它可以是一主兩備或者一主多備,這個根據業務需要來部署。有些業務場景對數據安全性要求很高,可以一主三備或者一主四備都可以。這個是數據存儲。
還有一個是Scheduler模塊,主要負責整個系統集群的監控、控制。在系統進行擴容或者主備切換時,Scheduler模塊相當於是整個系統的大腦一樣的控制模塊。對業務來說其實只關注SQL引擎層,不需要關注Scheduler,不需要關注數據是怎麼跨節點,怎麼分成多少個節點等,這些對業務來說是無感知的。

2.2 TDSQL水平擴容過程
整個擴容流程大家可以看一下:一開始數據都放在一個Set上,也就是在一個節點裏面。那麼擴容其實就是會把數據擴容到——這裏面有256個Set,會擴容到256台機器上。整個擴容大家可以看到有幾個要點:
一開始雖然數據是在一個節點上,在一台機器上,但是其實數據已經進行了拆分,圖示的這個例子來說是已經拆分成了256份。
水平擴容,簡單來說就是把這些分片遷移到其他的Set上,也就是其他的節點機器上,這樣就可以增加機器來為提供系統性能。
總結起來就是說,數據一開始已經切分好了,擴容過程相當於把分片遷到新的節點,整個擴容過程中,節點數是增加的,可以從1擴到2擴到3,甚至擴到最後可以到256,但是分片數是不變的。一開始256個分片在一個節點上,擴成兩個節點的話,有可能是每128個分片在一個節點上;擴到最後,可以擴到256個節點上,數據在256台機器,每台機器負責其中的一個分片。因此整個擴容簡單來說就是搬遷分片。具體細節我們後面會講到。
在私有雲或者是公有雲上,對整個擴容TDSQL提供了一個統一的前台頁面,用戶在使用的過程中非常方便。

我們看一下這個例子。現在這個案例中有兩個Set,也就是兩個節點,每一個節點負責一部分的路由,第一個節點負責0-31,另一個名字是3,負責的路由信息是32-63。現在是兩個節點,如果要進行擴容,在前台頁面上我們會有一個「添加Set」的按紐,點一下「添加Set」,就會彈出一個對話框,裏面默認會自動選擇之前的一個配置,用戶可以自己自定義,包括現在這個Set,需要多少資源以及內存、磁盤的分配等。

此外,因為擴容要進行路由切換,我們可以手動選擇一個時間,可以自動切換,也可以由業務判斷業務的實際情況,人工操作路由的切換。這些都可以根據業務的需要進行設置。
第一步創建好以後,剛才說大腦模塊會負責分配各種資源,以及初始化,並進行數據同步的整個邏輯。最後,大家會看到,本來第一個節點——原來是兩個節點,現在已經變成三個節點了。擴容之前,第一個節點負責是0-31,現在它只負責0-15,另外一部分路由由新的節點來負責。所以整個過程,大家可以看到,通過網頁上點一下就可以快速地從兩個節點添加到三個節點——我們還可以繼續添加Set,繼續根據業務的需要進行一鍵擴容。

三、TDSQL水平擴容背後的設計原理
剛才主要是介紹TDSQL的核心架構,以及水平擴容的特性和前台操作,(幫助)大家建立直觀的印象。
第三章,我會詳細介紹一下TDSQL水平擴容背後的設計原理,主要是看一下第一章提到的水平擴容會遇到的一些問題,我們是如何來解決這些問題的。這些問題是不管在哪個系統做水平擴容,都需要解決的。
3.1 設計原理:分區鍵選擇如何兼顧兼容性與性能

首先我們剛才提到,水平擴容第一個問題是數據如何進行拆分。因為數據拆分是第一步,這個會影響到後續整個使用過程。對TDSQL來說,數據拆分的邏輯放到一個創建表的語法裏面。需要業務去指定 shardkey「等於某個字段」——業務在設計表結構時需要選擇一個字段作為分區鍵,這樣的話TDSQL會根據這個分區鍵做數據的拆分,而訪問的話會根據分區鍵做數據的聚合。我們是希望業務在設計表結構的時候能夠參與進來,指定一個字段作為shardkey。這樣一來,兼容性與性能都能做到很好的平衡。
其實我們也可以做到用戶創建表的時候不指定shardkey,由我們底層這邊隨機選擇一個鍵做數據的拆分,但這個會影響後續的使用效率,比如不能特別好地發揮分佈式數據庫的使用性能。我們認為,業務層如果在設計表結構時能有少量參與的話,可以帶來非常大的性能優勢,讓兼容性和性能得到平衡。除此之外,如果由業務來選擇shardkey——分區鍵,在業務設計表結構的時候,我們可以看到多個表,可以選擇相關的那一列作為shardkey,這樣可以保證數據拆分時,相關的數據是放在同一個節點上的,這樣可以避免很多分佈式情況下的跨節點的數據交互。
我們在創建表的時候,分區表是我們最常用的,它把數據拆分到各個節點上。此外,其實我們提供了另外兩種——總共會提供三種類型的表,背後的主要思考是為了性能,就是說通過將global表這類數據是全量在各個節點上的表——一開始大家會看到,數據全量在各個節點上,就相當於是沒有分佈式的特性,沒有水平拆分的特性,但其實這種表,我們一般會用在數據量比較小、改動比較少的一些配置表中,通過數據的冗餘來保證後續訪問,特別是在操作的時候能夠盡量避免跨節點的數據交互。其他方面,shardkey來說,我們會根據user做一個Hash,這個好處是我們的數據會比較均衡地分佈在各個節點上,來保證數據不會有熱點。
3.2設計原理:擴容中的高可用和高可靠性

剛才也提到,因為整個擴容過程的流程會比較複雜,那麼整個擴容過程能否保證高可用或者高可靠性,以及對業務的感知是怎麼樣的,TDSQL是怎麼做的呢?
- 數據同步
第一步是數據同步階段。假設我們現在有兩個Set,然後我們發現其中一個SET現在磁盤容量已經比較危險了,比如可能達到80%以上了,這個時候要對它進行擴容,我們首先會新建一個實例,通過拷貝鏡像,新建實例,新建同步關係。建立同步的過程對業務無感知,而這個過程是實時的同步。
- 數據校驗
第二階段,則是持續地追平數據,同時持續地進行數據校驗。這個過程可能會持續一段時間,對於兩個同步之間的延時差無限接近時——比如我們定一個5秒的閾值,當我們發現已經追到5秒之內時,這個時候我們會進入第三個階段——路由更新階段。
- 路由更新
路由更新階段當中,首先我們會凍結寫請求,這個時候如果業務有寫過來的話,我們會拒掉,讓業務過兩秒鐘再重試,這個時候對業務其實是有秒級的影響。但是這個時間會非常短,凍結寫請求之後,第三個實例同步的時候很快就會發現數據全部追上來,並且校驗也沒問題,這個時候我們會修改路由,同時進行相關原子操作,在底層做到存儲層分區屏蔽,這樣就能保證SQL接入層在假如路由來不及更新的時數據也不會寫錯。因為底層做了改變,分區已經屏蔽了。這樣就可以保證數據的一致性。路由一旦更新好以後,第三個SET就可以接收用戶的請求,這個時候大家可以發現,第一個SET和第三個SET因為建立了同步,所以它們兩個是擁有全量數據的。
- 刪除冗餘數據
最後一步則需要把這些冗餘數據刪掉。刪冗餘數據用的是延遲刪除,保證刪除過程中可以慢慢刪,也不會造成比較大的IO波動,影響現網的業務。整個刪除過程中,我們做了分區屏蔽,同時會在SQL引擎層會做SQL的改寫,來保證當我們在底層雖然有冗餘數據,但用戶來查的時候即使是一個全掃描,我們也能保證不會多一些數據。
可以看到整個擴容流程,數據同步,還有校驗和刪除冗餘這幾個階段,時間耗費相對來說會比較長,因為要建同步的話,如果數據量比較大,整個拷貝鏡像或者是追binlog這段時間相對比較長。但是這幾個階段對業務其實沒有任何影響,業務根本就沒感知到現在新加了一個同步關係。那麼假如在建立同步關係時發現有問題,或者新建備機時出問題了,也完全可以再換一個備機,或者是經過重試,這個對業務沒有影響。路由更新階段,理論上對業務寫請求難以避免會造成秒級的影響,但我們會將這個影響時間窗口期控制在非常短,因為本身凍結寫請求是需要保證同步已經在5秒之內這樣一個比較小的閾值,同步到這個階段以後,我們才能發起路由更新操作。同時,我們對存儲層做了分區屏蔽來保證多個模塊之間,如果有更新不同時也不會有數據錯亂的問題。這是一個我們如何保證擴容中的高可用跟高可靠性的,整個擴容對業務影響非常小的原理過程。
3.3設計原理:分佈式事務

剛才講的是擴容階段大概的流程,以及TDSQL是如何解決問題的。接下來我們再看擴容完成以後,如何解決剛才說的水平擴容以後帶來的問題。首先是分佈式事務。
- 原子性、去中心化、性能線性增長
擴容以後,數據是跨節點了,系統本來只有一個節點,現在跨節點的話,如何保證數據的原子性,這個我們基於兩階段提交,然後實現了分佈式事務。整個處理邏輯對業務來說是完全屏蔽了背後的複雜性,對業務來說使用分佈式數據庫就跟使用單機MySQL一樣。如果業務這條SQL只訪問一個節點,那用普通的事務就可以;如果發現用戶的一條SQL或者一個事務操作了多個節點,我們會用兩階段提交。到最後會通過記日誌來保證整個分佈式事務的原子性。同時我們對整個分佈式事務在實現過程中做到完全去中心化,可以通過多個SQL來做TM,性能也可實現線性增長。除此之外,我們也做了大量的各種各樣的異常驗證機制,有非常健壯的異常處理和全局的試錯機制,並且我們也通過了TPCC的標準驗證。
3.4設計原理:如何實現擴容中性能線性增長
對於水平擴容來說,數據拆分到多個節點後主要帶來兩個問題:一個是剛才說事務原子性的問題,這個通過分佈式事務來解決;還有一個就是性能。
垂直擴容中一般是通過更換更好的CPU或者類似的方法,來實現性能線性增加。水平擴容而言,因為數據拆分到多個節點上去,如何才能很好地利用起拆分下去的各個節點,進行並行計算,,真正把水平分佈式數據庫的優勢發揮出來,需要大量的操作、大量的優化措施。TDSQL做了這樣一些優化措施。

一是相關數據存在同一個節點上。建表結構的時候,我們希望業務能參與進來一部分,在設計表結構的時候指定相關的一些鍵作為shardkey,這樣我們就能保證後端的相關數據是在一個節點上的。如果對這些數據進行聯合查詢就不需要跨節點。
同樣,我們通過並行計算、流式聚合來實現性能提升——我們把SQL拆分分發到各個後台的節點,然後通過每個節點並行計算,計算好以後再通過SQL引擎來做二次聚合,然後返回給用戶。而為了減少從後端把數據拉到SQL,減少數據的一個拉取的話,我們會做一些下推的查詢——把更多的條件下推到DB上。此外我們也做了數據冗餘,通過數據冗餘保證盡量減少跨節點的數據交互。
我們簡單看一個聚合——TDSQL是如何做到水平擴容以後,對業務基本無感知,使用方式跟使用單機MySQL一樣的。對業務來說,假設有7條數據,業務不用管這個表具體數據是存在一個節點還是多個節點,只需要插7條數據。系統會根據傳過來的SQL進行語法解析,並自動把這條數據進行改寫。7條數據,系統會根據分區鍵計算,發現這4個要發到第一個節點,另外3個發到第二個節點,然後進行改寫,改寫好之後插入這些數據。對用戶來說,就是執行了這麼一條,但是跨節點了,我們這邊會用到兩階段提交,從而變成多條SQL,進而保證一旦有問題兩邊會同時回滾。


數據插錄完以後,用戶如果要做一些查詢——事實上用戶不知道數據是拆分的,對他來說就是一個完整的表,他用類似聚合函數等進行查詢。同樣,這條SQL也會進行改寫,系統會把這條SQL發到兩個節點上,同時加一些平均函數,進行相應的轉換。到了各個節點,系統會先做數據聚合,到這邊再一次做聚合。增加這個步驟的好處是,這邊過來的話,我們可以通過做一個聚合,相當於在這裡不需要緩存太多的數據,並且做到一個流式計算,避免出現一次性消耗太多內存的情況。
對於比較複雜的一些SQL,比如多表或者是更多的子查詢,大家有興趣的話可以關注我們後面的分享——SQL引擎架構和引擎查詢實戰。
以上第三章我們比較詳細地介紹了TDSQL整個水平擴容的一些原理,比如數據如何進行拆分,水平擴容實踐,以及如何解決擴容過程中的問題,同樣也介紹水平擴容以後帶來的一些問題,TDSQL是如何解決的。
四、水平擴容實踐案例
第四章,我們簡單來介紹一些實踐和案例。
4.1 實踐:如何選擇分區鍵

剛才我們說,我們希望在創建表的時候業務參與進行表結構設計的時候,能考慮一下分區鍵的選擇。如何選擇分區鍵呢?這裡根據幾種類型來簡單介紹一下。
如果是面向用戶的互聯網應用,我們可以用用戶對應的字段,比如用戶ID,來做分區鍵。這樣保證在擁有大量用戶時,可以根據用戶ID將數據拆分到各個後端節點。
遊戲類應用,業務的邏輯主體是玩家,我們可以通過玩家對應的字段;電商應用的話,可以根據買家或者賣家的一些字段來作為分區鍵。物聯網的則可以通過比如設備的ID作為分區鍵。選擇分區鍵總體來說就是要做到對於數據能比較好地做進行拆分,避免最後出現漏點。也就是說,通過這個分區鍵選擇這個字段,可以讓數據比較均衡地分散到各個節點。訪問方面,當有比較多SQL請求的時候,其實是帶有分區鍵條件的。因為只有在這種情況下,才能更好地發揮分佈式的優勢——如果是條件裏面帶分區鍵,那這條SQL可以直接錄入到某一個節點上;如果沒有帶分區鍵,就意味着需要把這條SQL發到後端所有節點上。
這個大家可以看到,如果水平擴容到更多——從一個節點擴到256個節點,那某一條SQL寫不好的話,可能需要做256個節點全部的數據的聚合,這時性能就不會很好。
總結來說,我們希望業務在創建表,在設計表結構的時候盡量參與進來。因為不管是聚合函數或者是各種事務的操作,其實對業務基本上屬於無感知,而業務這時參與則意味着能夠換來很大的性能提升。
4.2 實踐:什麼時候擴容?
我們什麼時候擴容?在TDSQL裏面,我們會有大量的監控數據,對於各個模塊我們在本地會監控整個系統的運行狀態,機器上也會有各種日誌上報信息。基於這些信息,我們可以決定什麼時候進行擴容。

簡單來說,比如磁盤——如果發現數據磁盤使用率太高,這個時候可以進行擴容;或者SQL請求,或者CPU使用率接近100%了——目前基本假如達到80%使用率就要進行擴容。還有一種情況是,可能現在這個時候其實請求量比較少,資源使用比較充足,但如果業務提前告訴你,某個時候將進行一個活動,這個活動到時候請求量會增長好幾倍,這個時候我們也可以提前完成擴容。

下面再看幾個雲上的集群案例。這個大家看到,這個集群有4個SET,每個SET負責一部分的shardkey,這個路由信息是0-127,意思是它最後能擴到128個節點,所以能擴128倍。這個「128」可以由初始化的業務預估先定下來。因為如果池子太大的話,的確最後可以擴到幾千台,但是數據將比較散了。事實上今天每台雲上的或者實際的機器性能已經非常好,不需要幾千台的規格。

這是另外一個集群——它的節點數會多一點,有8個節點,每個節點也負責一部分的路由信息。這個數字只有64,所以這個最後可以擴到64個節點。這個是雲上的相關例子。
今天我的分享主要是這些內容,大家如果有什麼問題歡迎評論留言。
五、Q&A:
Q:沒擴容之前的SET裏面的表都是分區表,問一下是不是分區表?
A:是的,在沒擴容之前,相當於在這個,簡單說我們現在就一個節點,那麼我們告訴他256,這個值我們在進行初始化的時候就定下來的。而且這個值集群初始化以後就不會再變了。假設我們這個集群定了一個值是256——因為他可能認為這個數據量後面會非常非常大,可以定256。這個時候,數據都在一個節點上。這個時候用戶,按照我們剛才的語法創建了一個表,這個表在底層其實是分成256份的。所以他即使沒有進行擴容,它的數據是256份。再創建另外一個表,也是256份。用戶可能創建兩個表,但是每個表的底層我們有256個分區的,擴容就相當於分區把它遷到其他的地方去。
Q:各個節點的備份文件做恢復時如何保證彼此之間的一致性?
A:各個節點之間沒有相互關係,各個節點自己負責一部分的路由號段,只存儲部分數據,水平擴容只負責一部分數據,它們之間的備份其實是沒有相互的關係,所以這個備份其實是之間不相關的。每個節點我們可能有一主兩備,這個其實是我們有強同步機制,在複製的時候來保證數據強一致性。大家可以參考之前的分享,裏面會比較詳細地介紹《TDSQL在單個節點裏面,TDSQL一主多備架構是如何保證數據的強一致性的 》。
Q:兩階段在協調的時候能避免單點故障嗎?
A:首先在兩階段提交的時候,我們是用SQL引擎做事務的協調,這個是單個的事務。如果其他的連接發過來,可以拿其他的SQL引擎做事務協調。而且每個SQL引擎是做到無狀態的,可以進行水平擴展。所以這個其實是不會有太多的故障,我們可以根據性能隨機擴展的,可以做到性能的線性增長,沒有中心化。日誌這些都是被打散的,記日誌也會記到TDSQL後端的數據節點裏面,一主多備,內部保證強一致性,不會有單點故障。
TDSQL是騰訊TEG數據庫工作組下三大產品系之一,是一款騰訊自研的金融級分佈式數據庫產品,目前廣泛應用於金融、政務、物聯網、智慧零售等行業,擁有大量的分佈式數據庫最佳實踐。
特惠體驗雲數據庫
