TiDB 6.5 新特性解析丨過去一年,我們是如何讓 TiFlash 高效又穩定地榨乾 CPU?

PingCAP發表於2023-02-01

TiDB 6.5 LTS 版本已經發布了。這是 TiDB V6 的第二個長期支援版,攜帶了諸多備受期待的新特性:產品易用性進一步提升、核心不斷打磨,更加成熟、多樣化的災備能力、加強應用開發者生態構建……

TiDB 6.5 新特性解析系列文章由 PingCAP 產研團隊重磅打造,從原理分析、技術實現、和產品體驗幾個層面展示了 6.5 版本的多項功能最佳化,旨在幫助讀者更簡單、更全面的體驗 6.5 版本

本文為系列文章的第一篇,介紹了 TiFlash 在高併發場景下的穩定性和資源利用率的最佳化原理。

緣起

最近的某天,我們測試 TiFlash 在高併發查詢場景下的穩定性時,發現 TiFlash 終於可以長時間穩定將 CPU 完全打滿,這意味著我們能充分的利用 CPU 資源。回想一年多前,我們還在為高併發查詢下的 OOM(out-of memory)、OOT(out-of thread)、CPU 使用率不高等各種問題而絞盡腦汁。是時候來回顧一下我們做了哪些事情,讓量變引起質變。

1.png
<center>? CPU 使用率打滿</center>

我們都知道,對於分析型的查詢來說,有時候一個請求就能將機器的 CPU 資源打滿。所以,大部分 OLAP 系統在設計和實現的時候,很少考慮系統在高查詢併發下的表現——早期的 TiFlash 也沒在這方面考慮太多。

早期的 TiFlash 的資源管理比較初級——沒有高效的執行緒管理機制、缺少健壯的查詢任務排程器、每個查詢的記憶體使用沒有任何限制、最新寫入的資料儲存和管理也有較大最佳化空間。這些最佳化措施的缺位,導致早期的 TiFlash 在高併發查詢場景下表現不佳,經常無法將 CPU 使用率打滿,稍有不慎還可能出現 OOM 或 OOT。

過去一年裡,針對 TiFlash 在高併發場景下的穩定性和資源利用率這個問題,我們在儲存和計算上都做了不少嘗試和努力。如今回頭看,有了上面的結果,也算是達到了一個小里程碑。

DynamicThreadPool

TiFlash 最開始的執行緒管理非常簡單粗暴:請求到來時,按需建立新執行緒;請求結束之後,自動銷燬執行緒。在這種模式下,我們發現:對於一些邏輯比較複雜,但是資料量不大的查詢,無論怎麼增加查詢的併發,TiFlash 的整機 CPU 使用率都遠遠不能打滿。

2.png
<center>? CPU 使用率始終保持在 75% 以下</center>

經過一系列的研究之後,我們終於定位到問題的根本原因:高併發下,TiFlash 會頻繁地建立執行緒和釋放執行緒。在 Linux 核心中,執行緒在建立和釋放的時候,都會搶同一把全域性互斥鎖,從而在高併發執行緒建立和釋放時, 這些執行緒會發生排隊、阻塞的現象,進而導致應用的計算工作也被阻塞,而且併發越多,這個問題越嚴重,所以 CPU 使用率不會隨著併發增加而增加。具體分析可以參考文章:深入解析 TiFlash丨多併發下執行緒建立、釋放的阻塞問題

解決這個問題的直接思路是使用執行緒池,減少執行緒建立和釋放的頻率。但是,我們目前的查詢任務使用執行緒的模式是非搶佔的,對於固定大小的執行緒池,由於系統中沒有全域性的排程器,會有死鎖的風險。為此,我們引入了 DynamicThreadPool 這一特性。

在 DynamicThreadPool 中,執行緒分為兩類:

  1. 固定執行緒:固定數量的執行緒,生命期與整個執行緒池相同。
  2. 動態執行緒:執行過程中隨著負載升高而建立,會自行在冷卻後銷燬。

每當有新任務需要執行時,DynamicThreadPool 會按以下順序查詢可用執行緒:

  1. 空閒的固定執行緒。
  2. 空閒的動態執行緒。
  3. 當沒有可用執行緒時,建立新的動態執行緒服務當前任務。

所有空閒的動態執行緒會組成一個 LIFO 的連結串列,每個動態執行緒在處理完一個任務後都會將自身插入到連結串列頭部,這樣下次排程時會被優先使用,從而達到儘可能複用最近使用過的動態執行緒的目的。連結串列尾部的動態執行緒會在超過一個時間閾值沒有收到新任務之後判斷自身已冷卻,自行退出。

MinTSOScheduler

由於 DynamicThreadPool 沒有限制執行緒的數量,在遇到高併發查詢時,TiFlash 仍然有可能會遇到無法分配出執行緒(OOT)的問題。為了解決此問題,我們必須控制 TiFlash 中同時使用的執行緒數量。

為了控制同時使用的計算執行緒數量,同時避免死鎖,我們為 TiFlash 引入了名為 MinTSOScheduler 的查詢任務排程器——一個完全分散式的排程器,它僅依賴 TiFlash 節點自身的資訊。

3.png
<center>? MinTSOScheduler 的基本原理</center>

MinTSOScheduler 的基本原理是:保證 TiFlash 節點上最小的 start_ts 對應的所有 MPPTask 能正常執行。因為全域性最小的 start_ts 在各個節點上必然也是最小的 start_ts,所以 MinTSOScheduer 能夠保證全域性至少有一個查詢能順利執行從而保證整個系統不會有死鎖。而對於非最小 start_ts 的 MPPTask,則根據當前系統的執行緒使用情況來決定是否可以執行,所以也能達到控制系統執行緒使用量的目的。

MemoryTracker

DynamicThreadPool 和 MinTSOScheduler 基本上解決了執行緒頻繁建立和銷燬、執行緒使用數量不受控制兩大問題。對於一個執行高併發查詢的環境,還有一個重要的問題要解決——減少查詢之間的相互干擾。

實踐中,我們發現最重要的一點就是要避免其中某一個查詢忽然消耗掉大量記憶體,導致整個節點 OOM。為了避免某個大查詢導致的 OOM,我們顯著增強了 MemoryTracker 跟蹤和記錄每個 MPPTask 使用的記憶體的精確度。當記憶體使用超過限制時,可以強行中止請求,避免 OOM 影響其它請求。

PageStorage

PageStorage 是 TiFlash 中的一個儲存的抽象層,類似物件儲存。它主要是為了儲存一些較小的資料塊,如最新資料和 TiFlash 儲存引擎的後設資料。所以,PageStorage 主要面向新寫入資料的高頻讀寫設計。v6.1 及之前 TiFlash 使用的是 PageStorage 的 v2 版本(簡稱 PSv2)。

經過一系列的迭代和業務打磨,我們發現 PSv2 存在一些問題亟需改進:

  1. 在一些寫入負載,特別是 append-only 負載下,容易觸發激進的 GC 策略對硬碟資料進行重寫。重寫資料時引起較大的寫放大,以及記憶體的週期性快速上漲,造成系統不穩定。同時也會擠佔前臺寫入和查詢執行緒 CPU。
  2. 在 snapshot 釋放時進行記憶體中的垃圾回收,其中涉及較多記憶體小物件的複製。在高併發寫入和查詢的場景下,snapshot 釋放的過程與讀寫任務擠佔 CPU。

這些問題在大部分寫入和查詢併發較低的 OLAP 場景下,對系統的影響有限。但是,TiFlash 作為 TiDB 的 HTAP 架構中重要的一環,經常需要面對高併發的寫入和查詢。為此,我們重新設計並實現了全新的 PageStorage (簡稱 PSv3)以應對更嚴苛的 HTAP 負載需求。

4.png
<center>? PSv3 架構圖</center>

上圖是 PSv3 的整體架構。其中,橙色塊代表介面,綠色塊代表在硬碟上儲存的元件,藍色塊代表在記憶體中的元件。

  • WALStore 中維護資料(page)在 BlobFile 中位置,記憶體中的 PageDirectory 實現了 MVCC 支援。
  • 資料儲存在 BlobFile 中,如果其中的資料反覆重寫,會造成 CPU 以及 IO 資源的浪費。我們透過 SpaceMap 記錄了 BlobFile 上的資料塊使用情況(空閒或佔用)。刪除資料時,只需要更新一下標記。寫入資料時,可以直接從 SpaceMap 查詢符合要求的空閒塊直接寫入。大部分情況下,BlobFile 可以直接複用被刪除的空閒資料塊,避免資料重寫的發生,最大程度地減少了垃圾回收的需求,從而顯著減少 CPU 和記憶體空間使用。
  • 由於有 SpaceMap 的存在,寫執行緒在 SpaceMap 中分配好資料塊位置之後,多個寫執行緒的 IO 操作可以併發執行。在複用空間時 BlobFile 檔案大小不變,可以減少了檔案後設資料的 sync 操作。TiFlash 整體的寫延遲降低,進而減少等待資料一致性的 wait index 阻塞時間,提升查詢執行緒的 CPU 利用率。
  • 讓讀寫執行緒 snapshot 建立和釋放時的操作更高效,記憶體物件的整理的時間從釋放 snapshot 時改為在後臺執行緒進行回收,減少了對前臺讀寫任務的影響,從而提升了查詢執行緒的 CPU 利用率。

總結

DynamicThreadPoolMinTSOSchedulerPageStorageV3CPU 最大使用率
enableenableenable100%
disabledisableenbale75%
enabledisableenable90%
enableenabledisable75%
disableenableenable85%

上面這個表格總結了本文介紹的這幾個提升 TiFlash 穩定性和 CPU 使用率的關鍵特性的組合情況,可以看出:

  • DynamicThreadPool 解決了頻繁建立和銷燬執行緒帶來的開銷;PageStorage v3 大大降低了 GC 和 snapshot 的開銷,提升了高併發寫入和查詢的穩定性。這兩者對提升 CPU 利用率有明顯的效果。
  • MinTSOScheduler 排程器限制了查詢使用執行緒的數量,避免了出現分配不出執行緒的情況,可以有效防止高併發請求導致的 OOM、OOT。
  • 而 MemoryTracker(記憶體限制)透過主動 cancel 掉部分請求來防止整個程式 OOM,可以有效避免一個大查詢導致整個節點不可用(OOM)的情況發生。

除此之外,過去一年,TiFlash 在效能和功能方面也做了不少最佳化,感興趣的朋友可以關注我們的 github 程式碼和官方文件。以上全部改動可以在 TiDB v6.5 LTS 版本中體驗到,歡迎嘗試。

相關文章