CPU 使用率誤導:高佔用率可能代表記憶體等待

當監控系統顯示資料庫伺服器 CPU 使用率為 100% 時,這並不意味著處理器正在滿負荷執行計算任務。現代 CPU 架構複雜,若因等待從 RAM 讀取資料而停滯,作業系統仍會將其標記為忙碌狀態。

記憶體存取速度遠慢於 CPU 內部操作。現代 CPU 每奈秒可處理 4 至 8 條指令,但從 DRAM 讀取一次資料需耗時 50 至 100 奈秒。這意味著每次記憶體存取期間,CPU 足以執行約 400 至 800 條指令。

為緩解此延遲,CPU 內建多層快取(L1、L2、L3)。L1 快取存取時間約 1 奈秒,比 RAM 快 50 至 100 倍,但容量極小(約 64KB)。高效能資料庫設計的核心挑戰之一,即是讓資料操作盡量命中這些高速快取。

傳統 GROUP BY 實作的效能陷阱

以 GROUP BY 聚合查詢為例,直覺的實作方式是建立一個巨大的 HashTable,每當遇到新資料列時檢查鍵值是否存在並更新計數。若數據集中存在數百萬個唯一鍵值(如 UserID),HashTable 將超出 CPU 快取容量。

在 Pivot Lake 團隊的測試中,他們使用簡化的 Rust 程式碼對 ClickBench 提供的「hits」資料集(9,900 萬列)執行 GROUP BY UserID 查詢。該資料集包含約 1,700 萬個唯一 UserID。

測試環境為 CherryServers 提供的裸機伺服器,配備 AMD EPYC 9124 Genoa 處理器(16 核心)、128GB DDR5 記憶體。為了精確測量硬體計數器,團隊將程序綁定至單一 CPU 核心執行,並使用 perf stat 工具監控效能指標。

結果顯示,該查詢首次執行耗時 3.9 秒,第二次執行(資料已在 Page Cache 中)耗時 3.6 秒。然而,perf 數據揭示僅有 16.1% 的 CPU 時間實際用於執行指令(Retiring),高達 50.1% 的時間因等待後端記憶體存取而停滯(Backend Bound Memory)。

此外,該查詢過程中從 DRAM 拉取了約 4.5GB 的資料。由於每次載入最小單位為 64 字節的 Cache Line,這意味著大量的冷記憶體存取嚴重拖慢了整體處理速度。

Radix Partitioning:透過分區縮小工作集以利用快取

為解決上述問題,團隊採用 Radix Partitioning(基數分區)技術。該方法將處理過程分為兩個階段:第一階段僅根據鍵值的哈希值高位元(Top 12 bits)將資料分散到 4,096 個獨立的記憶體區塊中;第二階段則逐一分區建立小型 HashTable 進行聚合計算。

這種做法看似增加了額外操作,因為每筆資料都需經過哈希計算與分區寫入。但其核心優勢在於縮小了每個工作階段的 HashTable 規模,使其更有可能完全容納於 CPU 快取內。

當 HashTable 較小時,CPU 在查找鍵值時能從高速快取中直接讀取資料,避免了昂貴的 DRAM 存取延遲。這是一種以計算換取記憶體頻寬與延遲優勢的策略。

優化後的效能數據對比

實施 Radix Partitioning 後,同一查詢在單一核心上的執行時間從 3.6 秒降至 2.7 秒。perf 監控數據顯示,因記憶體等待而停滯的 CPU 時間比例從 50.1% 大幅下降至 23.5%,而實際執行指令的比例則從 16.1% 提升至 32.2%。

更重要的是,從 DRAM 拉取的資料量從 4.5GB 減少到 1.5GB。這證明了透過調整演算法結構以適應硬體記憶體層級架構,能顯著降低對慢速記憶體的依賴。

值得注意的是,優化後的版本實際執行的指令數量反而增加了(從約 114 億條增至約 193 億條)。然而,由於這些指令多在快取中快速執行,且避免了長時間的記憶體等待,整體吞吐量仍獲得顯著提升。

Pivot Lake 引擎的最終表現與社群回饋

作為對比基準,Pivot Lake 團隊展示了其完整優化後的 Pivot 引擎在相同硬體環境下的表現。在單一核心模式下,該查詢執行時間為 1.848 秒,從 DRAM 拉取的資料量進一步降至約 954MB。

若利用全部 16 個 CPU 核心進行平行處理,Pivot 引擎將該查詢的執行時間壓縮至 171.773 毫秒。這顯示除了記憶體存取優化外,平行化處理也是提升資料庫效能的關鍵因素。

社群討論中,開發者指出許多軟體團隊常誤以為 CPU 滿載即代表無法再提升速度,但透過清理記憶體存取模式(Memory Access Patterns),往往能大幅提升請求處理速率。另有評論提到,現代 CPU 擁有數百 MB 的快取,遠超舊時代硬碟容量,但軟體若過度依賴抽象層而忽視底層效率,仍會導致效能低落。