告别ELK,APO提供基于ClickHouse开箱即用的高效日志方案——APO 0.6.0发布

news/2024/10/20 6:57:57/

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 fieldsbar.String,              Stringfoo.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-performance

根据社区反馈,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-fluent-bit-vector-open-telemetry

提到有以下几种表结构:

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
SELECTstatus,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


http://www.ppmy.cn/news/1540458.html

相关文章

初学结构化查询语言的操作

SQL: Structured Query Language 结构化查询语言(Structured Query Language)简称SQL&#xff0c;是一种特殊目的的编程语言&#xff0c;是一种数 据库查询和程序设计语言&#xff0c;用于存取数据以及查询、更新和管理关系数据库系统。 一&#xff0c;SQL的分类 1&#xff…

记录:网鼎杯2024赛前热身CRYPT02密码学

下载附件后发现附件为图片&#xff0c;图片打开正常&#xff0c;判断密文藏在图片中 使用 editor 软件打开图片&#xff0c;在最底下发现密文 判断这段密文为 base64 加密&#xff0c;尝试本地编写脚本解密 得到flag

mysql connect -- C api编译链接问题,接口介绍(初始化和销毁,连接,执行sql语句,获取结果集的元数据和数据,设置编码格式)

目录 mysql connect 介绍 开发环境 编译链接问题 编译 链接 接口介绍 初始化和销毁 mysql_init() 句柄 mysql_close() 链接数据库 mysql_real_connect() 参数 返回值 show processlist 给mysql下达命令 mysql_query() 参数 返回值 查询结果的获取 引入 …

基于python+dj+mysql的音乐推荐系统网页设计

音乐网站开发 如果你在学Python&#xff0c;需相关的【配套资料工具】作为研究[doge][脱单doge] 可以后台✉私信up主&#xff0c;发送&#x1f449;关键词【音乐】 本章以音乐网站项目为例&#xff0c;介绍Django在实际项目开发中的应用&#xff0c;该网站共分为6个功能模块分…

leetcode289:生命游戏

根据 百度百科 &#xff0c; 生命游戏 &#xff0c;简称为 生命 &#xff0c;是英国数学家约翰何顿康威在 1970 年发明的细胞自动机。 给定一个包含 m n 个格子的面板&#xff0c;每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态&#xff1a; 1 即为 活细胞 &am…

宝塔PHP8.1安装fileinfo拓展失败解决办法

在宝塔面板中安装PHP8.1后&#xff0c;安装fileinfo扩展一直安装不上&#xff0c;查看日志有报错&#xff0c;于是手动来安装也报错。 宝塔报错&#xff1a; 手动命令行编译安装同&#xff0c;也有报错 cd /www/server/php/81/src/ext/fileinfo/ make distclean ./configure …

sharding sphere 加解密功能 like语句 SQL 解析报错

问题描述 应用在使用 sharding sphere 来实现加密后&#xff0c;对于 like sql 语句解析抛异常&#xff0c;异常信息如下&#xff1a; sharding sphere 版本 5.3.2 xml 文件SQL 语句&#xff1a; <select id"countSchoolByStatus" parameterType"java.la…

word取消自动单词首字母大写

情况说明&#xff1a;在word输入单词后首字母会自动变成大写 &#xff08;1&#xff09;点击菜单栏文件 &#xff08;2&#xff09;点击“更多”——>“选项” &#xff08;3&#xff09;点击“校对”——>“自动更正选项” &#xff08;4&#xff09;取消“句首字母大写…