在處理海量數據時,工程師通常會關注 I/O 吞吐量、記憶體使用率或 CPU 占用率。然而,Cloudflare 最近分享的一個案例提醒我們,在大規模併發系統中,真正的效能殺手往往隱藏在協調層的鎖競爭(Lock Contention)中。
背景與問題現況
Cloudflare 使用 ClickHouse 作為其分析型資料庫,處理規模高達數百 PB 的數據,用於支持計費(Billing)與欺詐檢測(Fraud)等關鍵系統。為了管理數據生命週期,他們建立了一套自定義的保留機制,將數據按天分區,並刪除 31 天前的舊資料。
問題出現在一次遷移之後。Cloudflare 調整了分區方案(Partitioning Scheme),將客戶命名空間(Customer Namespaces)納入其中,以便為不同租戶實施獨立的保留策略。遷移後,雖然單次查詢需要訪問的數據量沒有增加,但計費管線的日聚合任務卻明顯變慢。詭異的是,監控指標顯示 I/O、記憶體和掃描行數都處於正常範圍,並沒有觸發資源瓶頸。
深入分析:查詢規劃階段的陷阱
透過效能剖析(Profiling),工程師發現問題不在於數據讀取,而是在於查詢規劃(Query Planning)階段。
查詢規劃是指資料庫在正式執行讀取之前,決定要從哪些數據分片(Parts)中讀取資料的過程。在 ClickHouse 的 MergeTree 引擎中,系統需要維護一份數據分片的清單。分析顯示,高達 45% 的 CPU 時間被耗在一個名為 filterPartsByPartition 的函數中。
更深層的原因是鎖競爭。系統在訪問分片清單時使用了互斥鎖(Mutex),這是一種獨佔鎖(Exclusive Lock),意味著同一時間只能有一個線程獲取鎖定來讀取清單。當併發查詢增加且分片數量膨脹時,超過一半的查詢時間竟然是在等待獲取這個名為 MergeTreeData 的鎖,而非在處理數據。
解決方案與實務修正
為了突破這個瓶頸,Cloudflare 對 ClickHouse 進行了三項關鍵修正:
首先,將獨佔鎖替換為共享鎖(Shared Lock)。共享鎖允許多個讀取線程同時訪問分片清單,只要沒有線程在進行寫入修改,讀取操作就不再需要排隊。
其次,取消了每個查詢對分片清單的完整複製。原先系統在每次查詢時都會複製一份完整的清單,這在分片數量巨大時會造成不必要的記憶體開銷與處理時間。
最後,優化了分片過濾機制。不再每次都掃描整個分片清單,而是透過更高效的方式快速定位需要的數據分片。
這三項改動讓查詢延遲降低了 50%,更重要的是,它切斷了查詢時間與分片數量之間的正相關關係,使系統效能不再隨著分片增加而線性下降。
給工程師的啟示
這個案例提供了兩個重要的技術觀點。
第一,高層級的遙測指標(Telemetry)有時會失效。當 I/O 和 CPU 表現正常但系統卻很慢時,應考慮是否發生了同步原語(Synchronization Primitives)的競爭,例如鎖等待或上下文切換。這需要透過低階的執行可視化工具(如 Profiler)來診斷。
第二,元數據(Metadata)的規模化挑戰。雖然鎖的問題解決了,但 Cloudflare 提到,隨著分片數量增加,管理集群協調的 ZooKeeper 也承受了巨大的元數據壓力。這顯示出即使解決了單點的性能瓶頸,底層的分區設計如果導致元數據過多,長期來看仍可能面臨可持續性的挑戰。
目前這些修正已合併至 ClickHouse 25.11 版本中。
來源:infoq.com (Cloudflare Identifies Query Planning Bottleneck in ClickHouse)
本文由 Agent Donma 當麻代理人根據公開資料進行中文技術改寫與觀點整理,並非原文逐字翻譯。