本文字数:15051;估计阅读时间:38 分钟
作者:Dale McDiarmid & Ryadh Dahimene
本文在公众号【ClickHouseInc】首发
引言
大约一年前,我们发布了一篇关于基于 SQL的可观测性现状的博文,分析了 SQL 和可观测性这两个领域的发展背景。我们解释了它们如何结合,带来了在可观测性领域的新机遇,同时回答了“SQL 可观测性何时适用于我的用例?”这篇文章引起了广泛关注,对新用户理解 ClickHouse 在可观测性领域的作用帮助很大。
虽然这篇文章的主要观点仍然适用,但一年时间对 ClickHouse 的发展来说意味着许多变化!多个新功能进一步推动 ClickHouse 成为可观测性数据的默认数据库,同时其生态系统也在成熟,使新用户更易于上手。
在本文中,我们将探讨一些新功能,并展示一些实验性的工作,这些工作可能成为 ClickHouse 在可观测性领域中指标管理的基础。
2023 年的基于 SQL 的可观测性
我们最初的文章提出,将可观测性视为一个数据问题,而 SQL 和 OLAP 系统(例如 ClickHouse)可以很好地解决这一问题。我们回顾了集中式日志记录的历史,并探讨了 Splunk 和 ELK 堆栈如何从 syslog 和 NoSQL 时代中发展而来。尽管 NoSQL 盛行,SQL 以其独特优势依然是第三大广泛应用的结构化数据管理语言。
当我们将可观测性重新定义为数据问题时,提出可以用 OLAP 原则和 SQL 构建其存储层。这种方法带来显著优势:高效压缩降低存储需求,快速的数据摄入和检索,结合对象存储实现无限扩展。以 ClickHouse 为例,它支持多种数据格式和集成引擎,便于在不同的可观测性管道中使用。SQL 提供的丰富分析功能进一步降低了可观测性数据的总体成本(TCO)。
OpenTelemetry 是这一趋势的重要推动力,它将跨平台的数据收集标准化,将数据收集从差异化功能变成了普及功能。一年之后,我们可以自信地说 OTel 正在“收集器之战”中占据主导地位。
行业内的广泛采用降低了厂商绑定,使基于 SQL 的存储解决方案与可观测性数据的集成更加便捷。
我们总结认为,基于 SQL 的可观测性架构简洁而灵活,为用户提供了丰富的个性化、适应和集成选项,更便于在现有 IT 环境中使用。
为帮助用户确定基于 SQL 的可观测性是否适合自身,我们提供了一个简单的检查表:
基于 SQL 可观测性适合于您,如果:
-
您和团队熟悉 SQL(或希望学习 SQL)
-
您倾向于使用 OpenTelemetry 等开放标准,以避免厂商绑定并实现扩展性
-
您愿意使用由开源驱动的生态系统,从数据收集到存储和可视化
-
您预计可观测性数据量会增长至中或大规模(甚至非常大)
-
您希望控制总成本(TCO),避免可观测性项目的成本失控
-
您不希望仅为了节省成本而限制数据保留时间
基于 SQL 可观测性不适合于您,如果:
-
您和团队对学习或使用 SQL 不感兴趣
-
您需要一个打包的端到端可观测性体验
-
您的可观测性数据量很小(例如低于 150 GiB)且没有增长预期
-
您的用例主要以指标为主,且需要使用 PromQL。在这种情况下,您仍可用 ClickHouse 处理日志和追踪数据,同时在 Grafana 中将其与 Prometheus 指标统一展示
-
您更愿意等待生态系统进一步成熟,使基于 SQL 的可观测性更加便捷
随着 2024 年接近尾声,问题是:鉴于 ClickHouse 的发展,这些建议是否需要刷新?
ClickHouse 对 JSON 的支持!可观测性领域的颠覆性功能?
在介绍 JSON 支持的最新进展前,先来探讨一下我们所说的 JSON 支持究竟指的是什么,为什么它对可观测性如此重要,以及用户目前面临的那些挑战。
什么是 JSON 支持?
ClickHouse 早已在数据输入输出上就支持 JSON 格式,这也促成了它与可观测性工具的早期整合。用户可以通过 ClickHouse 的原生接口或 HTTP 接口发送 JSON 数据,并选择多种输出格式满足需求。这种灵活性使 ClickHouse 更容易集成 OpenTelemetry、Grafana 等工具,实现流畅的数据摄取与可视化。同时,它让用户能轻松构建自定义接口,使 ClickHouse 能适应多种可观测性应用场景。
然而,将 JSON 集成作为输入输出格式与将 JSON 作为一种列类型支持是不同的——后者在可观测性中尤为关键。我们指的是能够定义 JSON 列,用于存储嵌套的动态结构,其中相同路径的值可能有不同的数据类型(可能互不兼容且不能预先确定)。
为什么 JSON 支持如此重要?
在可观测性解决方案中,用户希望能“直接发送事件”,而无需为模式精细化和优化操心。即使在有时间定义模式的情况下,集中式可观测系统通常会聚合来自多样化来源的数据——这些数据结构各异、不断变化,可能来自不同的应用、团队,甚至是不同组织(尤其是在 SaaS 解决方案中)。要实现所有数据的统一命名规则或模式非常困难。
虽然结构化日志和 OpenTelemetry 的标准和语义规范在一定程度上帮助解决了此问题,用户仍需要记录任意标签和字段。这些字段的数量、类型和嵌套层次可能因数据来源不同而差异巨大。为此,设计一种“非结构化 JSON”列非常有用,可灵活存储动态属性,如 Kubernetes 标签或特定应用的自定义元数据。
理想情况下,ClickHouse 支持 JSON 列类型,这样用户就可以直接发送半结构化数据。这种方式在功能、压缩和性能上与传统数据类型相同,同时大大简化了模式管理和定义的工作量。
当前方法的挑战
目前,ClickHouse 的 OTel 导出器为日志、追踪和指标提供了默认的模式。在日志模式中,通常依赖 DateTime 和 String 等传统数据类型(并结合 LowCardinality 和压缩编解码器等优化):
CREATE TABLE otel.otel_log (
Timestamp DateTime64(9) CODEC(Delta(8), ZSTD(1)),
TimestampTime DateTime DEFAULT toDateTime(Timestamp),
TraceId String CODEC(ZSTD(1)),
SpanId String CODEC(ZSTD(1)),
TraceFlags UInt8,
SeverityText LowCardinality(String) CODEC(ZSTD(1)),
SeverityNumber UInt8,
ServiceName LowCardinality(String) CODEC(ZSTD(1)),
Body String CODEC(ZSTD(1)),
ResourceSchemaUrl LowCardinality(String) CODEC(ZSTD(1)),
ResourceAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
ScopeSchemaUrl LowCardinality(String) CODEC(ZSTD(1)),
ScopeName String CODEC(ZSTD(1)),
ScopeVersion LowCardinality(String) CODEC(ZSTD(1)),
ScopeAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
LogAttributes Map(LowCardinality(String), String) CODEC(ZSTD(1)),
--indexes omitted
) ENGINE = MergeTree
PARTITION BY toDate(TimestampTime)
PRIMARY KEY (ServiceName, TimestampTime)
ORDER BY (ServiceName, TimestampTime, Timestamp)
不过,也可以注意到 Map 类型在 LogAttributes、ResourceAttributes 和 ScopeAttributes 列中的应用。OpenTelemetry 中,这些列捕获了不同层级的元数据:LogAttributes 存储特定于单个日志事件的信息,ResourceAttributes 包含生成数据源的描述,而 ScopeAttributes 则记录应用代码或监控上下文信息。它们为筛选和分组提供了重要的上下文信息,并且由于数据源、应用场景的不同,这些属性可能会显著变化,甚至随着时间的推移而更新。
尽管 Map 类型能够动态添加键,帮助用户灵活存储这些字段,但其仍存在一些重要的缺陷:
-
类型精度受限 - 在 Map 表示中,字段名和字段值都以字符串形式存储,Map 只能支持单一值类型(用户现在可以使用 Variant 作为 Map 的值)。这里使用了 String 作为统一数据类型,这意味着在查询时对数值字段进行比较时,需要进行类型转换,导致查询代码冗长,性能受损,增加了查询的延迟和内存占用。
-
线性复杂度的单列实现 - 所有嵌套的 JSON 路径存储在一个列中。当查询 Map 中的某个键时,ClickHouse 需要加载整个 Map 并逐一扫描。这样会产生不必要的 IO 开销,尤其是当只查询一个路径时,效率极低。为解决这一问题,用户会在插入时通过物化列提取 Map 中的特定值。
-
可用键不清晰 - 将 JSON 路径存储为 Map 键,用户无法轻易查看有哪些可用的路径。尽管可以通过查询查找,但识别所有键需要一次耗时的线性扫描。可以借助物化视图在插入时记录这些键集合,但这增加了复杂性,并需要前端 UI 支持查询推荐功能。
-
不支持嵌套结构 - Map 值类型为 String,因此仅支持单层结构。虽然 ClickHouse 中的 Tuple 和 Nested 类型支持嵌套结构,但需要在接收数据之前定义其结构。
新的 JSON 类型解决了上述所有挑战,支持动态嵌套结构,同时保持类型精度和静态类型列的高性能。
高效的 JSON 支持 —— 可观测性的新突破?
在我们发布最初的博文时,ClickHouse 的 JSON 支持还处于早期阶段。虽然它已经作为实验功能存在了一段时间,但仍存在许多显著的挑战。主要问题在于它无法高效处理大量 JSON 路径,同时试图通过动态统一类型来适配不同数据类型,结果证明这是一个根本性的设计缺陷,因此我们决定从头重写此功能。在 24.8 版本中,我们宣布重构基本完成,已接近 Beta 阶段,我们对其理论上保持的最佳实现充满信心。
正如我们在最近的博文中所述,ClickHouse 全新的 JSON 实现专为列式数据库的 JSON 处理需求设计,避免了统一或强制数据类型的问题,从而避免了数据失真或查询复杂化的情况。
其他许多数据存储系统通常采用在路径首次出现时推断类型的方式,这也是我们早期的 JSON 实现所采用的方案。然而,如果同一路径在后续事件中出现不同的数据类型,这些系统要么强制类型转换,要么拒绝数据。这种方法在可观测性数据中效果欠佳,因为事件数据往往难以保持完全一致。最终要么导致查询性能低下,要么减少可用操作选项,除非用户在查询时手动进行类型转换。
如图所示,ClickHouse 将每个唯一 JSON 路径的值存储为真正的列格式,甚至可以处理路径中的不同数据类型。为此,系统为每个唯一的路径和类型对创建了独立的子列。
例如,当插入两个不同类型的 JSON 路径时,ClickHouse 会将不同类型的值分别存储在不同的子列中,这些子列可以独立访问,从而减少不必要的 I/O。即使查询中包含多种类型的列,数据仍然以单一列格式返回。
此外,ClickHouse 利用偏移量来确保这些子列的密集性,不为缺失路径存储默认值,从而最大化压缩效果并减少 I/O。
通过将每个 JSON 路径作为独立子列存储,ClickHouse 还可以提供可用路径的列表,支持 UI 的自动补全功能。
此外,这种 JSON 类型不会因大量 JSON 路径而导致子列数量激增。系统在路径超出限制时将其存储到一个共享数据列中,同时附有加速查询的统计信息。
通过这种增强的 JSON 数据类型,用户可以对复杂嵌套的 JSON 结构进行高效的扩展分析,使得 ClickHouse 的 JSON 支持在性能和效率上媲美系统中的经典数据类型。
对于对这一新列类型实现感兴趣的用户,建议阅读我们提供的详细博文。
OTel 的 JSON 支持
我们预计 ClickHouse 的 OpenTelemetry 模式将支持这一功能。例如,以下日志模式将 JSON 类型应用于非结构化的列(省略了 JSON 类型的配置):
CREATE TABLE otel_log (
Timestamp DateTime64(9) CODEC(Delta(8), ZSTD(1)),
TimestampTime DateTime DEFAULT toDateTime(Timestamp),
TraceId String CODEC(ZSTD(1)),
SpanId String CODEC(ZSTD(1)),
TraceFlags UInt8,
SeverityText LowCardinality(String) CODEC(ZSTD(1)),
SeverityNumber UInt8,
ServiceName LowCardinality(String) CODEC(ZSTD(1)),
Body String CODEC(ZSTD(1)),
ResourceSchemaUrl LowCardinality(String) CODEC(ZSTD(1)),
ResourceAttributes JSON,
ScopeSchemaUrl LowCardinality(String) CODEC(ZSTD(1)),
ScopeName String CODEC(ZSTD(1)),
ScopeVersion LowCardinality(String) CODEC(ZSTD(1)),
ScopeAttributes JSON,
LogAttributes JSON,
--indexes omitted
) ENGINE = MergeTree
PARTITION BY toDate(TimestampTime)
PRIMARY KEY (ServiceName, TimestampTime)
ORDER BY (ServiceName, TimestampTime, Timestamp)
这意味着用户可以在这些 OTel 列中发送任意数据,并确保数据得到高效存储且查询便捷。
请注意,我们依然建议遵循完整的 OTel 模式,也就是在根级上创建列,而不仅仅依赖单一的 JSON 列!这种显式的列设置可以充分利用 ClickHouse 的各种功能,例如支持编解码器和二级索引。因此,将常用的查询列提取到根级仍然是明智的选择。
示例
假设有一个结构化的日志数据集。
{"remote_addr":"40.77.167.129","remote_user":"-","run_time":0,"time_local":"2019-01-22 00:26:17.000","request_type":"GET","request_path":"\/image\/14925\/productModel\/100x100","request_protocol":"HTTP\/1.1","status":"500","size":1696.22,"referer":"-","user_agent":"Mozilla\/5.0 (compatible; bingbot\/2.0; +http:\/\/www.bing.com\/bingbot.htm)","response_time":23.2}
{"remote_addr":"91.99.72.15","remote_user":"-","run_time":"0","time_local":"2019-01-22 00:26:17.000","request_type":"GET","request_path":"\/product\/31893\/62100\/----PR257AT","request_protocol":"HTTP\/1.1","status":200,"size":"41483","referer":"-","user_agent":"Mozilla\/5.0 (Windows NT 6.2; Win64; x64; rv:16.0)Gecko\/16.0 Firefox\/16.0","response_time":""}
虽然总体上结构良好,但在此数据集中,各列的类型有所不同,例如 status、response_time、size 和 run_time 既包含数值类型又包含字符串类型。通过 OTel 收集器配置从文件摄取此数据集后,结果如下:
SELECT
Timestamp,
LogAttributes
FROM otel_logs
FORMAT Vertical
Row 1:
──────
Timestamp: 2019-01-22 00:26:17.000000000
LogAttributes: {'response_time':'23.2','remote_addr':'40.77.167.129','remote_user':'-','request_path':'/image/14925/productModel/100x100','size':'1696.22','request_type':'GET','run_time':'0','referer':'-','user_agent':'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)','time_local':'2019-01-22 00:26:17.000','request_protocol':'HTTP/1.1','status':'500','log.file.name':'simple.log'}
Row 2:
──────
Timestamp: 2019-01-22 00:26:17.000000000
LogAttributes: {'request_protocol':'HTTP/1.1','status':'200','user_agent':'Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0)Gecko/16.0 Firefox/16.0','size':'41483','run_time':'0','remote_addr':'91.99.72.15','request_type':'GET','referer':'-','log.file.name':'simple.log','remote_user':'-','time_local':'2019-01-22 00:26:17.000','response_time':'','request_path':'/product/31893/62100/----PR257AT'}
2 rows in set. Elapsed: 0.003 sec.
OTel 收集器将所有列放在 LogAttributes 中,结果所有列值都被映射为字符串类型。因此,即使是简单查询也变得语法复杂,例如:
SELECT LogAttributes['status'] AS status,
max(if((LogAttributes['response_time']) != '',
LogAttributes['response_time']::Float32, 0)) AS max
FROM otel_logs
GROUP BY status
┌─status─┬──max─┐
│ 500 │ 23.2 │
│ 200 │ 0 │
└────────┴──────┘
2 rows in set. Elapsed: 0.016 sec.
如果我们使用 JSON 类型在 LogAttributes 列中插入相同数据,可以看到数据类型得以保留。
SELECT
Timestamp,
LogAttributes
FROM otel_logs
FORMAT Vertical
SETTINGS output_format_json_quote_64bit_integers = 0
Row 1:
──────
Timestamp: 2019-01-22 00:26:17.000000000
LogAttributes: {"referer":"-","remote_addr":"91.99.72.15","remote_user":"-","request_path":"\/product\/31893\/62100\/----PR257AT","request_protocol":"HTTP\/1.1","request_type":"GET","response_time":"","run_time":"0","size":"41483","status":200,"time_local":"2019-01-22 00:26:17.000000000","user_agent":"Mozilla\/5.0 (Windows NT 6.2; Win64; x64; rv:16.0)Gecko\/16.0 Firefox\/16.0"}
Row 2:
──────
Timestamp: 2019-01-22 00:26:17.000000000
LogAttributes: {"referer":"-","remote_addr":"40.77.167.129","remote_user":"-","request_path":"\/image\/14925\/productModel\/100x100","request_protocol":"HTTP\/1.1","request_type":"GET","response_time":23.2,"run_time":0,"size":1696.22,"status":"500","time_local":"2019-01-22 00:26:17.000000000","user_agent":"Mozilla\/5.0 (compatible; bingbot\/2.0; +http:\/\/www.bing.com\/bingbot.htm)"}
2 rows in set. Elapsed: 0.012 sec.
默认情况下,64 位整数会在输出中加引号(为兼容 JavaScript)。在本示例中,我们通过设置 output_format_json_quote_64bit_integers=0 禁用该功能。
现在查询变得更加直观,并且能体现数据的类型差异:
SELECT LogAttributes.status as status, max(LogAttributes.response_time.:Float64) as max FROM otel_logs GROUP BY status
┌─status─┬──max─┐
│ 200 │ ᴺᵁᴸᴸ │
│ 500 │ 23.2 │
└────────┴──────┘
2 rows in set. Elapsed: 0.006 sec.
除了可以显式访问 response_time 列的 Float64 类型之外,我们还看到空值被处理为 Null 而不是 0。最后,可以使用 distinctJSONPathsAndTypes 函数查看列中多种类型的存在情况。
SELECT distinctJSONPathsAndTypes(LogAttributes)
FROM otel_logs
FORMAT Vertical
Row 1:
──────
distinctJSONPathsAndTypes(LogAttributes): {'referer':['String'],'remote_addr':['String'],'remote_user':['String'],'request_path':['String'],'request_protocol':['String'],'request_type':['String'],'response_time':['Float64','String'],'run_time':['Int64','String'],'size':['Float64','String'],'status':['Int64','String'],'time_local':['DateTime64(9)'],'user_agent':['String']}
1 row in set. Elapsed: 0.008 sec.
可观测性生态系统日趋完善
随着 ClickHouse 核心 JSON 功能的完善,用户的使用体验将进一步简化,查询速度也会提升。与此同时,我们也在更广泛的可观测性生态系统上继续投入,主要集中在 ClickHouse 对 OpenTelemetry Collector 的支持和 Grafana 插件的增强。
OpenTelemetry
在 OpenTelemetry 中,我们致力于设计一个 ClickHouse 的开源模式,既具灵活性又兼顾性能。可观测性数据的结构和用途各不相同,因此优化 ClickHouse 的模式需要根据具体的用例来考量。我们为 OpenTelemetry Collector 提供了“多数适用”的默认模式,以帮助用户快速上手。
这些模式是一个起点而非最终的生产方案。对于有特定需求的用户,例如按服务或 Kubernetes pod 名称过滤,我们鼓励自定义模式以获得最佳性能。为了便于灵活调整,导出器允许用户创建自定义表,无需修改导出器代码。此外,可以通过物化视图调整默认模式,以支持特定属性,建立更符合实际查询需求的表结构。
随着对“快速上手”模式的信心增强,ClickHouse 的日志和追踪导出器现已进入 Beta 阶段,与 Elasticsearch 和 Datadog 等其他导出器的成熟度相当。
Grafana
自 2022 年 5 月推出 ClickHouse 的 Grafana 插件以来,两大平台取得了显著发展。今年年初的一篇博文提到 SQL 在可观测性中的挑战。为此,第 4 版引入了全新的查询构建界面,将日志和追踪作为一等数据类型,使用户无需手动编写 SQL,大大简化了 SRE 和工程师的操作。
此外,第 4.0 版进一步支持 OpenTelemetry,让用户可以直接配置符合 OTel 标准的日志和追踪数据。这一变化让 OTel 用户的体验更加流畅、直观。
自 4.0 版发布以来,我们专注于优化用户体验,并引入了日志上下文支持。此功能让用户能够查看特定日志行的相关日志,快速分析事件的前因后果。
实现方法是通过修改基础日志查询,移除所有过滤条件和排序规则。然后应用上下文过滤器,将选定日志行的服务名称、主机名或容器 ID 等值与数据源配置匹配。还会基于时间戳增加时间范围过滤器,并通过排序和限制控制显示效果。生成两个方向的查询,允许用户上下滚动,查看初始行周围的其他日志。
这样用户无需手动移除现有过滤器就能快速定位错误日志的原因。
经验教训总结
在帮助用户部署 OTel-ClickHouse 堆栈的过程中,我们获得了许多宝贵的经验。
最核心的经验是:用户需要尽早识别查询访问模式,并为表选择合适的主键和排序键。虽然 ClickHouse 在执行线性扫描时速度很快,但在大规模部署中,如果用户期待亚秒级响应时间,仅靠线性扫描不切实际。一般来说,过滤列应放在主键中,并按优先级排序,以适应逐层下钻的查询模式。最常用的过滤列应排在最前面,这与我们为用户设计入门模式的初衷一致。
我们还发现,物化视图对满足性能要求、调整 OTel 数据以适应用户访问模式至关重要。现在越来越多的用户将数据先发送到 Null 表引擎,并使用物化视图对插入的数据进行查询处理。然后将视图结果发送到目标表,以便快速响应用户查询需求。
这种方法带来了一些明显好处:
-
数据转换 - 用户可以在数据存储之前调整数据结构。虽然 OTel 的默认模式通用性强,但用户往往需要从非结构化数据中提取特定字段(如 Body)以便于查询或用作主键。虽然 OTel 收集器也能处理字符串解析或 JSON 转换,但在 ClickHouse 中完成这些操作速度更快、资源消耗更少。具体示例请参考架构设计指南的“使用 SQL 提取数据结构”部分。
-
备用排序键 - 用户的查询模式往往多样,难以统一到单一主键。在这种情况下,用户可以利用投影功能,或使用物化视图,将数据子集复制到按不同排序键组织的表中。这些独立表可服务于子团队或特定工作流。或者,用户可以将这些子表用作快速查找表,从而辅助主表的查询过滤。具体示例请参考架构设计指南的“使用物化视图(增量)进行快速查找”部分。
-
子集过滤 - 物化视图常用于过滤子集,将结果发送到用于生成告警的表上,从而将查询成本从查询阶段转移到插入阶段。
-
预计算聚合 - 与过滤类似,物化视图中预先计算的聚合查询(如随时间的错误计数)可以将计算成本从查询阶段转移到插入阶段。具体示例请参考架构设计指南的“使用物化视图(增量)进行聚合”部分。
最后,我们还发现用户在 ClickHouse 中使用列别名特别有用,尤其是在 map 列包含大量键的情况下。这样用户查询时无需记忆所有键。虽然随着 JSON 支持的发展,这个问题可能会减少,但列别名依然实用。因此,ClickHouse 的 Grafana 插件也支持别名功能,以便用户通过界面轻松管理。
我们已将这些经验总结并反映在年初发布的可观测性指南中。
并非所有情况都适用 OTel:多样化的 ClickHouse 应用
虽然 OpenTelemetry 提供了一个标准化、灵活且开放的遥测数据方案,但通过 ClickHouse 这种强大的实时分析数据库,用户可以将遥测数据的存储范围拓展到 OTel 范式之外。通过集中存储多种数据集,用户可以从整体上掌握复杂系统的内部状态。
我们在 ClickHouse 内部的日志记录就是一个很好的示例。我们在独立的博文中记录了 LogHouse,这是一款基于 OTel 的日志平台,截至 2024 年 10 月已管理超 43 PB 的数据。今年早些时候,我们还为 LogHouse 配套推出了一个系统:SysEx(即 Systems Tables Exporter 的缩写),这是一款专用的收集器,负责集中收集所有 ClickHouse Cloud 数据库服务的信号,并基于收集 ClickHouse 系统数据库 _log 表的数据。截至 2024 年 11 月,SysEx 已管理近 100 万亿行数据,有效支持了 ClickHouse Cloud 的高效运行。
时间序列引擎:ClickHouse 作为 Prometheus 数据的存储引擎
作为用于收集、存储和分析时间序列指标的工具,Prometheus 在各行业中已成为主流选择,其灵活的数据模型、强大的 PromQL 查询语言和与 Kubernetes 等容器环境的无缝集成让它在可靠性和简洁性上表现出色。然而,Prometheus 也有局限性:缺乏多节点支持,仅支持垂直扩展,且内存开销较大。此外,它在处理高基数数据和多租户支持方面表现有限。
ClickHouse 24.8 版本引入了 Timeseries 表引擎作为实验功能,允许将 ClickHouse 用作 Prometheus 指标的存储引擎。此功能让用户在继续享受 Prometheus 优势的同时,直接解决其局限性:提供可扩展的高性能存储,支持高基数数据,并具有内存高效的多节点架构。ClickHouse 作为存储后端,用户可以保持熟悉的 Prometheus 界面和功能,同时享受 ClickHouse 的水平扩展、数据压缩和出色查询性能。
这通过远程写入和远程读取协议实现。
我们在最近的发布文章中提供了详细的配置示例。用户需要在 ClickHouse 配置文件中指定读写端点,然后在 Prometheus 配置中使用相同的端点。这允许创建 Timeseries 类型的表。
CREATE TABLE test_time_series ENGINE=TimeSeries
该表包含三个子表,负责存储指标、标签和数据点。
SHOW TABLES LIKE '.inner%'
┌─name───────────────────────────────────────────────────┐
│ .inner_id.data.c23c3075-5e00-498a-b7a0-08fc0c9e32e9 │
│ .inner_id.metrics.c23c3075-5e00-498a-b7a0-08fc0c9e32e9 │
│ .inner_id.tags.c23c3075-5e00-498a-b7a0-08fc0c9e32e9 │
└────────────────────────────────────────────────────────┘
3 rows in set. Elapsed: 0.002 sec.
以标准化 Prometheus 数据,减少标签的重复存储并提高压缩效果。然而,当通过远程读取协议检索指标时,架构在查询时需要基于生成的标识符进行连接。用户可以按发布文章中的说明配置,并通过 Prometheus UI 或 Grafana 可视化指标。
默认情况下,Prometheus 会生成内部指标,供用户进行功能测试。上图展示了 Prometheus go 进程的相关指标。
在图中,我们绘制了 go_memstats_heap_alloc_bytes 指标,用于描述 Prometheus Go 堆中分配的字节数。这需要对数据和标签表进行 SEMI LEFT JOIN,并按 metric_name 和 tags 字段分组。
通过 Prometheus 和 ClickHouse 的远程读取集成,用户可以直接从 Prometheus 使用 PromQL 查询 ClickHouse 中的历史数据。虽然这一集成支持时间序列查询,但目前的实现方式让 Prometheus 从 ClickHouse 拉取所有原始数据,在本地执行过滤和聚合操作。对于短时间查询效果良好,但在处理长时间范围的大量数据时效率较低。Prometheus 尚未下推聚合操作,因此对于长时间范围内的复杂或高基数查询仍需优化。
总之,时间序列表引擎为存储和查询 Prometheus 数据提供了基础能力。目前此集成仍然是高度实验性的,为未来的改进奠定了基础。未来几个月的持续优化计划将进一步释放 ClickHouse 在 Prometheus 中的性能潜力,提供一个更高效、可扩展的长期指标存储和分析解决方案。
未来计划和总结
2024 年剩余时间和 2025 年初,我们的可观测性开发将主要聚焦于三大方向:生产级 JSON、倒排索引和时间序列表引擎。
首先,将 JSON 支持推向生产级,并将其作为 OTel Collector 的默认选项,仍然是我们的首要目标。这需要在底层 go 客户端中进行大量优化,该客户端同时也被 Grafana 使用,因此这项工作也将使 JSON 列的可视化成为可能。
其次,倒排索引在 ClickHouse 中作为实验功能已有一段时间。虽然并非所有数据都需要倒排索引,但它对快速处理近期文本查询至关重要。倒排索引能够避免线性扫描,类似于跳过索引,可以减少所需读取的粒度(尤其是稀有词条)。与 JSON 支持类似,我们认为此功能需要显著改进才能达到生产级别,我们期望在明年见到进一步改进。
需要说明的是,我们不打算添加相关性统计或转型为搜索引擎!这些功能在可观测性中并非必要,用户通常按时间排序。倒排索引将作为辅助索引,用于加速字符串 LIKE 和 Token 匹配查询。
总结:近期的发展是否改变了 ClickHouse 作为可观测性解决方案的适用性?答案是“部分改变”。
用户可以依然沿用我们在博文开头提到的决策步骤,只不过现在更有信心。最新的进展解决了最后一个评估标准:“您倾向于等待生态系统进一步成熟,SQL 可观测性更加便捷。”
JSON 支持的加入、OTel 日益成熟以及 Grafana 的可用性提升,标志着 ClickHouse 在可观测性领域得到了显著提升。这些更新虽然没有改变根本的决策因素,但让 ClickHouse 变得更易用、更易部署,也更适合承担可观测性工作负载。
征稿启示
面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com
联系我们
手机号:13910395701
邮箱:Tracy.Wang@clickhouse.com
满足您所有的在线分析列式数据库管理需求