在分散式系統中進行調優不是開玩笑的事情。分散式系統中調優比單節點伺服器調優複雜得多,它的瓶頸可能出現在任何地方,單個節點上的系統資源,子元件,或者節點間的協作,甚至網路頻寬這些都可能成為瓶頸。效能調優就是發現並解決這些瓶頸的實踐,直到系統達到最佳效能水平。我會在本文中分享如何對 TiDB 的“寫入”操作進行調優,使其達到最佳效能的實踐。
TiDB 是開源的混合事務處理/分析處理(HTAP)的 NewSQL 資料庫。一個 TiDB 叢集擁有幾個 TiDB 服務、幾個 TiKV 服務和一組 Placement Deiver(PD)(通常 3-5 個節點)。TiDB 服務是無狀態 SQL 層,TiKV 服務是鍵值對儲存層,PD 則是管理元件,從頂層視角負責儲存後設資料以及負載均衡。下面是一個 TiDB 叢集的架構,你可以在 TiDB 官方文件中找到每個組成部分的詳細描述。
採集監控資料
Prometheus 是一個開源的系統監測的解決方案,採集每個內部元件的監控資料,並定期發給 Prometheus 。藉助開源的時序分析平臺 Grafana ,我們可以輕易觀測到這些資料的表現。使用 Ansible 部署 TiDB 時,Prometheus 和 Grafana 是預設安裝選項。通過觀察這些資料的變化,我們可以看到每個元件是否處於執行狀態,可以定位瓶頸所在,可以調整引數來解決問題。
插入 SQL 語句的寫入流(Writeflow)
假設我們使用如下 SQL 來插入一條資料到表 t
1 |
mysql >> INSERT INTO t(id, name, address) values(1, “Jack”, “Sunnyvale”); |
上面是一個簡單而直觀的簡述,介紹了 TiDB 如何處理 SQL 語句。TiDB 伺服器收到 SQL 語句後,根據索引的編號將語句轉換為一個或多個鍵值對(KV),這些鍵值對被髮送到相關聯的 TiKV 伺服器,這些伺服器以 Raft 日誌的形式複製儲存。最後,Raf 日誌被提交,這些鍵值對會被寫入指定的儲存引擎。
在此過程中,有 3 類關鍵的過程要處理:轉換 SQL 為多個鍵值對、Region 複製和二階段提交。接下來讓我們深入探討各細節。
從 SQL 轉換為鍵值對
與其他資料庫系統不同,TiDB 只儲存鍵值對,以提供無限的水平可伸縮性以及強大的一致性。那麼要如何實現諸如資料庫、表和索引等高層概念呢?在 TiDB 中,每個表都有一個關聯的全域性唯一編號,被稱為 “table-id”。特定表中的所有資料(包括記錄和索引)的鍵都是以 8 位元組的 table-id 開頭的。每個索引都有一個名為 “index-id” 的表範圍的唯一編號。下面展示了記錄鍵和索引鍵的編碼規則。
Region(區域)的概念
在 TiDB 中,Region 表示一個連續的、左閉右開的鍵值範圍 [start_key,end_key)。每個 Region 有多個副本,並且每個副本稱為一個 peer 。每個 Region 也歸屬於單獨的 Raft 組,以確保所有 peer 之間的資料一致性。(有關如何在 TiKV 中實現 Raft 一致性演算法的更多資訊,請參閱 PingCAP 傑出工程師唐劉的相關博文。)由於我之前提到的編碼規則的原因,同一表的臨近記錄很可能位於同一 Region 中。
當叢集第一次初始化時,只存在一個 Region 。當 Region 達到特定大小(當前預設值為96MB)時, Region 將動態分割為兩個鄰近的 Region ,並自動將資料分佈到系統中以提供水平擴充套件。
二階段提交
我們的事務處理模型設計靈感來源於 Percolator,並在此基礎上進行了一些優化。簡單地說,這是一個二階段提交協議,即預寫入和提交。
每個元件中都有更多的內容,但從巨集觀層次來理解足以為效能調優設定場景。現在我們來深入研究四種調優技術。
調優技巧 #1: 排程器
所有寫入命令都被髮送到排程器模型,然後被複制。排程器模型由一個排程執行緒和幾個工作執行緒組成。為什麼需要排程器模型?在向資料庫寫入資料之前,需要檢查是否允許這些寫命令,以及這些寫命令是否滿足事務約束。所有這些檢查工作都需要從底層儲存引擎讀取資訊,它們通過排程由工作執行緒來進行處理。
如果看到所有工作執行緒的 CPU 使用量總和超過 scheduler-worker-pool-size * 80% 時,就需要通過增加排程工作執行緒的數理來提高效能。
可以通過修改配置檔案中 ‘storage’ 節的 ‘scheduler-worker-pool-size’ 來改變排程工作執行緒的數量。對於 CPU 核心數目小於 16 的機器,預設情況下配置了 4 個排程工作執行緒,其它情況下預設值是 8。參閱相關程式碼部分:scheduler-worker-pool-size = 4
調優技巧 #2:raftstore程式與apply程式
像我前邊提到的,我們在多節點之間使用Raft實現強一致性。在將一個鍵值對寫入資料庫之前,這個鍵值對首先要被複製成Raft log格式,同時還要被寫入各個節點硬碟中儲存。在Raft log被提交後,相關的鍵值對才能被寫入資料庫。
這樣就產生兩種寫入操作:一個是寫Raft log,一個是把鍵值對寫入資料庫。為了在TiKV中獨立地執行這兩種操作,我們建立一個raftstore程式,它的工作是攔截所有Raft資訊,並寫Raft log到硬碟中;同時我們建立另一個程式apply worker,它的職責是把鍵值對寫到資料庫中。在Grafana中,這兩個程式顯示在TiKV皮膚的子皮膚Thread CPU中(如下圖所示)。它們都是極其重要的寫操作負載,在Grafana中我們很容易就能發現它們相當繁忙。
為什麼需要特別關注這兩個程式?當一些TiKV伺服器的apply或者raftstore程式很繁忙,而另一些機器卻很空閒的時候,也就是說寫操作負載不均衡的時候,這些比較繁忙的伺服器就成了叢集中的瓶頸。造成這種情況的一種原因是使用了單調遞增的列,比如使用AUTOINCREMENT指定主鍵,或者在值不斷增加的列上建立索引,例如最後一次訪問的時間戳。
要優化這樣的場景並消除瓶頸,必須避免在單調增加的列上設計主鍵和索引。
在傳統單節點資料庫系統上,使用AUTOINCREMENT關鍵字可以為順序寫入帶來極大好處,但是在分散式資料庫系統中,使所有元件的負載均衡才是最重要的。
調優技巧#3:RocksDB
RocksDB 是一個高效能,有大量特性的永久性 KV 儲存。 TiKV 使用 RocksDB 作為底層儲存引擎,和其他諸多功能,比如列族、範圍刪除、字首索引、memtable 字首布隆過濾器,sst 使用者定義屬性等等。 RocksDB 提供詳細的效能調優文件。
每個 TiKV 伺服器下面都有兩個 RocksDB 例項:一個儲存資料,我們稱之為 kv-engine ,另一個儲存 Raft 日誌,我們稱之為 raft-engine 。kv-engine 有4個列族:“default” 、“lock”、 “write” 和 “raft” 。大多數記錄儲存在 “default” 列族中,所有索引都儲存在 “write” 列族中。你可以通過修改配置檔案關聯部分中的 block-cache-size 值來調整這兩個 RocksDB 例項,以實現最佳效能。相關部分是: [rocksdb.defaultcf] block-cache-size = “1GB” 和 [rocksdb.writecf] block-cache-size = “1GB”
我們調整 block-cache-size 的原因是因為 TiKV 伺服器頻繁地從“write” 列族中讀取資料以檢查插入時是否滿足事務約束,所以為 “write” 列族的塊快取設定合適的大小非常重要。當 “write” 列族的 block-cache 命中率低於 90% 時,應該增加 “write” 列族的 block-cache-size 大小。 “write”列族的 block-cache-size 的預設值為總記憶體的 15% ,而 “default” 列族的預設值為 25% 。例如,如果我們在 32GB 記憶體的機器上部署 TiKV 節點,那麼對於 “default” 列族, “write” 列族的 block-cache-size 的值約為 4.8GB 到 8GB 。在繁重的寫入工作量中, “default” 列族中的資料很少被訪問,所以當我們確定 “write” 列族的快取命中率低於 90%(例如50%)時,我們知道 “write” 列族大約是預設 4.8 GB的兩倍。為了調優以獲得更好的效能,我們可以明確地將 “write” 列族的 block-cache-size 設定為 9GB 。但是,我們還需要將 “default” 列族的 block-cache-size 大小減少到 4GB ,以避免 OOM(記憶體不足)風險。你可以在 Grafana 的 “RocksDB-kv” 皮膚中找到 RocksDB 的詳細統計資訊,以幫助進行調優。
RocksDB-kv 皮膚
調優技巧#4: 批量插入
使用批量插入可以實現更好的寫入效能。從 TiDB 伺服器的角度來看,批量插入不僅可以減少客戶端與 TiDB 伺服器之間的 RPC 延遲,還可以減少 SQL 解析時間。在 TiKV 內部,批量插入可以通過將多個記錄合併到一個 Raft 日誌條目中來減少 Raft 資訊的總數量。根據我們的經驗,建議將批量大小保持在 50〜100 行之內。當一個表中有超過 10 個索引時,應減少批量處理的大小,因為按照上述編碼規則,插入一行類似資料將建立超過 10 個鍵值對。
總結
我希望本文能夠在使用 TiDB 時幫助你瞭解一些常見的瓶頸狀況,以及如何調優這些問題,以便在“寫入”過程中實現最優效能。綜上所述:
- 不要讓一些 TiKV 節點處理大部分“寫入”工作負載,避免在單調增加的列上設計主鍵和索引。
- 當 TiKV 排程模型中的排程器的總 CPU 使用率超過 scheduler-worker-pool-size*80% 時,請增加 scheduler-worker-pool-size 的值。
- 當寫入任務頻繁讀取’write’列族並且塊快取命中率低於 90% 時,在 RocksDB 中增加 block-cache-size 的值。
- 使用批量插入來提高“寫入”操作的效能。
我們的許多客戶,從電子商務市場和遊戲,到金融科技、媒體和旅行,已經在生產中使用這些調優技術,以充分利用 TiDB 的設計、體系結構和優化。期待在不久的將來分享他們的使用案例和經驗。