動態

詳情 返回 返回

告別ELK,APO提供基於ClickHouse開箱即用的高效日誌方案——APO 0.6.0發佈 - 動態 詳情

ELK一直是日誌領域的主流產品,但是ElasticSearch的成本很高,查詢效果隨着數據量的增加越來越慢。業界已經有很多公司,比如滴滴、B站、Uber、Cloudflare都已經使用ClickHose作為ElasticSearch的替代品,都取得了不錯的效果,實現了降本增效,費用節約大多在50%以上。但是目前使用ClickHose作為日誌方案,存在以下問題。

  • 主流的Vector+ClickHose並未實現開箱即用,有許多的管理配置工作
  • 絕大多數方案不支持近似全文檢索的功能(該功能很重要)
  • 使用雙數組或者Map的表結構查詢效率不高
  • ClickVisual是最接近的開箱即用的日誌方案,也存在以下問題:
    ○強依賴Kafka,對於某些中小用户而言方案不夠靈活,不友好
    ○未引入Vector,原生的ClickHose Kafka引擎在大流量情況下可能導致ClickHose內存爆掉(感謝社區大佬 十四反饋)

主流的Vector+ClickHouse方案並未實現開箱即用

目前業界很多公司都是基於Vector+ClickHouse的方案來實現日誌的採集和存儲,該方案需要管理維護的工作量相對而言比較高,適用於動手能力強的公司。

維護工作:為每種日誌手動維護一張表

每個公司的部門團隊可能日誌規範都不完全一致,如果需要對日誌內容進行快速搜索定位故障,就需要提前想好ClickHouse的表結構,然後調整Vector的配置文件,最終實現Vector根據不同日誌格式,parse成不同的日誌表字段,寫入不同的日誌表。
比如每種日誌都得建立以下類似的表結構,才能完成日誌按照ip、url等字段的索引實現快速搜索。但是另外一個部門的日誌也許就不需要IP和url字段,那麼該部門得重新設計表結構。

CREATE TABLE log
(
    `ip` String,
    `time` Datetime,
    `url` String,
    `status` UInt8,
    `size` UInt32,
    `agent` String
)
ENGINE = MergeTree
ORDER BY date(time)

使用雙數組或者Map的表結構查詢效率不高

為了能夠規避這些維護工作,所以很多公司對固定日誌表結構進行了調整,常見的有兩種方案,一種是雙數組方案,另外一種就是Map方案。

Uber和Signoz的日誌實現方案都是基於雙數組

其日誌表結構類似於下面這種

CREATE TABLE <table_name>
(
      //Common metadata fields.
      _namespace             String,
      _timestamp              Int64,
      hostname               String,
      zone                   String,
      ...

     //Raw log event.
     _source                 String,

    //Type-specific field names and field values.
    string.names             Array(String),
    string.values            Array(String),
    number.names             Array(String), 
    number.values            Array(Float64),
    bool.names               Array(String),
    bool.values              Array(UInt8),

    //Materialized fields
    bar.String,              String
    foo.Number               Float64,
   ...
)
...

滴滴、B站等日誌實現是基於Map結構

引入Map結構能夠動態實現日誌關鍵字段搜索

CREATE TABLE ck_bamai_stream.cn_bmauto_local
(
    `logTime` Int64 DEFAULT 0, --Log打印的時間
    `logTimeHour` DateTime MATERIALIZED toStartOfHour(toDateTime(logTime / 1000)),--將Log
    `odinLeaf` String DEFAULT '',
    `uri` LowCardinality(String) DEFAULT '',
    `traceid` string DEFAULT '',
    `cspanid` String DEFAULT '',
    `dltag` String DEFAULT '',
    `spanid` String DEFAULT '',
    `message` String DEFAULT '',
    `otherColumn` Map<String,String>
    `_sys_insert_time` DateTime MATERIALIZED now()
)
ENGINE =MergeTree
PARTITION BY toYYYYMMDD(logTimeHour)
ORDER BY(logTimeHour,odinLeaf,uri,traceid)
TTL _sys_insert_time +toIntervalDay(7),_sys_insert_time + toIntervalDay(3)To VOLUME 'hdfs
SETTINGS index_granularity = 8192,min_bytes_for_wide_part=31457280
Create Table <log_app_name> ON CLUSTER ...
{
     _timestamp      Datetime64(3),
     `log,level`     String CODC(ZSTD(1)),
     `log.msg`       String CODC(ZSTD(1)),
     `log.trace_id`  String CODC(ZSTD(1)),
     ...
     string_map MapV2(String, Nullable(String))
        CODEC(ZSTD(1))
     number_map MapV2(String, Nullable(Float64))
        CODEC(ZSTD(1))
     bool_map MapV2(String, Nullable(UInt8))
}
ENGIN = ReplicatedMergeTree(...)
PARTITION BY toYYYYMMDD(_timestamp)
ORDER BY timestamp
TTL  toDateTime(timestamp) + toIntervalDay(...),
  toDateTime(timestamp) + toIntervalDay(...) TO VOLUME `cold_volume`

Map的動態字段搜索效率低

https://clickhouse.ac.cn/docs/knowledgebase/improve-map-perfo...

根據社區反饋,map底層實現為線性數組,map查詢效率通常低於列查詢3~10倍,特別是日誌量規模越大,map查詢效率越低。
同時支持Map類型的最低clickhosue版本為21.11

  • 列式存儲優勢:ClickHouse 的核心優勢在於它是列式存儲數據庫,這意味着當執行查詢時,只需要讀取查詢中涉及的列,而不必加載不相關的列。列式存儲還能夠通過數據類型特定的壓縮技術顯著減少 IO 操作,從而加快查詢速度
  • 基於 Map 的查詢:Map 是一種鍵值對數據結構,在查詢時需要額外的開銷來解析嵌套結構,並且無法像列式存儲那樣直接跳過不相關的數據。雖然 ClickHouse 對 Map 數據類型有一些優化,但它在處理複雜結構時往往會比簡單的列查詢慢

雙數組的搜索效率也不高

雖然 ClickHouse 對Array有一定的優化,但雙數組結構仍然比單純的列查詢開銷大。
影響性能的因素:

  • 多級解析開銷:查詢雙數組時,需要進行多層嵌套解析。例如,訪問數組中的子數組意味着需要遍歷父數組,然後進一步解析子數組的結構,這比單純讀取一個列複雜得多
  • 隨機存取:雙數組的訪問模式往往比簡單的列查詢更加隨機化。訪問數組中的元素可能導致更多的跳轉,影響緩存命中率,從而降低性能
  • 內存使用和數據存儲:嵌套數組會使得 ClickHouse 的數據存儲和內存管理更加複雜,因為數組中的數據長度不固定,導致壓縮效果比單純列差,數據塊的大小也更加難以優化

性能上的差距取決於具體的查詢模式和數據結構:

  • 在簡單查詢場景(例如,讀取一個基本的列數據),單純的列查詢會比雙數組快得多,特別是在處理大規模數據時。性能差距可能達到 數倍甚至十倍 以上,尤其是當查詢不涉及嵌套結構時。
  • 在複雜查詢場景(例如,查詢涉及嵌套數組、需要頻繁地進行數組拆解和操作),雙數組的查詢性能通常明顯低於單純的列查詢。查詢雙數組的額外解析和處理開銷,會使查詢時間增加。根據不同的嵌套深度和數據量,性能可能下降 數倍。

ClickHouse的官方文檔中日誌方案也由於引入了Map效率不高

ClickHouse官方blog :

https://ClickHouse.com/blog/storing-log-data-in-ClickHouse-fl...

提到有以下幾種表結構:
OTEL的日誌字段表

CREATE TABLE otel.otel_logs
(
    `Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
    `TraceId` String CODEC(ZSTD(1)),
    `SpanId` String CODEC(ZSTD(1)),
    `TraceFlags` UInt32 CODEC(ZSTD(1)),
    `SeverityText` LowCardinality(String) CODEC(ZSTD(1)),
    `SeverityNumber` Int32 CODEC(ZSTD(1)),
    `ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
    `Body` String CODEC(ZSTD(1)),
    `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    `LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
    //數據索引
    INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1,
    INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_log_attr_key mapKeys(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_log_attr_value mapValues(LogAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
    INDEX idx_body Body TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1

Vector 字段表

CREATE TABLE vector.vector_logs
(
   `file` String,
   `timestamp` DateTime64(3),
   `kubernetes_container_id` LowCardinality(String),
   `kubernetes_container_image` LowCardinality(String),
   `kubernetes_container_name` LowCardinality(String),
   `kubernetes_namespace_labels`  Map(LowCardinality(String), String),
   `kubernetes_pod_annotations`  Map(LowCardinality(String), String),
   `kubernetes_pod_ip` IPv4,
   `kubernetes_pod_ips` Array(IPv4),
   `kubernetes_pod_labels` Map(LowCardinality(String), String),
   `kubernetes_pod_name` LowCardinality(String),
   `kubernetes_pod_namespace` LowCardinality(String),
   `kubernetes_pod_node_name` LowCardinality(String),
   `kubernetes_pod_owner` LowCardinality(String),
   `kubernetes_pod_uid` LowCardinality(String),
   `message` String,
   `source_type` LowCardinality(String),
   `stream` Enum('stdout', 'stderr')
)
ENGINE = MergeTree
ORDER BY (`kubernetes_container_name`, timestamp)

fluent字段表

CREATE TABLE fluent.fluent_logs
(
    `timestamp` DateTime64(9),
    `log` String,
    `kubernetes` Map(LowCardinality(String), String),
    `host` LowCardinality(String),
    `pod_name` LowCardinality(String),
    `stream` LowCardinality(String),
    `labels` Map(LowCardinality(String), String),
    `annotations` Map(LowCardinality(String), String)
)
ENGINE = MergeTree
ORDER BY (host, pod_name, timestamp)

日誌需要近似全文檢索

基於ElasticSearch的日誌方案,由於可以基於ElasticSearch實現的日誌內容分詞,所以很容易實現全文檢索,但是基於ClickHouse就很難實現該功能。
那是不是基於ClickHouse的方案就完全沒有辦法呢?
ClickHouse的索引介紹

  • tokenbf_v1 按非字母數字字符(non-alphanumeric)拆分。相當於按符號分詞,而通常日誌中會有大量符號
    在大牛的文章中,
    https://juejin.cn/post/7130514546069864456
    詳細介紹了全文檢索的實現,有興趣的可以仔細看下大牛的文章。

最理想的日誌方案應該滿足什麼條件?

我們認為理想的基於ClickHouse的日誌方案應該滿足以下幾條:

  • 使用列來進行檢索,而不是map或者雙array,保證高效的查詢效率
  • 用户不需要為了不同部門的日誌內容,進行維護單獨的表結構
  • 支持對原始日誌內容進行近似的全文檢索

我們調研了國內外幾乎所有基於ClickHouse的日誌方案,最後發現國內開源項目ClickVisual項目的思路最相近,ClickVisual幾乎可以做到開箱即用。


ClickVisual的方案不足

ClickVisual工作原理:

  • 針對每種日誌格式定義不同的parse規則,ClickVisual為每種規則生成一張新的日誌表。該日誌表存的就是解析之後的日誌,之後的日誌查詢都是針對該日誌表。因為解析之後的日誌已經按照ClickHouse列存儲了,所以關鍵字段查詢是非常快的
  • 基於Kafka表引擎,讀取Kafka原始日誌,落庫至臨時表中
  • 魔術開始的地方:基於ClickHouse的物化視圖,將原始日誌中的新增日誌_raw_log_內容按照日誌解析規則parse成日誌列格式,並將解析好的日誌存入該規則對應的日誌表中

臨時表

CREATE TABLE default.test_stream
(
    `status` String,
    `timestamp` Float64,
    `message` String CODEC(ZSTD(1))
)
ENGINE = Kafka
SETTINGS kafka_broker_list = '127.0.0.1:9092',
kafka_topic_list = 'test',
kafka_group_name = 'default_test',
kafka_format = 'JSONEachRow', 
kafka_num_consumers = 1,
kafka_skip_broken_messages = 0

物化視圖

CREATE MATERIALIZED VIEW default.test_view TO default.test
(
    `status` String,
    `_time_second_` DateTime,
    `_time_nanosecond_` DateTime64(9),
    `_raw_log_` String,
    // 日誌表的列
    `level` Nullable(String)
    //根據需要調整列
    ...
) AS
SELECT
    status,
    toDateTime(toInt64(timestamp)) AS _time_second_,
    fromUnixTimestamp64Nano(toInt64(timestamp * 1000000000)) AS _time_nanosecond_,
    message AS _raw_log_,
    // 物化視圖處理成列
    toNullable(toString(replaceAll(JSONExtractRaw(message, 'level'), '"', ''))) AS level
    // 根據需要添加更多解析規則
    ...
FROM default.test_stream
WHERE 1 = 1

按照日誌解析規則將_raw_log_parse成新的真實日誌表

CREATE TABLE default.test
(
    `status` String,
    `_time_second_` DateTime,
    `_time_nanosecond_` DateTime64(9),
    `_raw_log_` String CODEC(ZSTD(1)),
    // 該列通過物化視圖解析得到
    `level` Nullable(String),
    // 根據需要添加更多列
    ...
    INDEX idx_raw_log _raw_log_ TYPE tokenbf_v1(30720, 2, 0) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(_time_second_)
ORDER BY _time_second_
TTL toDateTime(_time_second_) + toIntervalDay(1)
SETTINGS index_granularity = 8192

每當需要分析新的索引字段,clickvisual會執行Add Colum為日誌表添加新的列,同時更新物化視圖添加新的解析處理規則。

ClickVisual的不足:

根據上述的原理:

  • 不支持高效的近似全文檢索,工作原理可以看出由於ClickVisual並未對_raw_log_進行跳數索引,所以也就導致ClickVisual不能高效的支持近似的對原始日誌全文檢索
  • 由於ClickVisual完全依賴Kafka表引擎來實現日誌的攝入,雖然ClickVisual也支持引入ClickHouse的已有日誌表結構進行查詢,但是很可能並不是直接針對列查詢,而是針對map數據查詢,只有通過Kafka引擎來攝入的日誌才能生成新日誌表結構,最終查詢才是針對列式查詢,才能有較高的查詢效率
  • Kafka表引擎讀寫日誌速度無法控制,如果日誌量非常多,導致ClickHouse物化視圖工作過程中內存爆掉

APO 日誌設計方案

ClickVisual已經非常接近理想日誌方案了,只是我們需要對ClickVisual的邏輯進行調整。

  1. 不使用ClickHouse的Kafka表引擎來完成日誌的攝取工作,而是改成Vector的方式完成日誌的攝取工作。這樣就不再依賴Kafka,對於中小用户日誌規模沒有那麼大的用户,可以直接使用,而不需要維護Kafka。雖然去掉了Kakfa,同時增加了Vector,但是Vector的運維工作相比kafka而言,Vector幾乎不需要運維
  2. 引入了Vector之後,可以通過配置Vector來調整參數,確保在大量日誌洪鋒的時候,也不至於將ClickHouse內存打爆
  3. 用户如果真的需要引入Kafka,也有已經維護好的Kafka,完全可以使用Vector先將原始日誌寫入Kafka,然後使用Vector從Kafka讀取出來,繼續實現後續的日誌處理
  4. APO引入了ClickHouse null表引擎,來實現原始日誌(從Vector寫入的) 轉換成按照日誌解析格式解析之後的真實日誌表。
  5. 所有的查詢都是針對真實日誌表的列查詢,所以性能比較高
  6. 在真實日誌表中,額外存儲了_raw_log_,配合跳數索引完成 近似日誌全文檢索。

歡迎使用APO全量日誌功能


APO v0.6.0更新日誌:

新增功能

  • 支持全量日誌的採集、處理與展示功能

缺陷修復

  • 修復服務端點存在特殊字符時,無法獲取到依賴延時曲線的問題
  • 修復無故障場景下頻繁採集故障現場數據的問題
  • 修復部分場景下數據庫調用無指標的問題
  • 修復傳統服務器場景下,網絡質量狀態無法關聯到告警的問題
  • 修復傳統服務器場景下,OneAgent配置注入失敗的問題

其他

  • 允許在創建 ClickHouse 表時選擇是否創建副本
  • 向下兼容 ClickHouse 版本,當前支持最低版本為 22.8.x

APO介紹:

國內開源首個 OpenTelemetry 結合 eBPF 的嚮導式可觀測性產品

apo.kindlingx.com

https://github.com/CloudDetail/apo

user avatar iceblue 頭像 bizseerbishikeji 頭像
點贊 2 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.