節約資源、提升性能,位元組跳動超大規模 Metrics 數據採集的優化之道

2023-06-30   InfoQ

原標題:節約資源、提升性能,位元組跳動超大規模 Metrics 數據採集的優化之道

編輯 | 李忠良

嘉賓 | 劉浩楊

在 ArchSummit 2023 北京站上,位元組跳動劉浩楊分享了《位元組跳動超大規模 Metrics 數據採集的實踐和探索》,他從位元組跳動可觀性平台的建設入手,討論了位元組跳動數據採集所面臨的問題和挑戰,介紹了在數據採集方面的內核優化和工程實踐,為許多在數據採集方面的企業提供了可落地的參考思路,本文為分享文章整理~

位元組跳動可觀性平台的構建思路有兩個主要方向。首先是 三位一體,「三位」指統一採集和分析指標、跟蹤和日誌數據,類似於融合分析的概念。除了這三類數據外,我們還採集事件和翻譯數據,並進行同步使用。「一體」是指在一個平台上處理整個可觀性的數據,並建設統一的引擎來處理日誌、跟蹤和指標數據。

其次是 數據融合處理。我們在不同層面上進行大量的數據採集,涵蓋了基礎設施層、容器層和中心應用業務層。例如使用 Trace ID 在業務應用和中間件層進行串聯。我們對 Log ID 和 Trace ID 進查詢之後,串聯搜索和分析。

位元組跳動可觀性平台的架構

位元組跳動可觀性平台的架構,採用水平分層的構建思路。

最底層是數據底座或數據引擎,它包括統一採集、數據傳輸和清洗過程。針對數據,我們使用兩種存儲,即指標存儲和日誌存儲。

在上層是分布式查詢引擎,再上層則是我們定義的 APM 引擎層。該引擎類似於中間件,提供可觀性所需的原子能力,如監控、報警、日誌和鏈路等。這些能力按領域劃分並封裝為原子能力。在這些原子能力之上是我們的產品層。

目前,我們在三個方向上開發了多個產品。第一個方向是雲產品觀測,因為位元組跳動有自己的雲平台,如火山引擎。我們在該平台上開發了大量雲產品的觀測能力,包括存儲網絡、RDS 產品和雲原生的 SaaS 平台等;第二個方向是開發運維,包括應用觀測和日誌分析,以及業務監控和事件等。我們還針對業務場景對鏈路進行了優化,通過鏈路數據進行性能排查和生成拓撲圖,並在此基礎上進行了架構治理工作,如強弱依賴和業務拓撲。

超大規模 Metrics 數據採集

在這樣的情況下,我們面臨著巨大的技術挑戰。

首先,我們的採集 Agent 部署數量約為百萬級別,而代理和 SDK 每秒接收的數據量達到了千億級別。在代理和 SDK 層面進行聚合後,數據被寫入後端的 TSDB 存儲,目前每秒寫入量超過 50 億,TSDB 的查詢 QPS 達到了 10 萬級別。

我們支持的業務場景涵蓋了位元組跳動的 ToC 和 ToB 業務。在位元組跳動內部,我們目前維護著數十萬個服務集群,大約接近 20 萬。微服務實例數量達到千萬級別,這意味著我們需要觀測數以千萬計的微服務容器實例,從而產生大量數據。

另外,位元組跳動使用了多種開發語言。我們的在線業務主要使用 GoLang,還有一些使用 Python,而對於對數據延遲要求較低的業務則 C++ 技術。此外,一些業務使用 Java 進行開發,還有其他一些相關的語言和 RPC 框架,大約有十幾種。在這樣的規模下,每天進行數萬次的服務發布,每次變更都會產生大量觀測數據。

如何支撐這樣的規模呢?我們內部自研了一個實時資料庫,稱為 Byte TSDB,其架構與業界常見的實時資料庫相似。前端是採集端,由 SDK 或代理將數據發送給一個全局的生產者,負責機房路由和租戶路由。後端是我們多租戶的 TSDB 架構。

與業界的其他 TSDB 相比, Byte TSDB可能存在一些顯著差異。在我們的 TSDB 中,組件比較多。首先,我們使用 MQ 進行數據寫入,其中有兩個消費鏈路。

第一個消費鏈路從 MQ 消費 Meta 數據,並將其放入熱層。在熱層中,我們默認緩存過去 28 小時的數據。這是因為我們知道在指標監測中,大部分查詢通常關注最近幾個小時的 CPU 和 QPS 等指標變化。因此,我們在熱層使用純內存方案來支持查詢。

第二個消費鏈路是從 MQ 消費數據後進行降級,並將降級後的數據寫入 MQ 的另一個 Topic。然後,一個流式消費組件負責從 MQ 讀取數據,並將其存入冷層。在冷層中,我們設計了用於存儲 Metrics 的格式,並將數據寫入 SDK 和 HDFS 上。對於冷層,我們認為只要存儲足夠,數據的存儲時間可以是無限的。

對於冷存儲,它主要面向的場景不涉及新業務。比如,如果需要查詢最近幾個小時的指標,我們可能需要對 Metrics 中的數據進行數據倉庫分析或離線分析。在冷層,我們可以支持內部的一些 BI 系統,以生成長期報表等。

資料庫最下面是控制面,它包括兩個組件,一個是配置伺服器 (Config Server),用戶的配置信息將存儲在其中;另一個是協調器 (Coordinator),負責協調數據寫入、路由信息和服務發現等組件之間的共享信息。

最後, 查詢層採用了分布式查詢模型。我們在國內部署了四個機房,為我們的 TSDB 架構提供了一個跨機房查詢的介面。這樣用戶可以通過跨機房查詢來檢索所需的數據,無論其位於哪個機房。不過,今天我們不會涉及更多與查詢相關的細節。

位元組的 TSDB 與社區提供的數據類型基本相同,包括 Counter、Meter、Histogram 和 Summary 等主要類型。我們提供了 SDK 來進行數據寫入,並為幾種主流語言提供了實現。在查詢方面,我們目前兼容 OpenTSDB 的查詢語法,並可以通過像 Grafana、Bosun 或 OpenAPI 等方式進行業務接入。

由於位元組內部使用的語言眾多,我們的 TSDB 最初支持 OpenTSDB 的數據協議。因此,在早期階段,我們的 SDK 的功能相對簡單,只是將用戶的數據序列化成 TSDB 的文本格式,然後通過一個 Metrics Agent 進行聚合,最後發送到後端。

因此,輕量級的 SDK 沒有執行太多的任務,類似於現在社區中的資料庫 SDK,它只是負責數據採集和數據包轉換。數據採集是在 Agent 上進行的,Agent 在 SDK 將數據發送過來後會進行 30 秒的聚合,然後通過 POS 方式進行上報。

基於這套架構,可以看出它的功能有一定的限制。第一個問題是是否支持靈活配置,因為現在大家關注秒級打點或者對數據有其他更多的配置需求,但在這套架構下是不支持的;另一個問題是,很多場景並不適用於 SDK 進行數據寫入,可能需要進行主動採集。在這種情況下,許多業務團隊會自行開發一個 Agent,並使用我們提供的 SDK 進行數據採集。但這種方式在位元組的整體視角下會導致維護多個 Agent 的情況,這限制了場景的適用性。

另外,我們發現在 SDK 進行序列化時會引發一些問題。舉個例子,我們提到一個極端情況,有一個使用 Flink 進行了一個離線任務,它的存儲量大約是每個任務管理器每秒處理 200 萬的數據。當它使用我們的 Java SDK 埋點後,發現 CPU 消耗非常高。經過一些分析,發現 Metrics SDK 中的打點消耗占了管理器進程的 36%。這個比例實際上非常高。在他們的機器上,僅僅進行打點,在這樣的數據量下,就占用了每個進程超過一核 CPU。

此外,我們將所有的聚合工作都放在 Agent 上會帶來一些問題。在社區中,很多方案,比如 Telegraph,允許在 Agent 上進行聚合,但在一些小規模的場景下,這是完全沒有問題的。但在我們的場景中,我們的裝機量已經達到了 100 萬台以上。

上圖可以看到在我們目前的打點量,比如從 SDK 到 Agent,每秒處理的打點數量超過 1000 億,幾乎每台機器上的 Agent 都處於負載狀態。

海量 Metric 數據採集優化實踐

為了解決引入的性能、成本和場景問題,我們採取了以下技術措施。首先,針對後端的 TSDB,我們每秒處理 50 億個數據點,每天產生的指標數據大約有 4PB。為了優化數據量和成本,我們引入了多值 Metric 的概念。在單值情況下,我們默認每個指標打 10 個點。而在多值情況下,我們只保存一個數據點,但該數據點包含了 10 個欄位,這帶來了幾個好處。

首先,這樣做可以優化存儲成本,通過標籤的復用,我們可以將多個時間序列的數據點存儲在一起,從而大幅降低存儲成本;其次,在進行多欄位查詢時,我們經常需要同時查看 P99 和 P95 等延遲指標,如果是單值情況下,需要發起兩次查詢來獲取數據。而在多值情況下,我們只需要進行一次查詢,就可以獲取到多個數據點的結果。

此外,多值還可以支持欄位之間的計算。在單值情況下,例如 Prometheus 目前也是單值的,它需要先掃描所需的欄位,然後在內存中進行二次計算。然而,在分布式查詢的場景下,涉及到大量數據時,我們會從多個工作節點上導出數據,然後在一個節點上進行計算。在多值情況下,我們可以在單個工作節點內完成查詢和計算,這對查詢性能有很大優化。

當然,我們也設計了一個私有的序列化協議,他與普通的緩衝區或文本協議不同。傳統協議存在一些問題,如壓縮率低以及在處理整個 Json 數據時,需要將所有數據打包在一起或解包後逐個讀取欄位。相比之下,我們的協議具有流式編碼和解碼的優勢。例如,在後端進行統計時,我們可以流式地編碼和解碼,以統計包中序列的數量以及 Metric 數據的大小。

在 SDK 方面,我們從整體架構的角度出發,將之前提到的每 30 秒的 Agent 聚合能力前置到了 SDK 上。首先,它減少了序列化的量。現在,我們在聚合後只需處理一個序列的點,然後進行序列化和發送;另外,SDK 還提供了基於業務進程的自定義打點精度配置。我們支持秒級監控,並且如果有更高的打點精度要求,也能很好地支持。此外,通過我們的優化措施,成功降低了 Agent 的負載。

然而,我們也意識到,當推廣新的架構和新的 SDK 到業務線上時,會面臨一些挑戰。為了減少 Agent 使用的資源,我們對原有的聚合鏈路進行了許多優化工作。這些優化包括對租戶數據的隔離,通過將不同租戶的數據存放在不同的空間中,來減少不同業務之間的打點故障。

此外,我們還進行了一些 C++ 的優化工作。例如,在解析標籤後,我們將字符串的處理方式改為使用池化方案,並通過使用單指令多數據指令集進行加速。對於壓縮算法,我們對 Day Lab 和 DSTD 進行了權衡。

最後,我們在新的硬體上進行了探索。除了常見的 CPU 和 GPU 之外,我們還引入了一種名為 DPU(數據處理單元)的新型硬體。DPU 在位元組內部的某些業務場景中得到了應用。我們與 DPU 團隊合作,針對這種硬體場景對我們的 Agent 和 C++ 代碼進行相關優化工作。

回到 SDK 層面,我們想將 SDK 內部進行聚合操作。我們採用了傳統的分層設計方法,提供了 Low level API,允許聲明 Counter 類型並進行打點操作。於此同時,我們還提供了 High levelAPI,針對 Java 和 Go 等語言,基於泛型實現了結構化的聲音打點功能。我們還考慮兼容第三方集成,如目前流行 Java Spring Boot 內置的 Mac Meta。在內核中,我們採用了基於 Pipeline 的處理流程。每個打點生成一個數據序列點,我們可以進行自定義處理,例如標籤重寫、添加或刪除相關標籤。

此外,我們還有一個聚合器和服務管理模塊,除了數據處理的 Pipeline 外,SDK 還包括租戶管理、自監控和與配置中心對接的模塊等。

下面介紹我們在 SDK 中用於序列聚合的數據結構。我們為什麼要選擇名為 KDTree 的數據結構呢?

KDTree 是一種對於多維度檢索非常友好的數據結構,也是一種平衡的二叉樹。對於每個 Metrics,都會生成 KDTree 結構。在每個結構中,可以看到根節點是該序列的 Tag value 的哈希,作為根節點。底下的子節點按照排序後的 Tag 進行分層。因此,當查詢一個序列時,不需要進行更多的比較,只需在節點上比較 Tag value 的字符串內存即可。

其次是 Signing Collector,它是一種針對數據結構的優化。當層級過多時,查詢性能會下降,且無法翻轉不平衡的情況。我們對該數據結構進行了兩種優化。首先是設置(setting),每個 Metrics 生成 1024 個 KDTree,超過 1024 後,根據哈希選擇一個樹,並將序列值乘以相應的權重。另一種優化是在發生較大傾斜時,將其退化為哈希 Map。

基於這些優化,我們設計了多段式的 Metrics 索引。舉個例子,假設我們統計一個 RPC 框架或 HTTP 框架中每個 SCP 請求的幾個值。我們可能使用 Counter 類型的 Metrics 來統計 QPS,使用 summary 數據結構來統計最新延遲,以及使用 Counter 來統計錯誤率。

在這種情況下,每個請求需要打三個點。如果我們使用類似 Prometheus 的系統,它會生成一組 Tag 對象,並對這三個 Metrics 分別進行打點。因此,在查詢過去的數據時,需要對三個 Metrics 進行三次查詢。

為了解決這個問題,我們設計了多段式索引。在 Metrics 中,有 prefix name 和 suffix name 兩個部分,前兩個部分用於定位到具體的 Metrics,而 suffix 的作用是在節點樹上掛載一組值,它們可以通過 suffix 區分。在之前提到的場景中,一個請求要打三個點,我們可以僅通過內存中的 Tag 索引進行一次查詢。查詢到該節點後,我們可以對內存中存儲的這三個點的值進行原子操作。這樣的優化對於 SDK 給用戶代碼帶來了顯著的打點延時提升。

另外,還有一個高級結構化 API 示例,在聲明 Tag 時,可以將 Tag 聲明為 Go 的結構體,並使用標籤指定 Tag 的名稱和是否有默認值。在下面的 metric set 部分,可以基於泛型的方式聲明一個 Counter 這樣可以避免一些問題,例如對於某個指標,在定義 key 時寫了一組字符串,然後每次打點時, 還需要確保這組值與 key 的順序對應。

當我們基於結構化進行打點時,我們只需要創建一個標籤對象,並將其傳遞給 API 中這樣我們就不需要在各處維護字符串了。這樣我們可以預先解析標籤的元數據,並對其進行排序。這樣,當進行打點時,我們就不需要重新對標籤進行排序了。因為在打點時,像 Prometheus 這樣的度量系統,對傳入的標籤進行排序是一個很大的開銷,然後再進行哈希查找。我們省去了排序這一步驟。

然後,在將數據放入 SDK 進行聚合之後,可能會帶來一些新問題。用戶可能會發現某些點無法查詢到,並擔心這是 SDK 引入的問題,為此,我們設計了一個名為 Series Query Debug Server 的功能。它允許我們在 SDK 收到的某個埠上輸入類似於 SQL 的 DSL 查詢語句,然後在當前查詢時對其內部存儲序列進行快照。

完成快照後,我們可以從中提取當前序列中特定度量的標籤以及其下包含的序列值,並為用戶提供自助調試。我們計劃將此功能作為標準功能開放給用戶使用。此外,基於這個快照,我們還計劃對度量序列進行轉儲,這樣可以了解當前 SDK 或用戶進程中那些 Metrics 正在打點,可以將該數據導出,並通過工具進行離線分析。

數據採集優化優化收益

在執行一個 Flink 的一個 ETL( Extract-Transform-Load)任務時,在相同的吞吐量下,通過引入新的匹配 SDK,CPU 的消耗從原來的 36% 降低到了 7% 左右。通常情況下,我們大部分都是在使用在線服務。我們選擇了一些在線服務來觀察,其中包括通過 Metrics 打點 SDK 帶來的高負載和低負載的情況。

在第一行中,原先 SDK 占比 15% 的在線服務,其 QPS 大約為幾萬。經過優化後,這個占比降低到了 9.5%,大約有 5% 左右的優化。對於一些 QPS 較低的在線服務來說,Metrics 引入的埋點消耗從 5% 降低到了 3%。

實際上,我們可以近似地認為每個在線服務大約可以節約 3% 左右。雖然在單個服務上看起來這個數字並不算大,但是考慮到像位元組 Go 有 800 多萬個微服務實例的情況,如果所有的服務都升級到新版本的 SDK 後,每個進程內的節約大約為 3%,那整體的資源節省將達到約 24 萬核。當然,這個評估可能不太準確,實際上需要在進行優化後才能進行資源的縮容。不過從整體上來看,在海量微服務場景下,這是一個非常可觀的收益。

未來展望

接下來,我們將討論 SDK 上所做的工作。首先,我們將整合採集所有數據的 Metrics track log。因為在最近兩年的可觀性領域,包括 CCF 底層社區,OpenTelemetry 是最受關注的項目之一。它在一個 SDK 框架下收集 Metrics、track log 和事件數據。我們將進行相應的整合,並考慮如何兼容相關 API 和協議。

其次,我們將支持主動上報數據和主動採集方式。我們提出了一個名為 Widget 的新項目,它負責 Metrics 數據的聚合與透傳,包括許多主動採集的能力。主動採集將包括獲取主機指標和其他指標等。此外,我們還可以通過 OpenTelemetry Magic SDK 直接通過代理進行數據上報。該代理具備強大的 Pipeline 能力,在用戶的業務集群中進行一些邊緣計算後。將數據上報到後端的實際資料庫。除了將其作為採集器使用之外,我們還將同時在後端的連接網關和流計算消費上使用該代理進行開發。

我們通過開源的方式,在現有業界優秀的 agent 中選擇一個合適的進行建設,而不是從零開始構建一個。我列舉了一些開源協議友好的選項,如 Phabit、Telegraph、Vector、Message、Collecto。最終,我們選擇了阿里他們團隊開源的 ilogtail 作為我們的 Agent。我們為 ilogtail 提供了原生的 Metrics 和 trace 協議的支持,並通過代碼生成的機制支持了插件功能。這樣,我們可以在公司內部建立自己的插件倉庫,並在構建過程中將核心項目和插件整合在一起。

我們還計劃在 ilogtail 上提供運維控制面的支持,此外,我們還計劃與阿里的同學合作,設計類似於 Flink 流計算的 pipeline 設計,包括在其上構建 SQL 引擎。

活動推薦