把ES換成ClickHouse,B站的日志系統像開掛了一樣……
一、背景介紹
日志作為線上定位問題排障的重要手段,在可觀測領域有著不可替代的作用。穩定性、成本、易用性、可擴展性都是日志系統需要追求的關鍵點。
B站基于Elastic Stack的日志系統(Billions) 從2017建設以來, 已經服務了超過5年,目前規模超過500臺機器,每日寫入日志量超過700TB。
ELK體系是業界最常用的日志技術棧,在傳輸上以結合規范key的JSON作為傳輸格式,易于多種語言實現和解析,并支持動態結構化字段。存儲上ElasticSearch支持全文檢索,能夠快速從雜亂的日志信息中搜尋到關鍵字。展示上Kibana具有美觀、易用等特性。
隨著業務系統的高速發展,日志系統的規模也隨之快速擴展,我們遇到了一系列的問題,同時可觀測業界隨著OpenTelemetry規范的成熟,推動著我們重新考量,邁入下一代日志系統。
二、遇到的問題
1)首先必須要提的就是成本和穩定性。日志作為一種應用產生的實時數據,隨著業務應用規模發展而緊跟著擴大。日志系統必須在具備高吞吐量的同時,也要具備較高的實時性要求。Elasticsearch由于分詞等特性,在寫吞吐量上有著明顯的瓶頸,分詞耗CPU且難以解決熱點問題。如果資源冗余不足,就容易導致穩定性下降,日志攝入發生延遲,日志的延遲會對排障產生極大負面影響。
2)同時由于壓縮率不高的原因,ES的存儲成本也較高,對內存有著較高的要求。這些因素導致我們日志必須進行常態化的采樣和限流,對用戶使用上造成了困擾,限制了排障的場景。
3)內存使用率的問題也迫使我們必須將warm階段的索引進行Close,避免占用內存。用戶如果需要查詢就必須操作進行Open,犧牲了一定的易用性。
4)為了穩定性和成本,動態Mapping也必須被關閉,有時用戶引導不到位,就會導致用戶發現自己搜索的日志遺留了Mapping配置而導致難以追溯查詢。
5)在運維上,ES7之前缺少生命周期的能力,我們必須維護一整套生命周期相關組件,來對索引進行預創建、關閉和刪除,不可避免的帶來高維護成本。
6)Kibana雖然好用,但也不是沒有缺點的,整體代碼復雜,二次開發困難。且每次升級ES必須升級到對應的Kibana版本也增加了用戶遷移的成本。還有一點就是Kibana Query雖然語法較為簡單,但對于初次接觸的研發還是有一定學習成本的。
7)在采集和傳輸上,我們制定了一套內部的日志格式規范,使用JSON作為傳輸格式,并提供了Java和Golang的SDK。這套傳輸格式本身在序列化/反序列化上性能一般,且私有協議難以避免兼容性和可維護性問。
三、新架構體系
針對上述的一系列問題,我們設計了Bilibili日志服務2.0的體系,主要的進化為使用ClickHouse作為存儲,實現了自研的日志可視化分析平臺,并使用OpenTelemetry作為統一日志上報協議。
如圖所示為日志實時上報和使用的全鏈路。日志從產生到消費會經過采集→攝入→存儲 →分析四個步驟,分別對應我們在鏈路上的各個組件,先做個簡單的介紹:
- OTEL Logging SDK
完整實現OTEL Logging日志模型規范和協議的結構化日志高性能SDK,提供了Golang和Java兩個主要語言實現。
- Log-Agent
日志采集器,以Agent部署方式部署在物理機上,通過Domain Socket接收OTEL協議日志,同時進行低延遲文件日志采集,包括容器環境下的采集。支持多種Format和一定的加工能力,如解析和切分等。
- Log-Ingester
負責從日志kafka訂閱日志數據, 然后將日志數據按時間維度和元數據維度(如AppID) 拆分,并進行多隊列聚合, 分別攢批寫入ClickHouse中.
- ClickHouse
我們使用的日志存儲方案,在ClickHouse高壓縮率列式存儲的基礎上,配合隱式列實現了動態Schema以獲得更強大的查詢性能,在結構化日志場景如猛虎添翼。
- Log-Query
日志查詢模塊,負責對日志查詢進行路由、負載均衡、緩存和限流,以及提供查詢語法簡化。
- BLS-Discovery
新一代日志的可視化分析平臺,提供一站式的日志檢索、查詢和分析,追求日志場景的高易用性,讓每個研發0學習成本無障礙使用。
下邊我們將針對幾個重點進行詳細設計闡述。
1、基于ClickHouse的日志存儲
新方案最核心的部分就是我們將日志的通用存儲換成了ClickHouse。
先說結果,我們在用戶只需要付出微小遷移成本的條件下(轉過來使用SQL語法進行查詢),達到了10倍的寫入吞吐性能,并以原先日志系統1/3的成本,存儲了同等規模量的日志。在查詢性能上,結構化字段的查詢性能提升2倍,99%的查詢能夠在3秒內完成。
下圖為同一份日志在Elasticsearch、ClickHouse和ClickHouse(zstd)中的容量,最終對比ES達到了1:6。
ClickHouse方案里另一個最大的提升是寫入性能,ClickHouse的寫入性能達到了ES的10倍以上。
在通用結構化日志場景,用戶往往是使用動態Schema的,所以我們引入了隱式列Map類型來存儲動態字段, 以同時獲得動態性和高查詢性能,稍后將會重點介紹隱式列的實現。
我們的表設計如下,我們針對每個日志組,都建立了一張復制表。表中的字段分為公共字段(即OTEL規范的Resource字段, 以及trace_id和span_id等),以及隱式列字段,string_map,number_map,bool_map分別對應字符串字段,數字字段和布爾字段。我們對常用的日志值類型進行分組,使用這三個字段能滿足大部分查詢和寫入的需求。
同時,我們根據日志重要程度和用戶需求定義了不同時間范圍的TTL。根據統計大部分(90%)的日志查詢集中在4小時以內,我們為日志制定了三個階段的日志生命周期,Hot,Warm和Cold.Hot階段,所有日志在高速存儲中,保證高寫入和檢索性能,通常我們的資源保證24小時日志數據會在此階段中;Warm階段,日志遷移到Sata盤中,同樣可以進行檢索,通常需要更長時間;Cold階段,在達到了第二個TTL時間后,在ClickHouse中刪除騰出空間,如果有需求會在HDFS中有備份。
對于大部分的字段,我們都使用了ZSTD(1)的壓縮模式,經過測試相比默認的Lz4提升了50%的壓縮率,在寫入和查詢性能上的代價不超過5%,適合日志寫多讀少的場景。
2、查詢網關
查詢網關承擔著查詢入口的任務。我們主要在這個組件上集成了查詢路由,查詢負載均衡,簡化查詢語法,緩存,限流等功能。
這里先介紹下簡化查詢語法的功能。作為日志對前端以及對外的接口,我們的目標是對用戶屏蔽一些底層復雜實現。如ClickHouse的Local表/分布式表,隱式列和公共字段的查詢區別,以及對用戶查詢進行限制(強制Limit,強制時間范圍)。
上圖可見我們允許用戶在編寫查詢SQL時,不需要關心字段是否為隱式列,也不需要關心目標表和集群,以最簡單的方式通過Restful API與日志查詢網關進行交互獲取日志數據。這樣的實現還有個目的就是為以后可能再一次的存儲引擎迭代做鋪墊,將日志查詢與底層實現解耦。
此外,在查詢網關上還集成了Luence語法的解析器,將其自動轉化為SQL語法,為用戶的API遷移提供便利。
3、可視化分析平臺
在升級日志架構的過程中,很多用戶反應希望能夠延續之前的日志使用方式,盡量減少遷移學習的成本。我們考慮了讓Kibana兼容ClickHouse作為存儲引擎。然而由于我們同時維護中Kibana5和7兩個版本,且越高版本的Kibana功能豐富,與Elastic Stack綁定過深,我們決定還是自研日志可視化查詢分析平臺。
我們的目標是:
- 提供接近Kibana使用習慣的界面,減少用戶的遷移成本;
- 作為排障的入口,和內部其他組件打通,如監控告警,分布式追蹤等。
緊記著上面兩個關鍵目標,我們開始了自研之路。
Kibana作為非常成熟的日志分析界面,具有非常多的細節,都是在使用過程中沉積下來的功能。任何一個功能用戶都有不低的使用頻率。如圖所示我們能實現了查詢語句高亮和提示、字段分布分析、日志時間分布預覽、查詢高亮以及日志略縮展示等等。
用戶在查詢日志時, 使用標準SQL的Where condition部分, 如 log.level = 'ERROR' 進行日志過濾, 并對用戶將隱式列和具體的表透明化(通過查詢網關的能力)。
我們還使用code-mirror2實現了查詢的自動補全和查詢提示。提示的內容包括常用歷史查詢, 字段, 關鍵字和函數, 進一步降低用戶的使用門檻。
在此基礎之上,我們開發了能夠領先于Kibana的使用體驗的殺手锏,我們自研的可視化分析平臺集成了快速分析能力。用戶可以直接輸入SQL聚合語句,即時對日志進行聚合分析。
同時我們可以將查詢分析界面作為一個入口,打通相關信息和功能。如日志告警快速的快速配置、日志寫入量統計和優化點、快速配置二級索引、快速跳轉分布式追蹤平臺等。
這些都是為了能讓原先使用Kibana的用戶能夠在無縫地切換到新平臺上來使用的基礎上,擁有更好的排障體驗。
4、日志告警
除了在日志分析界面上人為進行日志查詢排障外,日志監控規則也是常用且好用的快速感知系統問題的手段。
在日志服務2.0的版本中,日志告警服務在兼容了日志ClickHouse作為數據源的基礎上,將計算模型進行了統一化,剝離了原先Elasticsearch場景的特有語義,使得計算和觸發規則更靈活,配置更容易。
我們將一個日志告警規則定義為由以下幾個屬性組成:
- 名稱和數據源;
- 查詢時間范圍,如1分鐘;
- 計算間隔,如1分鐘;
- 計算函數,如日志計數,日志字段和,日志字段最大值最小值,日志字段去重計數等;
- 日志過濾規則,如region = 'sh' AND log.level = 'ERROR';
- 觸發條件,如 大于/小于,環比上升/下降,基于智能時序異常檢測等;
- 告警通道,如電話,短信,郵件等;
- 告警附帶信息,如固定文案,關鍵日志等;
- 通知對象,一般為可配置的Group(可以結合Oncall切換人員),或者可以是固定的人;
- 告警風暴抑制相關,如告警間隔時間,連續觸發規則等。
目前B站系統中目前已配置超過5000+的日志告警,我們提供了告警遷移工具,能夠自動通過原ES語法的規則,生成對應的2.0版本過濾規則,來幫助用戶快速遷移。
5、OpenTelemetry Logging
原OpenTracing[1]和OpenCensus[2]項目已經合并入OpenTelemetry項目,從趨勢和未來考慮,新項目不再推薦使用前述兩個項目,建議直接使用Opentelemtry通用api收集可觀測數據。
Opentelemetry[3]是一套工具集,專注于可觀測性領域的數據收集端,致力于可觀測性3大領域metrics、logs、traces的通用api規范以及支持編程語言的sdk實現。除了嵌入用戶代碼的sdk ,Opentelemetry也提供了可觀測性數據收集時的收集器[4]實現,該收集器可作為sidecar,daemonset,proxy 3種形式部署,支持多種可觀測協議的收集和導出。
目前,OTEL Logging的協議已經處于stable,主要定義了以下的標準日志模型。
我們完整的按照OTEL的標準實現了Golang和Java的SDK,并在采集器(Log-Agent)集成了OTEL兼容層。
6、如何解決日志搜索問題
得益于ClickHouse的高壓縮率和查詢性能,小日志量的應用日志直接可以搜索即可。在大日志量場景,對于某種唯一id的搜索,使用tokenbf_v1建立二級索引,并引導用戶使用hasToken)或通過上文描述的~`操作符進行搜索,跳過大部分的part,能獲得不亞于ES的查詢性能。對于某種日志模式的搜索,引導用戶盡可能使用logger_name,或者source(代碼行號)來進行搜索 同時盡量減少需要搜索的日志范圍。在此基礎上,我們還必須推進日志結構化。
一開始的日志往往是無結構化的,人可讀的。隨著微服務架構發展,日志作為可觀測性的三大支柱,越來越需要關注日志的機器可讀性。統一的日志平臺也對日志的結構化有要求,來進行復雜聚合分析。
ClickHouse方案中,由于缺少倒排索引,對日志結構化程度的要求會更高。在推進業務遷于新方案時,我們也需要同步進行結構化日志的推進。
對于這樣一段常見日志:
log.Info("report id=32 created by user 4253)"
結構化我們可以抽取report id和user id,作為日志屬性獨立輸出,并定義該段日志的類型,如:
log.Infov(log.KVString("log_type","report_created"),log.KVInt("report_id",32), log.KVInt("user_id",4253))
結構化后對于整體存儲和查詢性能上都能獲得提升,用戶可以更方便的對user id或report id進行搜索或分析。
四、Clickhouse功能增強與優化
在日志場景中,我們選用Clichouse作為底層的查詢引擎,主要原因有兩個:一個是Clichouse相較于ES具備更高的資源利用效率,在磁盤,內存方面的消耗都更低;另外一個就是Clichouse支持的豐富的數據和索引類型能夠滿足我們針對特定pattern的查詢性能需求。
1、Clickhouse配置優化
因為日志的數據量比較大,在實際的接入過程中我們也是遇到了一些問題。
1)Too many parts
這個問題在clickhouse寫入過程中還是比較常見的,根因是clickhouse merge的速度跟不上part的 生成速度。這個時候我們的優化方向可以從兩個方面層面考慮,寫入側和clickhouse側。
在日志的寫入側我們降低寫入的頻次,通過攢batch這種方式來降低clickhouse part的生成速度。缺點是這種攢批會導致數據的時延會增高,對于實時查詢的體感較差。而且,如果寫入只由batch大小控制,那么在數據斷流的時候就會有一部分數據無法寫入。因為日志場景下對數據查詢的時效性要求比較高,所以我們除了設置batch大小還會有一個超時時間的配置項。當達到這個超時時間,即便batch大小沒有達到指定大小,也會執行寫入操作。
在寫入側配置了滿足業務需求的參數配置之后,我們的part生成速度依然超過了merge的速度。所以我們在clickhouse這邊也進行了merge相關的參數配置,主要目的是控制merge的消耗的資源,同時提升merge的速度。
Merge相關的參數,我們主要修改了以下幾個:
min_bytes_for_wide_part
這個參數是用來指定落盤的part格式。當part的大小超過了配置就會生成wide part,否則為compact part。日志屬于寫多讀少的一個場景,通過增大這個參數,我們可以讓頻繁生成的part在落盤時多為compact part。因為compact part相較于wide part小文件的個數要更少一些,測試下來,我們發現相同的數據量,compact part的merge速度要優于wide part。相關的還有另一個參數min_rows_for_wide_part,作用跟min_bytes_for_wide_part相似。
max_bytes_to_merge_at_min_space_in_pool
當merge的線程資源比較緊張時,我們可以通過調整這個參數來配置可merge part的最大大小。默認大小是1M,我們將這個參數上調了。主要的原因是,我們在頻繁寫入的情況下,merge資源基本處于打滿的狀態。而寫入的part大小基本也都超過了1M,此時這些part就不被merge,進而導致part數據不斷變多,最終拋出Too many parts的問題。
max_bytes_to_merge_at_max_space_in_pool
這個參數是用來指定merge線程資源充足時可merge的最大part大小。通過調整這個參數,我們可以避免去merge一些較大的part。因為這些part的合并耗時可能是小時級別的,如果在這期間有較為頻繁的數據寫入,那就有可能會出現merge線程不夠而導致的too many parts問題。
background_pool_size
默認線程池大小為16,我們調整為32,可以有更多的線程資源參與merge。之所以沒有繼續上調這個參數,是因為較多的merge線程可能會導致系統的CPU和IO負載過高。
2)Zookeeper負載過高
Clickhouse的副本表需要頻繁的讀寫clickhouse,熟悉clickhouse的用戶都知道zookeeper集群是clickhouse集群的性能瓶頸之一。因為zookeeper的寫壓力是沒法通過增加節點數得到緩解,所以當一個集群的寫請求過于頻繁時,zookeeper就可能成為我們寫入失敗的主要原因了。我們的日志系統為了能夠在服務級別進行優化和數據TTL設置,就將每個接入的服務通過不同的表進行隔離。所以接入的服務越多,對應的表和分區就會越多。而分區和part過多,就會導致整個集群對zookeeper的讀寫請求頻次增加,進而導致zookeeper集群負載過高。針對這個問題,社區已經有相關的PR解決了,那就是auxiliary zookeeper。這個特性讓我們可以在一個clickhouse集群中配置多個zookeeper集群,配置好之后我們只需要在建分布式表時指定對應的zookeeper集群即可。這樣,zookeeper寫請求過于頻繁的問題也能得到很好的解決了。
2、Clickhouse動態類型Map
Map類型作為數據庫中一個重要的數據類型,能夠很好地滿足用戶對于表動態schema的需求。對于后期可能會動態增減的字段,或者因為數據屬性而不同的字段,我們可以將其抽象成一個或多個map存儲,使用不同的k-v來存儲這些動態字段。Clickhouse的在版本v21.1.2.15將Map類型作為一個實驗特性增加到支持的數據類型之中。而Map類型在B站的日志系統中解決的就是日志系統中不同服務具有不同的特有字段問題。使用Map字段一方面可以統一不同服務的表結構,另一方面使用Map能夠更好地預設表的schema,避免了后期因業務新增導致表結構頻繁變更的問題, 降低了整個日志鏈路的復雜程度。
但是隨著數據體量的增加和查詢時間跨度的延伸,針對clickhouse原生map類型的查詢和過濾效果越來越不如人意,雖然clickhouse目前支持的map類型在功能上能夠滿足我們的需求,但是性能上卻依然有提升的空間。
3、Clickhouse Map實現原理
首先我們可以看看原生Map的實現。原生的Map是通過Array(Tupple(key, value))這種嵌套的數據結構來保存Map的數據。當我們讀取某個指定key值的數據時,Clickhouse需要將指定的對應的ColumnKey和ColumnValue都讀取到內存中進行反序列化。
假設原始表的數據如下表所示:
當我們需要查詢k1對應的數據時,select map[’k1’] from map_table,那么每一行除了key值為k1的數據對我們而言都是不需要的。但是,在Clickhouse反序列化時,這些數據都會被讀取到內存之中,產生了不必要的計算和I/O。而且因為Clickhouse不支持Map類型的索引,這種讀放大造成的損耗在數據體量很大的情況下對查詢性能有很大的影響。
通過上面的例子,可以看出,原生Map對于我們而言主要有兩個瓶頸:
- Clickhouse不支持Map類型索引,沒法很好地下推過濾掉不包含指定key值的數據;
- 同一個Map中不需要的kv數據也會在查詢時讀取到內存中處理。
4、Map類型索引支持
針對上面的問題,我們的首次嘗試是對Clickhouse索引進行加強,使其可以支持Map類型。因為我們的場景中key值多為String類型,所以我們優先改造了bloom filter相關的索引。在數據寫入的時候我們會把每行數據的map都單獨拎出來處理,獲取到每一個key值,對key值創建索引。查詢時,我們會解析出需要查詢的key值,索引中不包含該值時則會跳過對應的granule,以達到盡量少讀取不必要數據的目的。
通過上述方式,我們在執行select map[’k1’] from map_table時,可以將未命中索引的granule都過濾掉,這樣可以在一定程度上減少不必要的計算和I/O。
創建測試表:
CREATE TABLE bloom_filter_map
(
`id` UInt32,
`map` Map(String, String),
INDEX map_index map TYPE tokenbf_v1(128, 3, 0) GRANULARITY 1
)
ENGINE = MergeTree
ORDER BY id
SETTINGS index_granularity = 2
----插入數據
insert into bloom_filter_map values (1, {'k1':'v1', 'k2':'v2'});
insert into bloom_filter_map values (2,{'k1':'v1_2','k3':'v3'});
insert into bloom_filter_map values (3,{'k4':'v4','k5':'v5'});
----查詢數據
select map['key1'] from bloom_filter_map;
通過query_id可以查看到只查詢了兩行數據,索引生效
通過上述的實現在索引粒度設置合適的情況下我們能夠有效地下推過濾掉部分非必要的數據。這部分的源碼我們已經貢獻到社區,相關PR[5]。
雖然支持了Map類型的索引能夠過濾部分數據,但是當map的key分布地比較稀疏時,例如上例中的k1,如果在每個granule中都出現的話我們還是會讀取很多無效的數據,同時,一個map中的我們不需要的kv我們還是沒法跳過。為了解決這個問題,我們實現了Clickhouse Map類型的隱式列。
5、Map隱式列簡介
隱式列就是在Map數據寫入的時候,我們會把map中的每個key單獨抽出來在底層作為一個Column存儲。通過這種對用戶透明的轉換,我們可以在查詢指定key時讀取對應的column,通過這種方式來避免讀取需要的kv。
通過這種轉換,當我們再執行類似select map[’k1’] from map_table這種SQL時,Clickhouse就只會去讀取column_k1這個Column的數據,能夠極大地避免讀放大的情況。
考慮到已有的業務,我們并沒有直接在原生的Map類型上進行改造,而是支持了一種新的數據類型MapV2,底層則是實現了Map到隱式列的轉換。
6、Map隱式列實現
1)數據寫入
隱式類的寫入格式跟原生Map一致。寫入時主要是在Map的反序列化過程中加入了一個構造隱式列的流程,主要的功能就是檢查map中的每一個key值。如果key值對應的Column存在則將value寫入到存在的Column中。如果Column不存在,就會創建一個新的Column,然后將value插入。需要注意的就是,每個隱式的Column行數需要保持一致,如果無值則需要插入默認值。
在數據寫入的過程中,每個part對應的columns.txt文件,我們額外追加隱式列的信息。這樣在加載part信息的時候,clichouse就可以通過loadColumns方法把隱式列的信息加載到part的元信息之中了。
2)數據查詢
查詢流程我們沒有做過多的改造, 就是在Parser層就的把select map[’k1’] from map_table轉換成針對隱式列的查詢,接下把它當做普通的Column查詢即可。
7、查詢測試
創建測試表:
---- 隱式列
CREATE TABLE lt.implicit_map_local
(
`id` UInt32,
`map` MapV2(String, String)
)
ENGINE = MergeTree
ORDER BY id
SETTINGS index_granularity = 8192;
---- 原始map類型
CREATE TABLE lt.normal_map_local
(
`id` UInt32,
`map` Map(String, String)
)
ENGINE = MergeTree
ORDER BY id
SETTINGS index_granularity = 8192;
測試數據通過jdbc寫入,自動生成10000000條數據,數據完全相同。
測試簡單的指定key值查詢。
此次測試中Map類型隱式列對于指定key值的查詢場景有顯著的性能提升。
Map類型是我們目前解決動態schame最優的選擇,原生Map類型的讀放大問題在通過我們的優化之后能夠一定緩解。而隱式列的實現則讓我們可以像對待普通列一樣,極大地避免了讀取非必要的map數據。同時,我們后面也加上了對隱式列的二級索引支持,進一步增加了優化讀取的手段。
當然,隱式列也有一些不適用的場景。
- 不支持select整個map字段
因為Map被拆成了多個column存儲,所以無法支持select *或者select map這樣的查詢。當然我們也進行過嘗試。主要實現過兩個方案:其一是冗余存儲一份Map數據,按照原來的格式查詢,當需要select整個map字段時我們就按原來的流程讀取。這樣查詢性能跟原生的Map一樣;還有一個方案就是在查詢時讀取Map對應的所有隱式列,然后實時生成Map返回,這種方案經過測試之后發現性能太差,最終也被放棄。
- 不適用于map key值非常稀疏的場景
通過以上幾個部分的講解,我們知道clickhouse的隱式列實際上就是把map中的key拆出來作為單獨的columns存儲。在這個情況下,如果用戶的map字段中的key值基數過高就會導致底層存儲的列過多。針對這種情況,我們是增加了一個max_implicit_columns的MergeTree參數,通過這個參數我們可以控制隱式列的個數,當超過閾值時會直接返回錯誤信息給用戶。在我們的日志系統的使用實踐手冊中我們會引導用戶按照規范創建key值,避免key的基數過高的情況出現。如果有不按照約束寫入的用戶,max_implicit_columns這個參數也會對其進行限制。
針對于第一個問題,我們也是實現了一套退而求其次的方案,那就是通過select每一個隱式列來實現。為此,我們會將每個part的隱式列都寫到系統表system.parts中。當用戶不知道有哪些key值時則可以通過查詢系統表獲取。
日志場景下,在功能上隱式列能夠滿足其對于動態schema的需求。同時,在查詢時,用戶一般都會勾選特定的字段。而在這種指定字段的查詢上,隱式列的查詢性能夠做到與普通列保持一致。而且,因為隱式列能夠像普通列一樣配置二級索引,對于一些對性能要求更加極致的場景,我們的優化手段也要更加的靈活一些。
五、下一步的工作
1、日志模式提取
目前雖然我們通過日志最佳實踐和日志規范引導用戶盡可能繼續結構化日志的輸出,但是仍然不可避免部分場景難以進行結構化,用戶會將大段文本輸入到日志中,在分析和查詢上都有一定受限。我們計劃實現日志模式并使用在查詢交互,日志壓縮, 后置結構化,異常模式檢測這些場景上。
2、結合湖倉一體
在湖倉一體日益成熟的背景下,日志入湖會帶來以下收益:
部分日志有著三年以上的存儲時間要求,比如合規要求的審計日志,關鍵業務日志等,數據湖的低成本存儲特性是這個場景的不二之選。
現在日志除了用來進行研發排障外,也有大量的業務價值蘊含其中。日志入湖后可以結合湖上的生態體系,快速結合如機器學習、BI分析、報表等功能,將日志的價值發揮到最大。
此外,我們長遠期探索減少日志上報的中間環節,如從agent直接到ClickHouse,去掉中間的Kafka,以及更深度的結合ClickHouse和湖倉一體,打通ClickHouse和iceberg。
3、Clickhouse全文檢索
在全文檢索場景下,Clickhouse的性能表現依舊與ES有一定的差距,為了能夠覆蓋日差查詢的全場景,clickhouse在這方面的探索依舊任重道遠。
在全文檢索索引的實踐上,我們也需要嘗試不同的數據結構,極力做到內存資源和查詢性能的雙重保證。