跳到主要内容

3 篇博文 含有标签「ClickHouse」

查看所有标签

APO v0.5.0 发布:可视化配置告警规则;优化时间筛选器;支持自建的ClickHouse和VictoriaMetrics

· 阅读需 5 分钟
Autopilot Observability
APO 向导式可观测性平台

Cover 图

APO 新版本 v0.5.0 正式发布!本次更新主要包含以下内容:

新增页面配置告警规则和通知

在之前的版本中,APO 平台仅支持展示配置文件中的告警规则,若用户需要添加或调整这些规则,必须手动编辑配置文件。而在新版本中,我们新增了一套可视化的告警规则配置界面,使用户能够直接通过 APO 控制台来进行告警设置。此外,配置界面内置了常用的指标查询模板,用户只需根据实际需求选取相应的指标并设定阈值,即可轻松完成规则配置。

1 图

同时新版本还支持配置告警通知,目前支持邮件通知和 Webhook 通知。

2 图

0.5.0 作为告警配置的第一个版本,仅包含了基础功能,未来我们还将继续优化用户体验,并带来更丰富的配置选项以满足更复杂的场景需求。欢迎大家积极向我们提出建议。

更好用的时间筛选器

在之前的版本中,APO 的时间筛选器仅支持查询绝对时间,并且需要用户手动触发更新操作。而在新版本中,我们重新设计了时间筛选器,增加了相对时间的支持,并实现了页面的自动刷新功能。以后再也不会出现“新监控了一个应用,但怎么刷新页面也没数据”的问题啦!

3 图

支持使用自建的 ClickHouse 和 VictoriaMetrics

从 0.5.0 版本开始,APO 支持将数据存储到用户自建的 ClickHouse 和 VictoriaMetrics 中,无论您是使用单节点还是分布式集群方案,APO 都能够无缝接入。在生产环境中,我们建议使用托管的 ClickHouse 和 VictoriaMetrics 集群来保证可用性。

近期,APO 社区正在积极设计开发“全量日志”的功能,我们调研分析了业内优秀的日志方案,结合在可观测性领域积累多年的经验,完整设计了从日志的采集、处理、存储到展示的方案,将 APO 对日志的思考融入其中。我们的目标始终是为社区提供一款开箱即用、高效率、低成本、强扩展性且拥有良好用户体验的可观测性产品,全量日志方案自然也不例外。全量日志功能预计将于10月开源,敬请期待!

我们衷心感谢所有参与测试和支持 APO 社区的用户们。正是因为有了你们的反馈和支持,APO 才能不断进步。我们期待着您的宝贵意见,也欢迎您继续参与到 APO 的成长旅程中来!


更多变化请查看下述更新列表。

新增功能

  • 新增页面配置告警规则和通知功能
  • 服务实例中关联实例所在节点信息,辅助排查节点

功能优化

  • 优化时间选择器,支持查看相对时间,支持自动更新
  • 优化故障现场链路页面描述,使信息显示更清晰

缺陷修复

  • 修复单进程镜像覆盖JAVA_OPTIONS环环境变量失败导致无法加载探针的问题
  • 修复部分情况下无法获取 Go 语言程序的链路追踪数据的问题

其他

  • 支持对接自建的低版本 VictoriaMetrics,建议版本 v1.78 以上
  • 支持对接自建的 ClickHouse 集群(安装时配置)
  • 服务概览无数据时提示安装和排查手册
  • 提供一键安装脚本部署测试应用,验证 APO 安装结果和产品功能
  • 提供仅使用链路追踪或采集指标的安装方案

APO选择ClickHouse存储Trace的考量

· 阅读需 6 分钟
Autopilot Observability
APO 向导式可观测性平台

Cover 图

OpenTelemetry生态已经很成熟,但对用户而言,选择OpenTelemetry仍然需要考虑以下几个问题:

  • 探针的成熟度
  • 海量Trace数据的存储和展示的问题

本文重点讨论海量Trace数据的存储与展示问题,APO定位是一个OpenTelmetry的发行版,本文将重点讨论APO团队是如何考虑这个问题的。

现有OpenTelemetry的Trace存储方案

OpenTelemetry生态过于灵活,选择众多,这也给用户带来了幸福的烦恼。

直接使用Jaeger+ElasticSearch方案

Jaeger作为老牌的Tracing方案,其使用习惯已经被很多用户所接受,Jaeger与OpenTelemetry同属于CNCF组织下的开源项目,所以两者也是结合最紧密的。

目前使用OpenTelemetry方式最快的方式使用的是Jaeger+ElasticSearch方案,该方案成熟。但是由于ElasticSearch的存储查询效率并不高,当规模较大的时候成本较大,所以很多用户期望有更加高效的存储方案。

新起的开源Signoz与Uptrace的做法

Signoz与Uptrace是近几年OpenTelemetry生态的发行版本,这两者都选择了ClickHouse作为存储方案,ClickHouse由于其强大的压缩和查询能力,成为很多可观测性方案的标准做法。

Signoz与Uptrace做法相同:自定义ClickHouse的表结构

自定义ClickHouse的表结构的好处在于,所有的内容完全能够自己掌控,但是坏处是其他生态产品很少会基于该自定义表结构进行演进,从而没有办法与其他生态集成。

大量已经习惯了使用Jaeger用户的在使用Signoz和Uptrace的时候都有一定的学习成本,比如需要理解:

  • Span自身花的时间应该如何查找
  • Span的tag应该如何才能查看

这对于没有接触过Jaeger的用户而言是可行的,选择Signoz和Uptrace没有太多差别,但对于已经熟悉Jaeger的用户不大友好。

ClickHouse官方的Exporter方式

ClickHouse官方针对OpenTelemetry生态推出了Exporter,解决了Trace如何落地到ClickHouse的问题,但是并未搭配界面使用,这意味着用户使用ClickHouse官方Exporter的用户,需要定制页面完成Trace的展示和分析工作,这对于绝大部分用户而言并不友好。

Jaeger 2.0 基于ClickHouse的实现Tracing存储方案

具体可以参考文章:迈向 Jaeger v2:更多 OpenTelemetry!

虽然当前Jaeger 1.X版本并没有正式支持ClickHouse,而是在1.57之前通过RemoteStorage 的插件方式支持,具体见链接,在最新的1.58之后,RemoteStorage 就不再支持了。


APO对Trace存储的思考

不同表结构对性能影响没有显著差别

我们团队调研过Jaeger官方关于ClickHouse不同表结构对于Trace插入和查询的影响(主要对比Jaeger RemoteStorage 的表结构和ClickHouse的官方exporter表结构),虽然表结构对性能影响有些许差异,但在插入、查询、压缩比方面各有千秋,而且性能差异对大部分用户也是能接受的。具体见链接

用户习惯最重要

由于APO在向导式界面已经屏蔽了Trace的细节,先通过指标和告警引导用户到需要查看的Trace时,用户才通过TraceID查看Trace。此时,我们认为用户能够以最小的成本理解Trace细节最重要,所以我们引入了Jaeger来展示Trace细节,并没有重新开发页面或者选择signoz、uptrace的方案。

APO对Jaeger RemoteStorage的扩展

Jaeger2.0 已经明确会支持ClickHouse了,在Jaeger 2.0发版之前,APO做了Jaeger RemoteStorage的扩展,使其能够支持1.58以后的版本。具体实现项目见链接


总结

ClickHouse不同的表结构对性能会有差异,但是只要使用ClickHouse其存储和查询效率就会比ElasticSearch高很多,所以在这种情况下,用户的体验就是最重要的。

对于用户而言,每天接触的新产品新功能很多,能够在新产品上无缝嫁接其已具备的成熟体验可能是最重要的。

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

· 阅读需 17 分钟

Cover 图

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-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
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全量日志功能 1图


APO v0.6.0更新日志:

新增功能

  • 支持全量日志的采集、处理与展示功能

缺陷修复

  • 修复服务端点存在特殊字符时,无法获取到依赖延时曲线的问题
  • 修复无故障场景下频繁采集故障现场数据的问题
  • 修复部分场景下数据库调用无指标的问题
  • 修复传统服务器场景下,网络质量状态无法关联到告警的问题
  • 修复传统服务器场景下,OneAgent配置注入失败的问题

其他

  • 允许在创建 ClickHouse 表时选择是否创建副本
  • 向下兼容 ClickHouse 版本,当前支持最低版本为 22.8.x

APO介绍:

国内开源首个 OpenTelemetry 结合 eBPF 的向导式可观测性产品

apo.kindlingx.com

https://github.com/CloudDetail/apo