作為雲原生技術先驅,騰訊雲資料庫核心團隊致力於不斷提升產品的可用性、可靠性、效能和可延伸性,為使用者提供更加極致的體驗。為幫助使用者瞭解極致體驗背後的關鍵技術點,本期帶來騰訊雲資料庫專家工程師王魯俊給大家分享的騰訊雲原生資料庫TDSQL-C的架構探索和實踐,內容主要分為四個部分:
本次分享主要分為四個部分:
第一部分,介紹騰訊雲原生資料庫 TDSQL-C 產品架構,包括產品的研發背景和架構主要特性;
第二部分,分享使用者場景實踐,針對線上真實的使用者場景做一些分析和針對性實踐;
第三部分,分享系統關鍵優化;
第四部分,分享產品未來演進。
背景
騰訊雲原生資料庫最初採用的是傳統架構,也就是 MySQL 範例,或者說是採用 Binlog 複製的主備方式的架構。但這種架構在現有的一些使用者需求來看是有很多問題的。
比如儲存容量。傳統架構範例的儲存上限受限於本地磁碟上限,一般是幾百 G,或者幾個 T 的量級,做擴充套件的成本非常高而且麻煩。當用戶資料非常多時,會做分庫分表,使用現有的分庫分表中介軟體或解決方案會帶來一些分散式事務的問題。
做業務的同學知道,分散式事務處理起來會比較麻煩,涉及如何應對故障,如何應對分散式事務產生的資料不一致等問題。使用者在儲存容量方面的需求是範例容量大於 100T,並且儲存容量能夠快速透明的擴充套件。
其次是可靠性。傳統架構基於 BinLog 複製,普通的非同步或者半同步的複製方式可能會丟資料,同步的複製方式效能損失會比較大。使用者在可靠性方面的需求是第一不能丟資料,即 RPO 等於 0;第二資料是有多副本容災的,也就是要達到一定程度的資料可靠性。
此外還有可用性,可用性對於使用者來講就是服務有多長時間不可用,比如在傳統架構發生一次 HA,或者宕機重啟,這段時間服務都是不可用的。HA、恢復時間慢對使用者來講很難接受,傳統架構 HA 或者副本的恢復速度可能達到了分鐘級。第二個問題是基於 BinLog 複製的時候,主備副本的延遲比較高,有些可能達到分鐘級,甚至達到小時級。使用者希望能夠快速切換 HA,實現秒級恢復,還包括回檔功能,如果有副本,希望副本的延遲能夠比較小,最好是秒級以下,甚至是毫秒級。
最後是可延伸性。傳統架構的擴充套件性是非常複雜的,基於 Binlog 建立唯讀副本也會非常複雜,要先把原始的資料給複製過來,然後搭建主備同步,唯讀副本才能開始工作,這個過程至少是分鐘級甚至是小時級別的。對使用者來講,他們希望當讀需求有擴充套件性需求的時候,可以實現秒級的讀副本擴充套件。
我們針對這四個方面的使用者需求,採用了儲存計算分離架構,這也是 TDSQL-C 所採納的核心的架構想法。
簡單來說,要解決儲存容量和可靠性方面的問題,第一我們會用雲端儲存,雲端儲存之間是可以水平擴充套件的,理論上它的容量是無限的,而且對於每一份資料都有多副本來保證可靠性。資料分散在雲端儲存的各個節點上,在這個基礎上可以做持續備份,並行回檔等功能。
在可用性方面,資料放在雲端儲存上之後,資料的分片是可以做並行恢復的,回檔也可以做並行回檔。物理複製的時延一般會比基於 Binlog 的邏輯複製更低一點。
最後在可延伸性方面,共用儲存的優勢更加明顯,當新建一個唯讀副本的時候,資料不需要複製一份出來,因為資料是在雲端儲存上作為共用資料存在的,只需要把資料共用,另外再構建增量的資料複製就可以了。
架構特性
上面這張圖是 TDSQL-C 的整體架構,從這個架構中我們可以看到,它整體上分為上一層的計算層和下一層的儲存層。
計算層有一個讀寫節點,可以提供讀寫請求,還有多個唯讀節點,可以提供讀請求,也就是圖裡邊的 Master 節點和 Slave 節點。當讀寫請求,尤其是寫請求進來以後,Master 節點也就是讀寫節點產生資料的修改,然後它會把修改產生的 InnoDB 的 Redo Log 下傳到整個儲存層,同時把 Redo Log 分發到自己的 RO 節點。
儲存層負責管理資料,當產生的 Redo 紀錄檔傳送到儲存層之後,它可以負責 Redo 紀錄檔的回放,Segment 把它儲存的頁面對應的 redo 紀錄檔 apply 到自己的頁面上來。整個儲存層是架設在 COS 儲存服務上。
TDSQL-C 的儲存可以自動擴容,最大支援超過 1PB 的容量,目前我們的產品最大支援到 96CPU 和 768GiB 的規格。效能方面,唯讀大概能跑到一百萬以上 QPS,寫效能也能超過 40 萬 QPS。
基於這種共用儲存的架構,我們可以做到秒級故障切換,包括秒級的快照備份和回檔,並且因為儲存層本身可以做彈性,計算層也可以做彈性,所以可以實現一定程度的 Serverless。
此外,當需要擴充套件唯讀的時候,可以很容易的增加唯讀節點,TDSQL-C 現在最多可以掛 15 個唯讀節點,並且在讀寫節點和唯讀節點之間只有毫秒級的延遲。因為整個工程是基於 MySQL 程式碼庫演化過來的,所以是百分之百相容 MySQL 的。
接下來,我會介紹並分析幾個比較典型的場景實踐。
Serverless
上圖描述的是一些業務預測未來一段時間的資料儲存或者資料計算的需求是持續上漲的,但實際上可能真實的使用者需求是圖中灰色的曲線。為了做好服務,使用者要提前買好服務庫範例,比如圖中紅色的折線,一開始就要準備好這種規格的資料庫範例。
這種情況有一個壞處,實際買的規格都是比真實需求偏大的,這就會造成儲存資源或計算資源的浪費。如果某些時刻有突發的流量進來,突然對儲存或者計算的資源要求非常高,就會出現機器範例資源跟不上、規格太小等情況,這會對業務造成很大的影響。
我們認為理想的情況應該是圖中藍色的曲線,這個曲線的整個資源容量跟真實業務需求的變化規律是一樣的,並且總是比真實的需求稍微多一點。這樣就能真正把資源充分利用起來,而且最大程度上降低成本開銷。
在一些真實的例子裡,我們發現有些業務是開發測試場景,業務真正上線之前,會做一些系統的測試開發,這個過程對資料庫的需求頻率是非常低的。還有一些像 IoT、邊緣計算、SaaS 平臺,他們的負載變化有非常大的規律性,白天壓力比較大,但是晚上壓力比較小。
TDSQL-C 做到了智慧極致的彈性,能夠根據負載來快速啟停範例。第二是按需計費,用了多少花多少,不用不付費,可以做到按秒的計量,按小時的結算。
彈性擴容
另一個我們線上上業務中發現的實踐需求是彈性擴容。有些業務每天都能產生大量的資料,比如有的業務一天產生幾百 G 的資料,並且這種業務對單庫的容量要求很高,通常都是幾十 T,甚至上百 T 的資料。
有些場景,像開發測試場景,開發完或者某一次測試完,資料庫直接就刪掉了,生命週期非常短。另外有些歷史庫場景,歷史資料儲存只是儲存最近一段時間的,特別老的資料直接就刪除了,刪除了這些資料希望空間立刻回收,不要再產生儲存成本了。
TDSQL-C 可以做到按需擴容,儲存根據操作頁面按需擴容,如果產生的資料比較多就擴充套件出來,不需要預先規劃好要做多少儲存。第二點是自動回收,有一些空閒空間,比如資料已經刪除了,這些資料實際不需要了,但是傳統的 RDS 是邏輯刪除的,這塊可能還會繼續產生費用,TDSQL-C 可以做到按實際的容量來計費。
備份回檔
很多場景對備份回檔要求比較高,比如金融行業,因為金融行業對資料安全關注度非常高,他們對備份的速度和備份的時效性都有很高的要求。還有像遊戲業務,可能會涉及到頻繁的回檔,所以對備份回檔的速度要求也比較高。
回檔作為「後悔藥」,對很多業務來講都是很重要的一個功能,使用者可能會產生一些誤操作。
TDSQL-C 可以做到持續備份,儲存分片可以根據備份點進行並行的獨立備份,同時可以做到設定全域性的一致性備份點來進行備份。此外,TDSQL-C 也可以做到並行回檔,每一個分片並行回檔各自的資料的全量和增量的備份,並行回放自己的紀錄檔。還有 PITR,也就是可以快速的恢復到資料庫的任意時間點的資料的狀態。
極速啟停
第一個優化就是前面提到的,如何做到極速啟停,也就是支援更好的 Serverless。
這邊有個測試資料,第一個測試資料叫停機時間,指的是計劃內的停機時間,即主動停機。TDSQL-C 跟傳統的 RDS 資料庫對比,傳統 RDS 資料庫停機需要 26 秒,TDSQL-C 可以做到 3 秒內停機。
第二個是啟動時間,就是停機之後重新把資料庫範例拉起來,大概需要多少時間能夠恢復起來,RDS 需要 48 秒,TDSQL-C 可以做到 4 秒就啟動起來。
我們分析了一下,這 48 秒有 21 秒是在做事務恢復,也就是第三個柱狀圖,我們對事務系統的並行初始化、表鎖恢復做了一些優化,可以把恢復時間降到一秒。
第四個指標叫效能恢復時間,這個指的是比如重啟之前資料庫的 QPS 大概跑到了 20 萬 QPS,重啟之後大概需要多長時間才能重新恢復到 20 萬 QPS。這對很多業務來說都是很重要的,是恢復質量的問題。傳統 RDS 在有些場景下需要 260 秒才能恢復,但是用 TDSQL-C 3 秒就能恢復了。
這裡我們做了一些優化,用的**獨立 BP **的優化方式。Buffer Pool 跟資料庫範例程序是解耦的,由這臺機器上的另外一個程序來負責管理這塊記憶體,當資料庫範例重啟之後,Buffer Pool 是可以繼續用的,這種方式就避免了重啟之後,整個 Buffer Pool 都是冷的,需要很長時間慢慢預熱,省了這個過程,所以恢復時間會非常快。
二級快取
另一個系統關鍵優化是二級快取。二級快取是 TDSQL-C 在儲存計算分離架構下做的比較創新的優化,也是對架構的一個重要補充。
如此前所說,儲存層是可以水平擴充套件的,這意味著資料量膨脹了很多倍,可能幾十倍、上百倍。資料量多了,但是計算層計算節點的 Buffer cache,也就是 InnoDB 的 Buffer Pool 的容量並沒有太大的變化,這就意味著需要用以前相同大小的 Buffer Pool 來服務更多的資料。
大家知道 InnoDB 的 Buffer Pool 在一定程度上承擔了讀快取的作用,服務更多的資料,意味著讀快取的效率可能會下降。以前一些並不是 IO Bound 的場景,在這種資料量大了的場景下就變成 IO Bound 了,或者以前本來就是 IO Bound 的場景,IO Bound 更嚴重了,這樣對效能影響還是比較大的。
其次,傳統 RDS 的 BufferPool 和本地磁碟空間的儲存中間,是沒有其他硬體儲存裝置的。但是在儲存計算分離架構下,Buffer Pool 可能跑得非常快,它的 IO 延遲很低,但資料是存放在遠端的機器上的,我們這邊叫 Remote IO。需要通過網路存取其他機器的 SSD,在這之間有至少兩個層次的硬體儲存裝置,一個是 SSD,就是本機硬碟,另外一塊是 Persistent Memory,就是持久化記憶體,這些是我們可以利用起來的。我們把這一類儲存用作 secondary cache,通過這種方式能夠有效的減緩 IO Bound 場景下 Buffer Pool 命中率低的問題,因為我們可以快取很多的熱資料,能夠加速資料的存取。
通過測試可以看到,隨著資料量的增大,整個效能提升還是比較明顯的,在很多場景下效能可以提升到百分之一百以上,達到一倍多。
當然這個問題也有其他的解決方案,有些產品用的是水平擴充套件 DRAM,類似於我們的 Buffer Pool,就是把 Buffer Pool 放在遠端的機器上,通過更好的 RDMA 來存取這部分記憶體,而且這個記憶體可能分散在多臺機器上,這種方式也能減緩 IO Bound 場景的一些開銷。
但相對來講,我個人認為使用 Secondary cache 這種方式系統的整體應用成本更低一點,因為畢竟記憶體的成本比 SSD 的成本高的多,尤其是非易失性記憶體,像 3D Xpoint,慢慢流行起來之後,價格是慢慢降低的。用二級快取的方式,整體的範例成本能夠下降非常多。
極致伸縮
還有一個優化是極致伸縮,我們把儲存功能下放到儲存層之後,儲存層會有儲存池這樣一個概念。
有一些邏輯跟傳統 RDS 方式是類似的,比如段管理還是以 1M 的 extent 為粒度來管理。也有些邏輯有很大的差異,比如我們把儲存空間的擴充套件,整個 offload 到儲存層,整個空間都池化。當我們發現某個 extent 裡面的所有頁面都回收了之後,變成了一個 Free Extent,就可以物理上真正的把它刪除,而不只是標記為刪除。通過這種方式真正刪除之後,客戶的儲存成本就降低下來了,也就是能夠實現按需計費的能力。
極速備份回檔
極速備份回檔的實現包含兩部分:備份、回檔。
此前提到,我們的資料是分散到分散式儲存元件上的,分散式儲存包含很多的儲存節點,而且它本身還有一定的計算能力。當範例需要做備份的時候,每個儲存節點都可以獨自的去做備份,我們叫自治備份,它可以持續的做備份。當我們需要全域性一致的備份位點時,可以由計算節點來負責協調,通過一些特殊的命令,或者紀錄檔來通知所有儲存層的節點基於快照做一個統一的備份。
跟備份相反的一個操作是回檔,基於備份再把範例的資料恢復到某個時間點,回檔也是並行回檔的,每個計算節點都可以獨立的做自己的回放。
針對極速備份回檔的測試資料看,1TB 的備份時間,RDS 範例需要 61 分鐘,TDSQL-C 只需要 21 分鐘就夠了。1TB 的回檔恢復時間,RDS 需要 168 分鐘之多,但 TDSQL-C 22 分鐘就可以恢復出來。
Instant DDL
還有一些優化是功能性優化,包含 Instant DDL 和並行構建索引。
Instant DDL 是 MySQL 8.0 新增的一個功能,它指的是在處理新增列,或修改列型別,或刪除列 DDL 的時候,可以僅僅修改原資料就直接返回。
大概的原理是,比如這張表原來有三列,現在需要新增一列,變成四列,只需要在系統表裡面標記一下,這張表就從原來的三列變成了四列。之後再新寫入的資料都是按四列寫入的,原來的資料在磁碟上存的是三列的,新插入的資料會打上新格式資料的標記,原來的資料是沒有標記的,當用戶讀取的時候,返回客戶之前根據標記來決定。如果是舊資料,我們就給它補一個新的列,一般補預設值 default value;如果是新列就直接返回,通過這種方式就做到了 O(1) 的 DDL,時間非常短。
並行構建索引
接下來介紹下並行構建索引,比如 create index,或者 optimize table 的時候,都會涉及到一些表的重建。
RDS 構建索引的時候,尤其是 8.0 的相對早一點的版本,都是單執行緒構建的。構建過程是先掃描所有的主表資料,掃描之後,根據掃描到的每一行主表資料,再根據索引資訊,生成對應的索引行,這些索引行生成後儲存到臨時檔案裡面。
第二步是對這個臨時檔案按照索引行的索引鍵進行排序,一般是 mergesort,完成之後把它匯入到一個空的 Btree 裡,這樣就完成了整個索引的構建。我們針對這塊做了並行化優化,掃描主表、外排,還有把資料匯入到 Btree,這三個過程都是可以並行化的。
在第一個階段,我們基於 InnoDB 8.0 的 parallel DDL 做了並行掃描。第二步的 mergesort 我們也做了基於取樣的並行化,通過這種方式來提升並行度。第三步構建 Btree 的時候,也是可以並行化的,比如產生了八萬行的索引行,如果八並行,每一個並行執行緒負責一萬行資料的構建。最後再把形成的八個子 Btree 合併成一個大的 Btree,再去壓縮層高等等。我們測下來很多場景下能提升到兩倍以上,還有很多場景可以提高的更多。
未來演進
第一個我們在探索的演進叫 Global Database。
如上圖所示,左邊有一個 Primary 範例,這個範例讀寫節點產生了 Redo log,Redo log 需要分發到儲存層,我們現在新增了 Log Store 模組,它負責接收和分發紀錄檔,通過這種方式,Log Store 一定程度上可以提升紀錄檔的響應速度和整體 Redo log 的 IO 吞吐,進一步提升寫效能。
另外一個很重要的點是 Primary 範例跟右側 Standby 範例可以通過各自的 Log Store 來建立資料複製的鏈路。通過這種方式,相當於擴充套件出了一個唯讀節點,實現讀擴充套件,而且是跨 Region 的讀,因為 Standby 可以部署在另外一個 Region 上。另外我們通過這種方式實現了跨 Region 容災,這對於很多金融業務來講都是剛需。
另一個我們在探索的演進是計算下推。
根據我們的架構,儲存和計算是分開的,計算在上面,儲存在下面,儲存不只有儲存能力,還擁有一定的計算能力,像剛才提到的備份恢復,每個節點可以獨立持續的做備份,就是利用儲存層計算能力的一個例子。
除此之外還有很多業務邏輯也可以通過這種方式把儲存層的計算資源利用起來,第一個是頁面內計算下推。比如現在要做一個有條件的掃描,掃描到了某個頁面,這個頁面可能有一百行資料,滿足條件的有五條,可以把條件下推到儲存層,直接放在儲存層做過濾,只把這五條資料返回給計算層就可以了,這就避免了把這一百行全部讀到計算層,在計算層再做計算,減少了中間網路頻寬的消耗。
第二個是 Undo 頁面間的計算下推,我們 InnoDB 是支援 MVCC 即多版本的,舉個例子,我們現在啟動一個唯讀,這個讀事務快照相對比較老,比如讀一小時之前的,當我現在去讀的時候,發現某一行資料太新了,不是我想要的那個資料,需要找到以前的版本。
這個過程在 InnoDB 裡面,需要找到這一行對應的 Undo 頁面,把它的前映象找出來,要讀的是 Undo 頁面記錄的前映象,這個過程如果放在計算節點做,需要把原始的頁面資料載入到計算節點,然後根據讀快照把 Undo 頁面找出來,再把 Undo 頁面應用到資料頁面上,產生一箇舊版本的資料。這整個過程都可以在儲存層來做,我們現在也是把這個下沉下來的。
還有一個下推叫寫下推。比如我就改某個頁面 Header 部分的前幾個位元組,或者 Page Header 的某個欄位,這種情況很多是盲寫的,不需要讀出來,直接可以更新,這種情況也是可以下推的。
計算下推在儲存計算分離的架構下是很自然的一個事情,剛才講到了儲存計算分離,它的儲存層帶有一定的計算能力,大量的計算實際上都是可以下沉到儲存層的,哪些計算可以下推並沒有很明顯的邊界。我個人覺得除了事務之外,大部分計算型的都可以下推到儲存層,甚至可以把儲存層的一些計算資源當成純粹的計算資源,不關心它是不是存資料。