天天看點

存儲成本一降再降,B站日志平台的演進之路

作者:dbaplus社群

作者介紹

季俊宇,哔哩哔哩進階開發工程師;

李銳,哔哩哔哩資深開發工程師。

一、背景

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

1、存儲成本的優化

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

2、提升日志排障能力

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

3、存算一體方案的挑戰

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

4、滿足業務對于資料複雜處理的訴求

随着使用者對日志資料分析需求的增多,複雜的ETL操作變得必要。現階段,需要将ClickHouse日志資料導出到分布式檔案系統(如HDFS)進行處理,然後再重新導入ClickHouse,導緻導入導出的成本較高。我們的目标是整合離線和線上的資料處理和互動流程,打通公司的整個大資料體系,實作零轉換操作。使用者可以直接使用日志平台完成日志的一般查詢,對于特别複雜或嚴重影響日志平台性能的場景可以直接使用大資料套件進行資料查詢或二次處理,避免不必要的導入導出成本,同時滿足查詢性能需求。

5、提高資源使用率

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

二、業界調研

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

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

  • 存算一體

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

  • 存算分離

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

1、OpenSearch/Clickhouse/SLS

這裡提到的OpenSearch(AWS推的elasticsearch項目)主要是指其remote storage方案, 或者是一些公司基于内部分布式存儲重構的ElasticSearch存算分離方案。Clickhouse是指一些公司基于clickhouse建構存算分離方案 。這些方案不管是開源或者閉源,都是針對原本産品定位和體系做了相關設計,對性能和自控力上做了很高的“強調”。這些項目雖然在定位以及側重點上都有所不同,但在一個比較大的層面的思路基本是比較相似的,最底層支援多種存儲系統,提供filesystem的抽象,比如支援hdfs也支援對象存儲。

在這個之上建構存儲引擎層,存儲引擎可能是獨立的程序也可能是在計算引擎中的一個子產品,但這幾個基本定義了自己的資料組織方式,即table format。一般的還會配有metaservice做中繼資料管理。indexservice做索引加速,local cache做通路加速或結果加速等。如果這邊采用這樣子的方案前提下,會有兩個選擇,

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

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

2、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架構

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

  • 支援海量資料存儲的低成本存儲系統;
  • 業界通用的table format可以支援各種查詢引擎查詢;
  • 一個或多個高效的查詢引擎,可以實作較為靈活的擴縮容;
  • 一個查詢網關屏蔽底層的查詢引擎的差異。

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

1、整體架構

存儲成本一降再降,B站日志平台的演進之路

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的混合存儲,實作了自研的可視化分析平台,并統一了日志的上報協定。

2、日志引擎

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、通路加速層

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

  • 3.0是2.0的延續,我們2.0時在日志場景做了不少優化,也沉澱了不少技術積累,同時在熱資料上clickhouse并沒有成為"問題";
  • 一圈調研之後确實沒有比clickhouse更适合目前的背景的通路加速層引擎(低成本、高性能);
  • 公司有專業的clickhouse團隊,日志團隊和clickhouse團隊建構了良好的合作基礎,能夠共同進退。

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

4、核心通路層

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

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

  • 我們是希望使用被業界主流認可的table format以友善後續架構的疊代演進,這三個其實都滿足;
  • 最好B站有相關團隊在維護并進行二次開發,因為介于日志團隊人員情況,目前并不适合自己去維護一套format并進行二次開發;
  • 對于日志場景來說,其實需要的主要是一個可以持續追加寫入并且可以動态改變schema的表格存儲(schema less)。對更新、time travel等并不感冒;
  • 我們希望一款定位簡單清晰的format,能夠比較容易進行二次開發,比如中繼資料優化,索引優化等,我們并不需要大而全且複雜的東西,畢竟我們的場景是日志平台,并不是要做一個大資料計算平台。

綜上我們最後選擇了Iceberg作為我們的table format。

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

  • 對于存算分離架構來說,計算池化/存儲池化是一個必然要考慮的問題,而擁有一個足夠大的存儲池,更加有利于對資料放置的排程,更加有利于閑散io的利用,後續做相關的優化也更加不容易掣肘。而在B站目前情況下hdfs的存儲規模遠大于對象存儲;
  • hdfs長期做為整個大資料存儲底座天然和整個大資料有更好的配合,也就各種大資料引擎都考慮對hdfs的優化。而我們3.0的一個目标是和大資料體系打通。

是以我們最終選擇了hdfs作為底層存儲系統,預設EC采用6+3配比,僅需 1.5 倍存儲成本用來儲存日志資料就能提供比之前 Clickhouse 2副本更高的資料持久性。

最後說問題3,因為整個架構是開放的,其實B站内部所有的大資料查詢引擎都是可以直接查詢iceberg的。日志平台本身采用的查詢引擎預設是trino,采用trino的幾個核心原因主要是:

  • trino和iceberg是一個團隊在進行研發,相關團隊在兩者結合上做了不少優化,比如索引優化、小檔案優化等;
  • trino目前在日志場景提供了不錯的查詢性能,是可以滿足絕大部分場景的(在實際業務場景中可以實作1400億行資料點查20秒傳回);
  • B站trino采用容器化部署,當資源不足時可以較為友善的進行擴容。

是以我們最終選擇了trino作為預設的查詢引擎。當然我們對一些其他查詢引擎也保持觀望,比如: presto + velox,spark + gluten,StarRocks資料湖方案等等

5、日志表的設計

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

  • 公共字段,公共字段包含抽象出來的所有日志都會有的獨立字段,例如timestamp, app_id等等;
  • log_msg字段,log_msg字段是日志的文本字段,使用者可基于該字段進行文字檢索;
  • 私有字段,私有字段在各業務日志中并不相同,且可能會随着業務日志埋點的不同動态變化,不同于log_msg文本字段,私有字段是日志的次元資料,主要用于在日志查詢時點查或範圍過濾。

6、日志資料的異步優化

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

  • 小檔案合并。實時寫入的日志資料可能會産生大量的小檔案,對HDFS NameNode産生較大壓力,且小檔案會影響查詢性能,Iceberg資料優化任務會盡量将小檔案合并成期望大小的檔案;
  • 資料排序群組織。資料的排序組織方式會影響索引的效果,以及壓縮的效率,Iceberg資料優化任務會按照表的中繼資料定義對日志資料進行重新的排序組織,我們支援對于Iceberg表定義檔案間和檔案内不同的排序方式,以及Order/Z-Order/Hibert-Curve-Order等多種排序方式,資料的排序組織可能和小檔案合并在同一個任務中完成;
  • 索引生成。除了Iceberg本身的MinMax Metrics,以及Parquet/Orc檔案内部的MinMax,BloomFilter等Segment Metrics,我們的湖倉一體平台還支援更多擴充的檔案級别的索引,Iceberg資料優化服務根據使用者自定義的Iceberg表的索引類型,在1,2兩步完成後拉起Spark任務生成對應的索引資料;
  • Iceberg Metadata優化。頻繁的資料寫入會産生大量的snapshot,影響通路Iceberg表中繼資料的性能,Iceberg資料優化服務也會自動拉起對應任務清理過期snapshot。
存儲成本一降再降,B站日志平台的演進之路

通過湖倉一體平台提供的能力,我們可以結合日志場景資料和查詢的具體情況,對于日志資料進行合理的配置和管理優化,使得大規模日志資料的低成本互動式分析成為可能。

6、正向索引的使用

日志資料的查詢普遍會限制在一定的時間範圍内,如何根據使用者查詢的時間範圍盡量減少需要掃描的資料量是加速查詢性能的關鍵之一,日志表的時間分區(一般是天分區)能夠進行分區級别的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;           
存儲成本一降再降,B站日志平台的演進之路

7、反向索引的使用

除了時間範圍和基于公共字段的過濾條件,常在使用者查詢中出現的過濾條件還包括基于log_msg字段的文字檢索條件,特别是在日志排障場景中,如何根據文字檢索條件進一步縮小需要掃描的資料是支援互動式日志分析的關鍵。

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

1)TokenBloomFilter索引

我們首先擴充Iceberg實作了一個輕量級的TokenBloomFilter索引,支援在Iceberg檔案級别對索引字段先分詞,分詞後生成BloomFilter索引。BloomFilter資料結構占用空間小,非常适合針對低頻詞的檔案檢索。

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

2)TokenBitMap索引

TokenBitMap索引主要是基于著名開源文字檢索架構Lucene的一些基礎能力實作,并沒有直接使用Lucene索引,這主要基于如下考慮:

  • 日志排障是典型的精确文字檢索場景,日志平台需要精确傳回所有滿足使用者檢索條件的資料,不需要打分,排序,同義詞等能力,Lucene作為比較全能的文字檢索架構,對于精确文字檢索場景備援的能力會帶來額外的代價;
  • Iceberg日志資料在文字檢索場景下主要用于曆史日志資料的排障,通路相對低頻,我們更關注在低存儲成本下加速查詢性能,Lucene索引的存儲成本過高,有時甚至索引檔案大小超過資料檔案本身;
  • 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,低頻詞/中頻詞/高頻詞的劃分通過參數控制,可以根據實際日志資料情況靈活調整。

3)反向索引的性能測試

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

低詞頻查詢:

select count(*) from test01 where has_token(log_msg, '1666505943110300001');           
存儲成本一降再降,B站日志平台的演進之路

中詞頻查詢:

select count(*) from test01 where has_token(log_msg, '1978979513');           
存儲成本一降再降,B站日志平台的演進之路

高詞頻查詢:

select count(*) from test01 where has_token(log_msg, '1664553600');           
存儲成本一降再降,B站日志平台的演進之路

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

8、進一步的探索

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

  • 支援基于map_keys(col)/map_values(col)表達式建立索引,此索引可以用于常見Map類型過濾條件element_at的Data Skipping,例如對于過濾條件element_at(col, 'key1') = 'v1', 可以首先使用基于map_keys(col)生成的索引判斷‘key1‘是否在檔案中存在,然後使用基于map_values(col)的索引判斷‘v1’是否在檔案中存在;
  • 如果使用者日志查詢隻會經常使用某一個key值做過濾,則可以直接基于element_at(col, 'key1')表達式建立索引,隻從Map中抽取‘key1’對應的value建構索引,進而減少索引大小,提升索引過濾效果;
  • 支援基于json_scalar_extract($json_path)表達式建立索引,使用者可以使用此方式從json字段中抽取常見内部字段建構索引,在查詢時,如果使用對應json路徑抽取的字段作為過濾條件,則可以通過索引判斷是否可以跳過掃描檔案。

9、計算下推

目前log-agent主要以實體機部署為主,即B站幾乎所有機器上都部署了log-agent服務。目前log-agent支援多種input/processor/output等。

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

10、消費排程

1)旨在解決的問題

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

  • log-agent可以根據AppID+StreamID路由規則進行排程到不同的log-gateway叢集。預設情況下,高優日志進入kafka-proxy-high叢集,沒有特殊要求的日志進入到日志大叢集(絕大部分日志都在這個叢集), 另外有特殊場景要求的,比如極高優要求完全不想被其他人影響的,值得專門部署一套鍊路的,我們也支援專用叢集,但原則上我們盡量會避免,因為這在資源使用率上并不會有很好的效果。對于出現任意叢集出現不穩定時,我們優先會考慮對叢集快速彈性的擴容(log-gateway是無狀态的), 當擴容不能解決問題時,我們可以快速将該叢集的流量一部分或所有切到其他叢集中;
  • log-gateway可以根據AppID+StreamID次元路由規則進行排程到不同的kafka叢集。同樣我們把kafka分成了高優/專用/通用大叢集,絕大部分日志會進通用大叢集。由于kafka是一個有狀态服務,加之其相關設計實作彈性擴縮容能力并不太理想。在這個層面我們會優先把相關日志流排程到其他叢集,同時配合下遊log-consumer的擴容;
  • kafka topic層面我們同樣采用大+小的方式,對于一些特别大,或優先級高的我們會拆分單獨的topic(這裡提一點在我們的架構下,把一個或多個流拆分到其他topic是很簡單的事情);對于一般的日志流我們會根據資源使用相對均勻得拆到到N個topic裡面。采用大+小的主要是成本+容災之間的tradeoff;
  • log-consumer同樣是一個無狀态服務,采用golang編寫,容器化部署,整體資源使用率比同樣場景的flink至少少50%。可以實作友善的彈性伸縮,同時可以根據路由規則動态消費不同的topic以實作充分的資源均衡利用。

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

2)打通大資料體系

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

①批處理場景分區送出

這個政策是基于Kafka消費延遲和寫入延遲的雙重名額來動态送出Hive分區。

  • 監控寫入程式的消費延遲:這是初始步驟,需要計算日志的上報時間和寫入存儲的時間差,這樣就可以得到日志在實際被寫入之前的延遲時間。這是一項關鍵的度量,因為它可以了解資料從接收到實際寫入存儲的耗時。
  • 監控 Kafka 的消費 lag:觀察到資料消費存在延遲時,對比消費延遲時間和消費端的吞吐量,可以預估出一個延遲資料被消費掉的時間。
  • 結合寫入延遲和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 是基于檔案級别進行統計的,是以我們可以在檔案級别擷取到對應的分區資訊。

11、日志聚類

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

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

  • 聚類過程需要盡可能快,而且結果應非常穩定。換言之,聚類的類别和結果不應有波動。
  • 需要能夠保證日志模式的一緻性,以便在不同的時間段内,通過日志類别檢視其波動和變化。

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

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

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

存儲成本一降再降,B站日志平台的演進之路

日志模式解析過程可以了解為是一個倒推日志列印代碼的過程,也是一個對日志聚類的過程(相同pattern的日志認為是同一類日志)。

算法思路設計:

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

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

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

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

預處理的擷取日志平台表達式查詢後的全部日志資料,(對于超過10w條的日志進行采樣)在對日志進行解析前,都會先進行分詞,因為詞是表達完整含義的最小機關,将一些特殊詞,如IP位址、時間等給識别出來,

然後替換為特殊字元或去掉,這是由于這些特殊詞明顯是參數,如此處理可以有效提高相同模式日志的相似度。提取日志消息對應的 日志行号特征資料

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

  • 根據日志的長度分組和日志行号等以及根據日志的前幾個單詞分組,樹深度決定了用前多少個單詞進行分組。
  • 解析樹的上層節點以日志行号特征和日志消息的長度(token的數量)區分日志組,根據預處理後的日志消息前幾個單詞依次向下搜尋,直到葉子節點。葉子節點下存儲着該組别中的聚類簇,

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

相似度計算邏輯如下, 在找到simSeq最大的日志組後,将其與自适應的相似度門檻值st進行比較,如果simSeq≥st,那麼就會傳回該組作為最佳比對。

存儲成本一降再降,B站日志平台的演進之路
  • 更新解析樹,将每個日志消息解析為字段,并按照固定深度樹的結構進行插入。每個字段都對應樹中的一個節點,如果節點已存在,則更新節點的統計資訊;如果節點不存在,則建立新節點,對于比對上的子pattern。

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

  • 層次融合,對于相似的pattern進行融合,結合LCS(最大公共子序列)的思路進行融合,将改善聚類效果,比如使同一行号下不同的pattern和不同行号特征下的子pattern聚類得到公共pattern。
  • 模型儲存與推理,聚類後的模型按appid進行儲存,在後續實時日志聚類推理過程中,将直接日志消息與模型的解析流程進行比對,未比對上的日志将實時更新聚類的模型。

下面是日志聚類的效果:

存儲成本一降再降,B站日志平台的演進之路

四、整體收益

綜上所述,通過我們對日志系統的持續演進, 進一步降低了存儲成本(至少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/)

作者丨 季俊宇&李銳

來源丨公衆号: 哔哩哔哩技術 (ID:bilibili-TC)

dbaplus社群歡迎廣大技術人員投稿,投稿郵箱:[email protected]

關于我們

dbaplus社群是圍繞Database、BigData、AIOps的企業級專業社群。資深大咖、技術幹貨,每天精品原創文章推送,每周線上技術分享,每月線下技術沙龍,每季度Gdevops&DAMS行業大會。

關注公衆号【dbaplus社群】,擷取更多原創技術文章和精選工具下載下傳