持續降本:B 站日誌平臺 3-0 演進之路

本期作者

季俊宇

嗶哩嗶哩高級開發工程師

李銳

嗶哩嗶哩資深開發工程師

背景

基於 ClickHouse 的 Billions2.0 日誌方案上線後(B 站基於 Clickhouse 的下一代日誌體系建設實踐),雖然能夠降低 60% 的存儲成本,但仍然存在幾個比較明顯的問題,需要進一步的優化和解決。

一、存儲成本的優化

對於大規模的日誌數據,存儲成本一直是困擾企業的一個問題。我們採用了基於 ClickHouse 的解決方案,該方案實現了高效的數據編碼和壓縮率,有效降低了存儲成本。然而,當前 ClickHouse 日誌表數據依賴於雙副本方案,存儲成本仍有優化空間。

二、提升日誌排障能力

日誌做爲可觀測性 (logs/metrics/tracing/event) 的一環, 一個核心要求是提升排障能力,我們的目標是提升日誌排障能力,以支持 DevOps 中的問題定位和版本比對。我們致力於提升定位異常日誌的速度,並幫助快速發現和定位問題。這樣,我們能夠滿足對快速解決研發需求的追求。

三、存算一體方案的挑戰

原生 ClickHouse 採用的是 Share Nothing 架構,這種存算一體的方案在不增加計算節點的情況下無法容納海量的日誌數據。同時對於機型的選擇也會更加困難,向 B 站這邊每年的機型都是相對固定,對於日誌系統這塊一個是很難有相關機型滿足 (日誌存儲量遠大於需要的計算量),如果用通用機型意味着會存在不必要的資源浪費。如果使用專用機型,往往會出現類似 "過擬合" 的效果,如果出現資源不足或者因爲優化資源節省,很難做全公司層面資源騰挪,對資源混布也會更加困難。另外如果簡單的走存算一體方案隨着資源規模的變大,在追求降本增效的前提下必然會出現存儲計算比越來越大的情況,這意味當出現單個節點故障或擴容搬遷等需要副本修復或轉移的代價也越來越高。

四、滿足業務對於數據複雜處理的訴求

隨着用戶對日誌數據分析需求的增多,複雜的 ETL 操作變得必要。現階段,需要將 ClickHouse 日誌數據導出到分佈式文件系統(如 HDFS)進行處理,然後再重新導入 ClickHouse,導致導入導出的成本較高。我們的目標是整合離線和在線的數據處理和交互流程,打通公司的整個大數據體系,實現零轉換操作。用戶可以直接使用日誌平臺完成日誌的一般查詢,對於特別複雜或嚴重影響日誌平臺性能的場景可以直接使用大數據套件進行數據查詢或二次處理,避免不必要的導入導出成本,同時滿足查詢性能需求。

五、提高資源利用率

一方面,使用 ClickHouse 整機的成本比較高,日誌場景又是越久遠的越沒人查詢,所以我們希望我們的成本轉成固定成本 + 按使用靈活變化的成本。 另一方面,雖然各大公司都有做資源混布,但一個機器的資源是否可以完全被利用起來除了和調度算法和策略相關,也和業務模型相關。在實際中一臺機器上往往會有那麼 1 核兩核的邊角料不好用掉。因此我們希望把這些資源作爲補充一方面可以卸載一部分日誌平臺的計算資源,一方面提升整體的資源利用率。

業界調研

爲了解決上述問題,我們從日誌平臺本身的問題出發,進行了一系列的方案調研和討論,核心圍繞如何滿足解決上述問題,以及在解決上訴問題的前提下如何有效的確保 ROI,我們目的是解決問題找到合適當前狀態並對未來發展呈現開放狀態 (不會出現規模體量或業界有變化不得不大幅度掉頭) , 目的不是要做一個什麼東西去發論文。另外 B 站日誌團隊並不像一些公司動輒十幾號人,有充足的人力去做各種自研,實際的研發就 3 個人。同時當前問題又是緊迫的擺在團隊面前我們必須要能夠實現階段性產出比如半年就能拿到初步的收益,在後續在迭代中又可以逐步完善達到更高目標,逐步做深做強。

我們的調研主要包括 OpenSearch/Clickhouse/Loki/SLS 以及一些公司的內部方案。大的層面主要分爲 2 個派系:

  1. 存算一體。

    通過依託於各大雲廠商提供的彈性塊存儲或 NAS 等方式或公司內有一個非常強大的塊 / 文件存儲團隊,配合不同存儲和 ecs 套餐做資源生命週期流轉。這種方式再一定層度上可以降低一些成本,解決存儲計算的錯配,但對於其他方面並不能解決問題,這顯然對於 B 站來說並不合適,這個下面就不展開了。

  2. 存算分離。

    下面我們簡單展開說一下這一塊的調研情況。

OpenSearch/Clickhouse/SLS

這裏提到的 OpenSearch(AWS 推的 elasticsearch 項目) 主要是指其 remote storage 方案, 或者是一些公司基於內部分佈式存儲重構的 ElasticSearch 存算分離方案。Clickhouse 是指一些公司基於 clickhouse 構建存算分離方案 。這些方案不管是開源或者閉源,都是針對原本產品定位和體系做了相關設計,對性能和自控力上做了很高的 “強調”。這些項目雖然在定位以及側重點上都有所不同,但在一個比較大的層面的思路基本是比較相似的,最底層支持多種存儲系統,提供 filesystem 的抽象,比如支持 hdfs 也支持對象存儲。在這個之上構建存儲引擎層,存儲引擎可能是獨立的進程也可能是在計算引擎中的一個模塊,但這幾個基本定義了自己的數據組織方式,即 table format。一般的還會配有 metaservice 做元數據管理。indexservice 做索引加速,local cache 做訪問加速或結果加速等。如果這邊採用這樣子的方案前提下,會有兩個選擇,

  1. 基於一個產品自研。就像阿里雲的賣的 ElasticSearch 存算分離方案一樣。這樣子做對於當前的團隊面對的上訴提到的背景來說非常不切實際。

  2. 就直接用這些開源的解決方案。比如我們是不是可以直接用 opensearch?且不論這個方案是不是久經考驗,假設就是經歷過這樣子考驗,比如字節前段時間開源的 ByConity 內部有一定的使用規模,但我們是不是可以在較短時間內掌握這樣子的東西,社區是不是真的活躍。更爲關鍵的是這些方案基本都是數據封閉的,並不能滿足我們對開放的要求。同時也不能滿足我們對於和整個大數據體系結合的目標,我們希望非必要不需要做數據轉換流轉,應該進行原地查詢。

Loki

前面提到的 OpenSearch/Clickhouse 本身功能都非常強大,定位是 olap 產品非日誌系統,做日誌系統需要配套構建包括數據採集,數據管道,數據分發,日誌查詢等能力構建。而 Loki 設計之初就是定位輕量級低成本日誌系統,提供了完整的日誌系統能力。因爲 B 站在當前的日誌平臺 2.0 上已經具有了相關的基礎完整能力 (即使採用最多也要和當前的看怎麼結合),所以我們下面主要簡單說一下 Loki 的存儲引擎相關的設計。基本思路還是類似上面的分成 index store 和 chunk store。index store 存儲索引,也就是一行日誌的標籤,chunk store 存儲實際的數據。通過標籤(key+value) 計算出唯一 ID 關聯到一個 series(所以使用 loki 一般推薦標籤少一些,標籤基數低一些,不然會出現大量 series),一個 series 由若干 chunk 組成, 每個 chunk 在 chunk store 裏面對應一個實際的文件。寫入通過追加寫的方式寫入到 chunk。一個典型的查詢爲根據標籤查詢到對應的 series,通過 seriesID 查到關聯的 chunkID,然後暴力讀取每個 chunk 並根據其他條件 grep 數據,然後聚合返回。整個設計簡單直接,在思路上提供了一個不錯的想法:“暴力或許有時候也能解決問題”。當然就他這樣子的索引設計方式在實際場景中往往會導致小文件過多進而導致性能不達預期,使用場景會比較受限。一個是類似上面 opensearch 等的原因,二個是並不能支撐內部數據規模體量,所以很快我們放棄了 loki 的想法。

Billions3.0 架構

結合上訴的調研我們發現,我們需要幾個東西:

  1. 支持海量數據存儲的低成本存儲系統

  2. 業界通用的 table format 可以支持各種查詢引擎查詢

  3.  一個或多個高效的查詢引擎,可以實現較爲靈活的擴縮容

  4. 一個查詢網關屏蔽底層的查詢引擎的差異。

熟悉大數據的同學不難看出,這就是一個典型的湖倉一體想法。

整體架構

圖片

billions 3.0 日誌平臺,涵蓋了日誌採集、數據網關、數據管道、加工投遞、日誌引擎、查詢網關以及統一接入等,實現了整個端到端的一體,同時在架構上始終保持着放開狀態。下面簡單介紹各個層負責的主要工作以及能力。

日誌採集: 日誌採集這塊我們實現了日誌採集器 log-agent,支持 otel 協議以及常見的十幾種日誌格式採集,支持基礎的日誌處理下推,包括但不限於: 日誌格式解析,數據過濾,數據採樣等。主要以物理機 daemon 方式部署負責採集物理機以及容器產生的日誌,基本覆蓋了 B 站的全日誌場景。

數據網關: log-gateway 當前最新版本代號 kafka-proxy,主要負責日誌採集器上報數據的聚合投遞到數據管道,主要實現日誌數據的路由投遞到對應的數據管道集羣,同時實現透明的數據管道降級切換。數據網關以通用大集羣 + 高優集羣 + 專用集羣的方式部署。

數據管道: 這塊目的是爲了實現整個日誌流量的削峯填谷,同時實現採集和處理的解耦。這塊我們主要使用的 kafka 集羣實現。kafka 作爲老牌的消息中間件,各種計算引擎等實現了相關 connector。當前以通用大機羣 + 高優集羣 + 專用集羣的方式部署。

加工投遞: 這塊以自研的 log-consumer 爲主,flink job 爲輔。log-consumer 專注簡單場景的日誌加工投遞提供高性能和高靈活性,flink job 負責複雜場景的日誌加工投遞解決業務的特殊需求。業務在使用上根據不同的配置會最終生成對應的 log-consumer 或者 flink job 任務。這塊我們除了本身的數據入日誌引擎外,爲一些業務對於秒級可見性實時日誌消費的需求,我們還支持 kafka/databus(在線場景消息隊列) 消費。

日誌引擎: 當前採用 clickhouse + iceberg + hdfs + trino 的實現方式。給日誌平臺提供核心的存儲以及計算能力的同時也支持外部計算引擎 (flink/spark/presto 等) 基於 iceberg 進行直接查詢消費。

查詢網關: 主要目的爲屏蔽底層查詢引擎差異,實現統一的查詢語義,當前支持 DSL 以及類 SQL 語法。比如在 grafana 上配置日誌指標監控可以不需要知道底層是什麼。

統一接入: 主要是我們的用戶交互平臺以及 openapi 服務。日誌平臺支持採集接入、租戶管理、查詢分析以及監控告警等能力。

針對上述問題,我們設計了 billions3.0 日誌服務體系,主要實現了 iceberg + clickhouse 的混合存儲,實現了自研的可視化分析平臺,並統一了日誌的上報協議。

日誌引擎

B 站日誌平臺 2.0 日誌引擎完全基於 clickhouse 構建,基於一個基本假設天內數據查詢頻率遠大於超過一天數據。熱數據 (一天內) 採用 nvme 盤存儲以提供最快的查詢速度,冷數據 (超過一天) 採用 HDD 盤存儲。採用 clickhouse 自帶的基於 TTL 的數據生命週期管理方式進行數據流轉淘汰。

3.0 日誌引擎基本思路是:訪問加速層 + table format + 查詢引擎。當前數據訪問加速層採用的 clickhouse,table format 採用的 iceberg,查詢引擎默認使用的 trino。基本思路爲 log-consumer 雙寫 clickhouse 和 iceberg,查詢由 log-query 作爲統一查詢屏蔽 clickhouse 和 trino。對於大數據套件來說所有的數據已經在數據湖中,可以通過各種查詢引擎對數據進行直接查詢或者二次處理。

訪問加速層

3.0 日誌引擎的查詢加速層採用的是 clickhouse,主要是以下幾個原因:

  1. 3.0 是 2.0 的延續,我們 2.0 時在日誌場景做了不少優化,也沉澱了不少技術積累,同時在熱數據上 clickhouse 並沒有成爲 "問題"

  2. 一圈調研之後確實沒有比 clickhouse 更適合當前的背景的訪問加速層引擎 (低成本、高性能)

  3. 公司有專業的 clickhouse 團隊,日誌團隊和 clickhouse 團隊構建了良好的合作基礎,能夠共同進退

與 2.0 不同的是 clickhouse 不再被認爲是數據生命週期流轉的必要的階段,而是做爲一個訪問加速作用。在實際的場景中,有業務日誌類似於審計日誌等並不需要很快的查詢速度,也不存在明顯的查詢冷熱分層的情況,我們當前會選擇關閉 clickhouse 的寫入以減少不必要的資源浪費。因爲 clickhouse 在 3.0 中只是作爲訪問加速層存在,以現在架構下要進行加速層引擎的插拔並不是一件很難的事情,哪天出現更加合適的引擎我們也會考慮進行必要的替換,或者在一些場景下使用 clickhouse,在一些場景下使用另外的引擎。

核心訪問層

這塊我們需要考慮的是幾個問題:我們應該選擇哪種 table format?我們應該選擇哪種底層存儲系統?我們應該選擇哪種查詢引擎?

先來說問題 1,業界現在主流的 table format 主要有: iceberg、hudi、delta lake 等。幾個 table format 隨着過幾年的發展能力上也越發趨於類似。從日誌平臺的角度看:

  1. 我們是希望使用被業界主流認可的 table format 以方便後續架構的迭代演進,這三個其實都滿足。

  2. 最好 B 站有相關團隊在維護並進行二次開發,因爲介於日誌團隊人員情況,當前並不適合自己去維護一套 format 並進行二次開發。

  3. 對於日誌場景來說,其實需要的主要是一個可以持續追加寫入並且可以動態改變 schema 的表格存儲 (schema less)。對更新、time travel 等並不感冒。

  4. 我們希望一款定位簡單清晰的 format,能夠比較容易進行二次開發,比如元數據優化,索引優化等,我們並不需要大而全且複雜的東西,畢竟我們的場景是日誌平臺,並不是要做一個大數據計算平臺。

綜上我們最後選擇了 iceberg 作爲我們的 table format。

再來說問題 2,其實在 B 站 (自建機房) 並沒有太多的選擇,主要有對象存儲和 hdfs(我們並不打算去自研底層存儲這個並不適合我們團隊)。兩個產品都提供了數據做 EC 以實現低成本存儲,也就是在低成本上兩邊並沒有特別的差異。最後我們選擇 hdfs 主要考慮了幾個點:

  1. 對於存算分離架構來說,計算池化 / 存儲池化是一個必然要考慮的問題,而擁有一個足夠大的存儲池,更加有利於對數據放置的調度,更加有利於閒散 io 的利用,後續做相關的優化也更加不容易掣肘。而在 B 站當前情況下 hdfs 的存儲規模遠大於對象存儲。

  2. hdfs 長期做爲整個大數據存儲底座天然和整個大數據有更好的配合,也就各種大數據引擎都考慮對 hdfs 的優化。而我們 3.0 的一個目標是和大數據體系打通

所以我們最終選擇了 hdfs 作爲底層存儲系統,默認 EC 採用 6+3 配比,僅需 1.5 倍存儲成本用來保存日誌數據就能提供比之前 Clickhouse 2 副本更高的數據持久性。

最後說問題 3,因爲整個架構是開放的,其實 B 站內部所有的大數據查詢引擎都是可以直接查詢 iceberg 的。日誌平臺本身採用的查詢引擎默認是 trino,採用 trino 的幾個核心原因主要是:

  1. trino 和 iceberg 是一個團隊在進行研發,相關團隊在兩者結合上做了不少優化,比如索引優化、小文件優化等

  2. trino 當前在日誌場景提供了不錯的查詢性能,是可以滿足絕大部分場景的 (在實際業務場景中可以實現 1400 億行數據點查 20 秒返回)。

  3. B 站 trino 採用容器化部署,當資源不足時可以較爲方便的進行擴容

所以我們最終選擇了 trino 作爲默認的查詢引擎。當然我們對一些其他查詢引擎也保持觀望,比如: presto + velox,spark + gluten,StarRocks 數據湖方案等等

日誌表的設計

Iceberg 日誌數據按照業務存儲在不同的日誌表中,日誌表按照天作爲分區,部分日誌表可能按照業務字段構建二級分區,日誌表中的字段主要按照以下方式規劃:

  1. 公共字段,公共字段包含抽象出來的所有日誌都會有的獨立字段,例如 timestamp, app_id 等等。

  2. log_msg 字段,log_msg 字段是日誌的文本字段,用戶可基於該字段進行文本檢索。

  3. 私有字段,私有字段在各業務日誌中並不相同,且可能會隨着業務日誌埋點的不同動態變化,不同於 log_msg 文本字段,私有字段是日誌的維度數據,主要用於在日誌查詢時點查或範圍過濾。

日誌數據的異步優化

嗶哩嗶哩基於 Iceberg 的湖倉一體平臺提供了對於 Iceberg 數據進行管理優化的能力,通過採集 Iceberg 表的 Commit 信息(類似於 Mysql 的 Binlog)結合表本身的元信息 (表的排序字段,索引等),按照一定規則和策略拉起 Spark 任務對已經寫入 Iceberg 表的數據異步進行重新的組織和優化,具體的能力包括:

  1. 小文件合併。實時寫入的日誌數據可能會產生大量的小文件,對 HDFS NameNode 產生較大壓力,且小文件會影響查詢性能,Iceberg 數據優化任務會盡量將小文件合併成期望大小的文件。

  2. 數據排序和組織。數據的排序組織方式會影響索引的效果,以及壓縮的效率,Iceberg 數據優化任務會按照表的元數據定義對日誌數據進行重新的排序組織,我們支持對於 Iceberg 表定義文件間和文件內不同的排序方式,以及 Order/Z-Order/Hibert-Curve-Order 等多種排序方式,數據的排序組織可能和小文件合併在同一個任務中完成。

  3. 索引生成。除了 Iceberg 本身的 MinMax Metrics,以及 Parquet/Orc 文件內部的 MinMax,BloomFilter 等 Segment Metrics,我們的湖倉一體平臺還支持更多擴展的文件級別的索引,Iceberg 數據優化服務根據用戶自定義的 Iceberg 表的索引類型,在 1,2 兩步完成後拉起 Spark 任務生成對應的索引數據。

  4. Iceberg Metadata 優化。頻繁的數據寫入會產生大量的 snapshot,影響訪問 Iceberg 表元數據的性能,Iceberg 數據優化服務也會自動拉起對應任務清理過期 snapshot。

圖片

通過湖倉一體平臺提供的能力,我們可以結合日誌場景數據和查詢的具體情況,對於日誌數據進行合理的配置和管理優化,使得大規模日誌數據的低成本交互式分析成爲可能。

正向索引的使用

日誌數據的查詢普遍會限制在一定的時間範圍內,如何根據用戶查詢的時間範圍儘量減少需要掃描的數據量是加速查詢性能的關鍵之一,日誌表的時間分區(一般是天分區)能夠進行分區級別的 Data Skipping,只掃描滿足時間過濾條件的分區數據,但是對於時間範圍更小的查詢,比如 2023-05-20:10:05:00 ~ 2023-05-20:10:15:00,則需要通過正向索引和數據排序組織進行進一步的 Data Skipping。在實踐中,我們可以將_timestamp 字段設置爲文件間和文件內排序字段,使得優化後的 Iceberg 數據在分區內按照_timestamp 充分聚集,在 Iceberg 文件級別,通過 Iceberg 的 MinMax Metrics 在 Trino 查詢的 Coordinator getSplits 階段將不需要的文件直接 Skip 掉,對於沒有過濾掉的文件,在 Trino Worker 處理 Split,讀取 Orc 數據時,還可以繼續用 Orc Segment 級別的 MinMax Metric 進行文件內 Segment 級別的 Data Skipping。

對於其他常見的過濾字段,則可以通過二級索引進行 Data Skipping,比如對於常見的點查過濾,可以考慮在該字段上配置 BloomFilter 索引,對於範圍過濾,可以在該字段上配置 BloomRangeFilter 索引等。

基於 Iceberg 原生和我們擴展的正向索引,通過合理的索引配置,我們可以根據用戶查詢中基於公共字段的過濾條件把需要掃描的數據限制在相對較小的範圍內了,爲交互式查詢打下一個良好的基礎。

針對高基數字段的點查:

select * from test where arg_trid = '1007997177f95bd44536bb570fd193830ab1' and (log_date = '20230512' or log_date = '20230513') order by _timestamp desc limit 200;

反向索引的使用

除了時間範圍和基於公共字段的過濾條件,常在用戶查詢中出現的過濾條件還包括基於 log_msg 字段的文本檢索條件,特別是在日誌排障場景中,如何根據文本檢索條件進一步縮小需要掃描的數據是支持交互式日誌分析的關鍵。

如何快速地進行文本檢索是工業界和學術界已經探索了很多年的方向,技術已經非常成熟,其中最主要的手段就是通過反向索引進行查詢加速。

TokenBloomFilter 索引

我們首先擴展 Iceberg 實現了一個輕量級的 TokenBloomFilter 索引,支持在 Iceberg 文件級別對索引字段先分詞,分詞後生成 BloomFilter 索引。BloomFilter 數據結構佔用空間小,非常適合針對低頻詞的文件檢索。

但是 Bloomfitler 是一種 Approximate 數據結構,有出現 False Positive Probability 的可能,所以只能用於 membership 的判斷,無法準確定位到符合檢索條件的數據行,對於部分場景,BloomFilter 索引過濾文件的效果不是很好,比如日誌檢索中經常出現的 Phrase 查詢,TokenBloomFilter 索引只能根據 Phrase 短語中分詞後的 term 是否全部出現在文檔中判斷是否可以跳過掃描文件,而無法充分利用檢索條件表達的 "Phrase 短語中分詞後的 term 全部出現在文檔中的某一行且滿足出現順序" 的約束條件。基於此,我們進一步實現了 TokenBitMap 索引。

TokenBitMap 索引

TokenBitMap 索引主要是基於著名開源文本檢索框架 Lucene 的一些基礎能力實現,並沒有直接使用 Lucene 索引,這主要基於如下考慮:

  1. 日誌排障是典型的精確文本檢索場景,日誌平臺需要精確返回所有滿足用戶檢索條件的數據,不需要打分,排序,同義詞等能力,Lucene 作爲比較全能的文本檢索框架,對於精確文本檢索場景冗餘的能力會帶來額外的代價。

  2. Iceberg 日誌數據在文本檢索場景下主要用於歷史日誌數據的排障,訪問相對低頻,我們更關注在低存儲成本下加速查詢性能,Lucene 索引的存儲成本過高,有時甚至索引文件大小超過數據文件本身。

  3. Lucene 索引是爲本地文件系統所設計,每個 Lucene 索引會產生數十個索引文件,Iceberg 存儲在 HDFS 上,大量小文件對於 HDFS 不友好。

所以我們使用 Lucene 的基礎能力實現了一個相比 Lucene 索引更加輕量級的索引類型:TokenBitMap 索引。Token BitMap 索引結構十分簡單,索引文件包括 Token 字典和 BitMap 索引兩部分,Token 字典使用 Lucene 的 FST 存儲,FST 會記錄 Token 對應的 BitMap 在 BitMap 索引文件中的偏移量,在匹配 Token 時,會優先讀取 FST 進行存在性判斷,如果存在,通過 FST 獲取 Token 在 BitMap 索引中的偏移量,並返回相應的 BitMap。

由於 BitMap 包含了 Token 在數據文件中出現的 RowId 信息,可以根據過濾條件表達式進行交併差計算,返回確定的行級的 DataSkipping 信息。此外,我們還支持將 TokenBitMap 索引匹配出的 BitMap 透傳到 Trino 的 TableScan 節點中,在訪問 Parquet/Orc 文件時,使用 BitMap 信息進行精確的文件內 Segment Skipping,儘可能減少需要掃描的數據量。

相比於 TokenBloomFilter 索引,TokenBitMap 索引可以更加充分地利用文本檢索條件過濾掃描數據,不過 TokenBitMap 索引的缺點就是佔用存儲空間過大,在實現 TokenBitMap 索引時,我們也針對這方面進行的重點的優化設計。首先是分詞器,分詞器決定了索引字段分詞後 Token 的數量,從而決定 FST 的大小和 BitMap 的數量,我們實現了一個自定義的 LogAnalyzer,在 EnglishAnalyzer 的默認停用詞基礎上新增了日誌文本中通用的關鍵詞,比如 timestamp、app_id 等,同時限制了 token 的最大長度,默認最大長度爲 40,並對數字類型 token 進行了裁剪,這些優化後,生成的 Token 索引整體接近 50% 存儲空間的減少。其次,對於 BitMap 的存儲,分爲三種情況,低頻詞,中頻詞,高頻詞,對於低頻詞,相比於使用 BitMap 存儲其行號信息,使用壓縮數組存儲空間反而更小,對於高頻詞,其 BitMap 存儲所需空間較大,但是因爲其廣泛存在文件的大部分數據行中,對於 Data Skipping 作用甚小,ROI 小,我們不存儲這種類型的 BitMap,低頻詞 / 中頻詞 / 高頻詞的劃分通過參數控制,可以根據實際日誌數據情況靈活調整。

反向索引的性能測試

我們使用實際日誌數據進行了測試對比,330GB ORC 格式的日誌數據,生成 TokenBloomFilter 索引 2.1GB,生成 TokenBitMap 索引 76.6GB,使用了低頻詞 / 中頻詞 / 高頻詞(出現的次數分別是 25/2813/127204438 次)檢索的性能如下:

低詞頻查詢:

select count(*) from test01 where has_token(log_msg, '1666505943110300001');

中詞頻查詢:

select count(*) from test01 where has_token(log_msg, '1978979513');

高詞頻查詢:

select count(*) from test01 where has_token(log_msg, '1664553600');

可以看到,在中低詞頻的檢索中,對比於 TokenBloomFilter,TokenBitMap 索引的查詢性能更好,在需要掃描的數據量和查詢消耗的 CPU 時間方面優勢更加明顯。不過在實際的日誌排障使用場景中,考慮到最近的日誌數據在 ClickHouse 有存儲加速,Iceberg 日誌數據主要滿足歷史以及跨天日誌數據排障,查詢頻次較低,我們更關注存儲成本的代價,所以對於大部分日誌數據,只創建 TokenBloomFilter 索引,只對少部分查詢頻次較高,性能要求較高的日誌數據構建 TokenBitMap 索引。

進一步的探索

日誌數據除了如 timestamp/app_id 等公共字段及 log_msg 文本字段,通常還會在數據入湖過程中抽取出不同業務各自的私有字段用於日誌查詢時更方便的檢索過濾,這些私有字段各業務皆不相同且可能動態變化,所以通常使用 Map 或者 Json 類型字段存儲,對於此類字段,如何更好地利用過濾條件進行 Data Skipping,是我們進一步探索的方向,我們在這方面的工作如下:

  1. 支持基於 map_keys(col)/map_values(col) 表達式創建索引,此索引可以用於常見 Map 類型過濾條件 element_at 的 Data Skipping,例如對於過濾條件 element_at(col, 'key1') = 'v1', 可以首先使用基於 map_keys(col) 生成的索引判斷‘key1‘是否在文件中存在,然後使用基於 map_values(col) 的索引判斷‘v1’是否在文件中存在。

  2. 如果用戶日誌查詢只會經常使用某一個 key 值做過濾,則可以直接基於 element_at(col, 'key1') 表達式創建索引,只從 Map 中抽取‘key1’對應的 value 構建索引,從而減少索引大小,提升索引過濾效果。

  3. 支持基於 json_scalar_extract($json_path) 表達式創建索引,用戶可以使用此方式從 json 字段中抽取常見內部字段構建索引,在查詢時,如果使用對應 json 路徑抽取的字段作爲過濾條件,則可以通過索引判斷是否可以跳過掃描文件。

計算下推

爲了減少後端資源的使用,我們可以在 log-agent 上執行一部分簡單的計算,把後端的計算卸載到相關節點上,把物理機上的閒散資源利用起來。其中比較典型的玩法是支持下推非結構化 / 半結構化日誌解析爲結構化日誌,我們通過不同的參數配置可以讓相關轉換是在消費端進行還是採集端進行。現在只有小部分因爲相關機器資源使用要求,我們計算還是在消費端專門的消費服務進行解析,大部分日誌的結構化轉換我們都已經在 log-agent 完成。

消費調度

1.1.1 旨在解決的問題

考慮到容災和可用性要求,我們在 3.0 中的基本思路是按高優集羣 + 專用集羣 + 通用大集羣的方式進行數據分流。

  1. log-agent 可以根據 AppID+StreamID 路由規則進行調度到不同的 log-gateway 集羣。默認情況下,高優日誌進入 kafka-proxy-high 集羣,沒有特殊要求的日誌進入到日誌大集羣 (絕大部分日誌都在這個集羣), 另外有特殊場景要求的,比如極高優要求完全不想被其他人影響的,值得專門部署一套鏈路的,我們也支持專用集羣,但原則上我們儘量會避免,因爲這在資源利用率上並不會有很好的效果。對於出現任意集羣出現不穩定時,我們優先會考慮對集羣快速彈性的擴容 (log-gateway 是無狀態的), 當擴容不能解決問題時,我們可以快速將該集羣的流量一部分或所有切到其他集羣中。

  2. log-gateway 可以根據 AppID+StreamID 維度路由規則進行調度到不同的 kafka 集羣。同樣我們把 kafka 分成了高優 / 專用 / 通用大集羣,絕大部分日誌會進通用大集羣。由於 kafka 是一個有狀態服務,加之其相關設計實現彈性擴縮容能力並不太理想。在這個層面我們會優先把相關日誌流調度到其他集羣,同時配合下游 log-consumer 的擴容。

  3. kafka topic 層面我們同樣採用大 + 小的方式,對於一些特別大,或優先級高的我們會拆分單獨的 topic(這裏提一點在我們的架構下,把一個或多個流拆分到其他 topic 是很簡單的事情);對於一般的日誌流我們會根據資源使用相對均勻得拆到到 N 個 topic 裏面。採用大 + 小的主要是成本 + 容災之間的 tradeoff。

  4. log-consumer 同樣是一個無狀態服務,採用 golang 編寫,容器化部署,整體資源使用率比同樣場景的 flink 至少少 50%。可以實現方便的彈性伸縮,同時可以根據路由規則動態消費不同的 topic 以實現充分的資源均衡利用。

該方案上線之後效果顯著,年初頻繁因爲業務突增流量導致整個日誌鏈路整體不可用的情況得到很好的抑制, 半年來未發生因爲這塊出現相關故障。

打通大數據體系

得益於我們架構上採用了 iceberg 這種 table format,打通 B 站大數據體系變得容易起來。下面簡單提一下批處理場景和流處理場景。

批處理場景分區提交

這個策略是基於 Kafka 消費延遲和寫入延遲的雙重指標來動態提交 Hive 分區。

  1. 監控寫入程序的消費延遲:這是初始步驟,需要計算日誌的上報時間和寫入存儲的時間差,這樣就可以得到日誌在實際被寫入之前的延遲時間。這是一項關鍵的度量,因爲它可以瞭解數據從接收到實際寫入存儲的耗時。

  2. 監控 Kafka 的消費 lag:觀察到數據消費存在延遲時,對比消費延遲時間和消費端的吞吐量,可以預估出一個延遲數據被消費掉的時間。

  3. 結合寫入延遲和 Kafka 的 lag:在這個階段,我們結合寫入延遲和 Kafka 的 lag,以及預定的提交延遲閾值,來決定是否提交 Hive 分區。可以設定一個規則,如果寫入延遲和 Kafka 的 lag 都超過了預設的閾值,那麼就提交該分區。

流處理場景分區提交

Flink 側是使用 Flink 作爲觀察者發送消息通知,觀察者爲 Iceberg 端,被觀察者分區是否就緒是引擎端可以直接感知的事情。具體的感知方式會因不同的引擎而異。對於 Flink,我們可以利用 Watermark 這個概念感知分區是否就緒。當分區就緒後,我們可以註冊一個事件處理函數和對應的事件類型——在我們的例子中,是實現了 Flink 自帶的 PartitionCommitPolicy 的 CommitPolicy。在 CommitPolicy 中,我們實現具體的 commit 邏輯,即調用調度平臺 API 以實現分區就緒的通知機制。

具體實現這一設計思路需要對 Flink 寫入 Iceberg 的線程模型進行修改。我們可以在 IcebergStreamWriter 算子的 prepareSnapshotPreBarrier 階段增加分區處理邏輯,並把分區信息發送到下游 IcebergFilesCommitter 算子。這些新的分區信息(我們稱之爲 pendingPartition)被存儲在一個 Set 中,等待提交。當這些 pendingPartitions 滿足提交條件後,我們將其從 Iterator 中移除。

分區處理邏輯的實現借鑑了 Hive connector 的做法。在 checkpoint 完成時,我們將可提交的分區(committablePartition)發送到下游的 IcebergFilesCommitter 算子。IcebergFilesCommitter 收到 committablePartition 後,會將這些 committablePartition 加到 pendingPartitions 裏。

當分區就緒時,我們會調用 Archer(B 站 DAG 任務調度平臺) API 完成消息通知。爲了在批量計算過程中支持 Iceberg 表,我們需要設計一套在分區就緒後進行消息通知的策略,分區就緒的標誌分爲兩部分,一部分是觀察分區就緒的條件,另一部分是分區就緒後的消息通知設計。消息通知設計的時候,主要考慮在分區就緒的時候,在哪個層面通知 Archer 調度下游任務,其中包含兩種設計思路:一種是將 Flink 作爲觀察者發送消息通知,另一種是將 Iceberg 作爲觀察者發送消息通知。

在 Flink 觀察者模式下,分區就緒的標誌是引擎測可以直接感知的,具體的感知方式會因不同的引擎而有所不同,對於 Flink,我們可以使用 watermark 這個概念來感知分區是否就緒。在分區就緒後,我們可以註冊一個事件處理函數和對應的事件類型 ArcherCommitPolicy(實現了 Flink 自帶的 PartitionCommitPolicy),並且在 ArcherCommitPolicy 裏實現具體的 commit 邏輯,即調用 Archer API 來實現分區就緒的通知機制。由於 Iceberg 是基於文件級別進行統計的,所以我們可以在文件級別獲取到對應的分區信息。

日誌聚類

我們加強了日誌分析的能力,幫助用戶進行更好的日誌排障。在服務出現問題時候,通常 ERROR 的日誌量會暴增,不利於問題的定位,使用我們的輕量級日誌聚類功能,可以將相似度高的日誌聚合,做到秒級返回日誌聚類,迅速理解日誌全景,提升問題定位效率。

日誌聚類在 DevOps 中可以被應用於問題定位和版本比對,這對於快速發現異常日誌和定位問題是非常有幫助的。主要的設計需求包括:

  1. 聚類過程需要儘可能快,而且結果應非常穩定。換言之,聚類的類別和結果不應有波動。

  2. 需要能夠保證日誌模式的一致性,以便在不同的時間段內,通過日誌類別查看其波動和變化。

設計思路是結合阿里雲和觀測雲的日誌聚類功能。阿里雲採用全量日誌聚類,將所有日誌數據通過聚類模型獲取其模式。這需要消耗大量的計算資源,且模式和索引需要落盤,從而增加了約 10% 的日誌存儲。觀測雲則選擇對部分日誌進行聚類,它查詢限定時間範圍內的 1w 條日誌數據進行聚類,因此其聚類結果可能不完全穩定,同時也無法進行日誌對比。

因此,我們的目標是在需求更少的資源的同時,獲得更豐富且更穩定的聚類結果。

我們可以用下面這張圖來理解日誌聚類所做的工作:

日誌模式解析過程可以理解爲是一個倒推日誌打印代碼的過程,也是一個對日誌聚類的過程(相同 pattern 的日誌認爲是同一類日誌)。

算法思路設計:

被同一條代碼打印出來的日誌肯定是相似的,所以我們可以得到第一種模式解析的思路,給出文本相似度公式或距離公式,通過聚類算法,將相同模式的日誌聚到一起,

然後再獲取日誌模板,業界基於聚類的日誌模式解析算法,如 Drain3、Lenma、Logmine、SHISO 等。但在實際聚類過程中會往往存在很多的問題,聚類速度慢,大量的 pattern 類別、全量計算消耗大量資源等問題,

我們設計了基於固定深度解析樹的思路,多個子 pattern 進行層次融合的方式,結合代碼行號等特徵對聚類速度和精度進行加速聚類

整體算法步驟分爲以下幾個部分:

預處理的獲取日誌平臺表達式查詢後的全部日誌數據,(對於超過 10w 條的日誌進行採樣)在對日誌進行解析前,都會先進行分詞,因爲詞是表達完整含義的最小單位,將一些特殊詞,如 IP 地址、時間等給識別出來,

然後替換爲特殊字符或去掉,這是由於這些特殊詞明顯是參數,如此處理可以有效提高相同模式日誌的相似度。提取日誌消息對應的 日誌行號特徵數據

聚類的簡單過程如下,我們首先構建一個固定深度的解析樹,對於日誌進行聚類,

1,根據日誌的長度分組和日誌行號等以及根據日誌的前幾個單詞分組,樹深度決定了用前多少個單詞進行分組。

2,解析樹的上層節點以日誌行號特徵和日誌消息的長度(token 的數量)區分日誌組,根據預處理後的日誌消息前幾個單詞依次向下搜索,直到葉子節點。葉子節點下存儲着該組別中的聚類簇,

搜索到葉子節點後再計算相似度,根據相似度計算結果更新子聚類中心或者創建新的聚類子簇

相似度計算邏輯如下, 在找到 simSeq 最大的日誌組後,將其與自適應的相似度閾值 st 進行比較,如果 simSeq≥st,那麼就會返回該組作爲最佳匹配。

圖片

3,更新解析樹,將每個日誌消息解析爲字段,並按照固定深度樹的結構進行插入。每個字段都對應樹中的一個節點,如果節點已存在,則更新節點的統計信息;如果節點不存在,則創建新節點,對於匹配上的子 pattern,

描日誌消息和日誌事件相同位置的 token,如果兩個 token 相同,則不修改該 token 位置上的 token。否則,在日誌事件中通過通配符 * 更新該 token 位置上的 token。

4,層次融合,對於相似的 pattern 進行融合,結合 LCS(最大公共子序列)的思路進行融合,將改善聚類效果,比如使同一行號下不同的 pattern 和不同行號特徵下的子 pattern 聚類得到公共 pattern。

5,模型保存與推理

聚類後的模型按 appid 進行保存,在後續實時日誌聚類推理過程中,將直接日誌消息與模型的解析流程進行匹配,未匹配上的日誌將實時更新聚類的模型

下面是日誌聚類的效果:

整體收益

綜上所述,通過我們對日誌系統的持續演進, 進一步降低了存儲成本 (至少 20%) 並增強了日誌系統的穩定性, 保證了日誌的低延遲、低成本, 以支持全公司的各類日誌數據, 以及滿足他們的查詢和進一步使用需求。我們還基於 iceberg 實現了離在線一體架構的演進的同時還保持了架構的開放性。

同時, 我們圍繞日誌作爲核心, 構建了一整套針對 MTTR 的日誌服務和功能, 包括日誌一站式快速分析、基於最小代價的日誌聚類、靈活配置打通可觀測性平臺的日誌告警等, 幫助業務顯著降低平均故障修復時間。

未來展望

在過去半年時間裏我們完成了上訴相關的工作,基本解決了開頭提到的幾大問題。但當前系統仍然存在諸多不足以及功能補齊。

  1. clickhouse 多集羣平滑拆分。解決 clickhouse 集羣越來越大導致的不必要的穩定性問題。

  2. 日誌數據 insight 能力,幫助業務進行日誌管理,簡化業務自主日誌優化以及降本。

  3. 基於 opentelemery 和整個可觀測性平臺更強的聯動,提供更強的根因分析以及排障能力。

  4. 實現快速海外雲上部署。當前方案嚴重依賴 B 站大數據體系以及微服務體系,以至於海外雲上部署困難重重。

  5. 統一可觀測性平臺幾大組件底層技術支撐能力。讓 logs/tracing/metrics 基於統一的架構上,實現更大層面的資源混合調度

  6. 探索爲日誌而生的 iceberg meta service 以及 index service 可行性,進一步提升對於海量日誌查詢下的性能。

  7. 探索更加彈性的數據管道以及消費端組件,提供更靈活的資源調度。

  8. 探索 log-agent 基於 wasm 的動態算子下推能力。

參考文獻

[1] B 站基於 Clickhouse 的下一代日誌體系建設實踐

[2] B 站基於 Iceberg 的湖倉一體架構實踐

[3] Architecture | Grafana Loki documentation(https://grafana.com/docs/loki/latest/fundamentals/architecture/

[4] Remote-backed storage - OpenSearch documentation(https://opensearch.org/docs/latest/tuning-your-cluster/availability-and-recovery/remote-store/index/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/tnP6hI2KwPde7PGg6QyUCw