0
120
0
0
首页/专栏/ 技术分享/ 查看内容

ClickHouse 官方文档:设计可观测性数据模型【2/5】

 admin   发表于  2024-12-13 14:50
专栏 技术分享



本文是 ClickHouse 官网文档可观测性系列文章,共有 5 篇:
1. 概述
2. 设计数据模型
3. 管理数据
4. 与 OpenTelemetry 集成
5. 使用 Grafana 分析数据


本篇为第二篇《设计数据模型》,正文如下:


我们建议用户为日志和跟踪设计自己的数据模型,主要原因如下:

  • 选择合适的主键:默认模式使用了基于 ORDER BY 的设计,这种方式是为特定的访问模式优化的。然而,您的实际访问模式可能与此不完全一致,因此需要根据需求选择主键。

  • 提取新结构:用户可能希望从现有列中提取新字段,例如从 Body 列中解析出额外的信息。这可以通过使用物化列来实现,在更复杂的情况下,可以利用物化视图。实现这些功能需要对数据模型进行修改。

  • 优化 Map 类型的使用:默认模式中,Map 类型被用来存储属性以保存任意元数据。这种设计非常实用,尤其是当事件的元数据无法事先定义时,因为像 ClickHouse 这样严格类型的数据库难以直接处理这些动态数据。然而,与普通列相比,访问 Map 键及其值的效率较低。为了解决这一问题,可以通过修改模式,将访问频率较高的 Map 键提升为顶层列。详情请参见“使用 SQL 提取结构”。这一过程同样需要调整数据模型。

  • 简化 Map 键访问:直接访问 Map 键需要较为冗长的查询语法。用户可以通过定义别名来简化这些查询。详细说明请参见“使用别名”。

  • 优化二级索引的使用:默认模式包含二级索引,用于加速 Map 属性的访问和文本查询。这些索引并非总是必要的,同时会占用额外的磁盘空间。在启用这些索引之前,建议进行测试以确认其是否真的需要。更多信息请参见“二级 / 数据跳过索引”。

  • 使用编解码器优化压缩:如果用户对数据特性有足够的了解,并有证据表明优化压缩能够显著提升性能,则可以考虑为列自定义编解码器。

以上各项场景将在后文中详细说明。

重要提示:尽管鼓励用户对数据模型进行扩展和优化,以实现最佳的压缩和查询性能,但建议尽可能遵循 OTel(OpenTelemetry)模式中的核心字段命名规则。例如,ClickHouse 的 Grafana 插件依赖一些基础字段(如 Timestamp 和 SeverityText)来辅助构建查询。日志和跟踪所需的必备字段已在文档 [1][2] 中列出。用户也可以通过插件配置自定义这些字段名以覆盖默认设置。


通过 SQL 提取日志结构

无论是结构化日志还是非结构化日志,用户通常需要以下功能:

  • 从字符串中提取字段。相比查询时使用字符串操作,提前将字段提取为独立的列能显著提升查询效率。

  • 从 Map 类型中提取键值。默认数据模式中,任意属性被存储在 Map 类型的列中。这种设计允许用户在定义日志和追踪时无需提前预定义属性列,这对某些场景非常实用。例如,当从 Kubernetes 收集日志时,通常无法提前确定所有属性,尤其是希望保留 pod 标签以便后续查询时。尽管 Map 提供了灵活性,但其键值的查询效率不如普通列。因此,将常用的 Map 键提取为表的顶层列通常是更高效的选择。

示例查询:

假设我们希望统计接收到最多 POST 请求的 URL 路径。此路径信息以 JSON 数据块的形式存储在 Body 列中(字符串类型)。如果用户在采集器中启用了 json_parser,这部分数据也可能存储在 LogAttributes 列中,类型为 Map(String, String)

SELECT LogAttributesFROM otel_logsLIMIT 1FORMAT Vertical
Row 1:──────Body: {"remote_addr":"54.36.149.41","remote_user":"-","run_time":"0","time_local":"2019-01-22 00:26:14.000","request_type":"GET","request_path":"\/filter\/27|13 ,27| 5 ,p53","request_protocol":"HTTP\/1.1","status":"200","size":"30577","referer":"-","user_agent":"Mozilla\/5.0 (compatible; AhrefsBot\/6.1; +http:\/\/ahrefs.com\/robot\/)"}LogAttributes: {'status':'200','log.file.name':'access-structured.log','request_protocol':'HTTP/1.1','run_time':'0','time_local':'2019-01-22 00:26:14.000','size':'30577','user_agent':'Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)','referer':'-','remote_user':'-','request_type':'GET','request_path':'/filter/27|13 ,27| 5 ,p53','remote_addr':'54.36.149.41'}

当 LogAttributes 可用时,可以通过以下查询统计网站中接收到最多 POST 请求的 URL 路径:

SELECT path(LogAttributes['request_path']) AS path, count() AS cFROM otel_logsWHERE ((LogAttributes['request_type']) = 'POST')GROUP BY pathORDER BY c DESCLIMIT 5
┌─path─────────────────────┬─────c─┐│ /m/updateVariation │ 12182│ /site/productCard │ 11080│ /site/productPrice │ 10876│ /site/productModelImages │ 10866│ /site/productAdditives │ 10866└──────────────────────────┴───────┘
5 rows in set. Elapsed: 0.735 sec. Processed 10.36 million rows, 4.65 GB (14.10 million rows/s., 6.32 GB/s.)Peak memory usage: 153.71 MiB.

在上述查询中,使用了 Map 的语法(如 LogAttributes['request_path']),同时通过 path 函数去除 URL 的查询参数。

如果用户未启用 JSON 解析,LogAttributes 将为空,则需要通过 JSON 函数从 Body 列中的字符串提取字段。

优先选择 ClickHouse 进行解析

我们建议用户优先在 ClickHouse 中解析结构化日志的 JSON 数据。我们相信 ClickHouse 提供了业内最快的 JSON 解析能力。当然,我们也理解某些用户可能希望将日志存储到其他系统,并不希望将解析逻辑局限在 SQL 中。

SELECT path(JSONExtractString(Body, 'request_path')) AS path, count() AS cFROM otel_logsWHERE JSONExtractString(Body, 'request_type') = 'POST'GROUP BY pathORDER BY c DESCLIMIT 5
┌─path─────────────────────┬─────c─┐│ /m/updateVariation │ 12182│ /site/productCard │ 11080│ /site/productPrice │ 10876│ /site/productAdditives │ 10866│ /site/productModelImages │ 10866└──────────────────────────┴───────┘
5 rows in set. Elapsed: 0.668 sec. Processed 10.37 million rows, 5.13 GB (15.52 million rows/s., 7.68 GB/s.)Peak memory usage: 172.30 MiB.

针对非结构化日志的处理

SELECT Body, LogAttributesFROM otel_logsLIMIT 1FORMAT Vertical
Row 1:──────Body: 151.233.185.144 - - [22/Jan/2019:19:08:54 +0330] "GET /image/105/brand HTTP/1.1" 200 2653 "https://www.zanbil.ir/filter/b43,p56" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" "-"LogAttributes: {'log.file.name':'access-unstructured.log'}

对于非结构化日志,类似的查询需要使用 extractAllGroupsVertical 函数,通过正则表达式解析数据。

SELECT    path((groups[1])[2]) AS path,    count() AS cFROM(    SELECT extractAllGroupsVertical(Body, '(\\w+)\\s([^\\s]+)\\sHTTP/\\d\\.\\d') AS groups    FROM otel_logs    WHERE ((groups[1])[1]) = 'POST')GROUP BY pathORDER BY c DESCLIMIT 5
┌─path─────────────────────┬─────c─┐│ /m/updateVariation │ 12182│ /site/productCard │ 11080│ /site/productPrice │ 10876│ /site/productModelImages │ 10866│ /site/productAdditives │ 10866└──────────────────────────┴───────┘
5 rows in set. Elapsed: 1.953 sec. Processed 10.37 million rows, 3.59 GB (5.31 million rows/s., 1.84 GB/s.)

相比结构化日志,解析非结构化日志的查询复杂度更高,性能成本也更大(尤其需要注意性能差异)。因此,我们建议用户尽可能选择使用结构化日志。

使用正则表达式字典进行优化

上述查询可以通过正则表达式字典进一步优化,这将显著提升查询效率。具体操作请参见“使用字典”一节。

以上两种处理场景都可以通过在数据写入时,将查询逻辑预处理并存储到 ClickHouse 来实现。我们将在下文探讨几种具体方法,并说明每种方法的适用场景。

应该选择 OTeL 还是 ClickHouse 进行处理?

用户还可以按照相关文档中的描述,利用 OTel Collector 的处理器和操作符完成日志处理。然而,大多数情况下,用户会发现 ClickHouse 的资源效率和处理速度明显优于 OTel Collector。但需要注意的是,将所有事件处理逻辑放在 SQL 中的主要缺点是,可能导致解决方案过于依赖 ClickHouse。例如,某些用户可能希望通过 OTel Collector 将处理后的日志发送到其他目标存储,例如 S3。


物化字段

物化字段是从其他列提取数据结构的最简单方法。这类字段的值会在数据写入时自动计算,无法通过 INSERT 查询直接指定。

存储成本

物化字段会增加一定的存储开销,因为在写入时,提取的值会被存储为新的磁盘列。

物化字段支持 ClickHouse 的所有表达式,并可使用多种分析函数对数据进行处理,例如字符串操作(包括正则表达式和搜索)、URL 解析、类型转换、从 JSON 中提取值以及数学运算。

我们推荐在基础数据处理场景中使用物化字段,尤其适用于从 Map 类型列中提取值,将其提升为顶层字段,或进行类型转换。在简单的模式设计中,或与物化视图结合使用时,这些字段能发挥最佳效果。例如,在以下日志模式中,JSON 数据已通过采集器提取到 LogAttributes 列中:

CREATE TABLE 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)),    `ResourceSchemaUrl` String CODEC(ZSTD(1)),    `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),    `ScopeSchemaUrl` String CODEC(ZSTD(1)),    `ScopeName` String CODEC(ZSTD(1)),    `ScopeVersion` String CODEC(ZSTD(1)),    `ScopeAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),    `LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),    `RequestPage` String MATERIALIZED path(LogAttributes['request_path']),    `RequestType` LowCardinality(String) MATERIALIZED LogAttributes['request_type'],    `RefererDomain` String MATERIALIZED domain(LogAttributes['referer']))ENGINE = MergeTreePARTITION BY toDate(Timestamp)ORDER BY (ServiceName, SeverityText, toUnixTimestamp(Timestamp), TraceId)

关于如何通过 JSON 函数从字符串类型的 Body 列提取数据的等效模式,请参见相关文档【https://pastila.nl/?005cbb97/513b174a7d6114bf17ecc657428cf829#gqoOOiomEjIiG6zlWhE+Sg==】。

在示例中,我们定义了三个物化字段,分别提取了请求页面、请求类型和引用域名。这些字段从 Map 中提取键值,并对其值进行了相应的处理。经过优化后,查询性能显著提升:

SELECT RequestPage AS path, count() AS cFROM otel_logsWHERE RequestType = 'POST'GROUP BY pathORDER BY c DESCLIMIT 5
┌─path─────────────────────┬─────c─┐│ /m/updateVariation │ 12182│ /site/productCard │ 11080│ /site/productPrice │ 10876│ /site/productAdditives │ 10866│ /site/productModelImages │ 10866└──────────────────────────┴───────┘
5 rows in set. Elapsed: 0.173 sec. Processed 10.37 million rows, 418.03 MB (60.07 million rows/s., 2.42 GB/s.)Peak memory usage: 3.16 MiB.


注意

默认情况下,物化字段不会出现在 SELECT * 查询的结果中。这是为了确保 SELECT * 的结果可以直接通过 INSERT 插回表中。如果需要包含物化字段,可以通过设置 asterisk_include_materialized_columns=1 来启用该功能,在 Grafana 中也可以启用(参见数据源配置中的“Additional Settings -> Custom Settings”)。


基于物化视图的日志与追踪优化

物化视图为日志和追踪的数据处理提供了一种更强大的 SQL 筛选和转换方式。

通过物化视图,用户可以将计算的开销从查询阶段转移到数据写入阶段。ClickHouse 的物化视图本质上是一个触发器,它会在数据写入表时执行指定查询,并将查询结果存储到另一个“目标”表中。

实时更新的特性

在 ClickHouse 中,物化视图随着数据流入基表实时更新,其功能更接近于持续更新的索引。而在其他数据库中,物化视图通常是查询结果的静态快照,需要手动刷新(类似于 ClickHouse 中的可刷新物化视图)。

物化视图的查询可以是几乎任何 SELECT 语句,包括聚合查询,但在涉及 Joins 时存在一定的限制。对于日志和追踪所需的转换和筛选任务,几乎所有的查询都可以被支持。

需要注意,物化视图的查询只是一个触发器,用于对插入源表的每一行数据执行计算,并将结果写入目标表。

为了避免数据同时存储在源表和目标表中,可以将源表的存储引擎更改为 Null 表引擎,同时保留原始表的模式。OTel 采集器将继续向源表发送数据。例如,对于日志,可以将 otel_logs 表修改为:

CREATE TABLE 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)),    `ResourceSchemaUrl` String CODEC(ZSTD(1)),    `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),    `ScopeSchemaUrl` String CODEC(ZSTD(1)),    `ScopeName` String CODEC(ZSTD(1)),    `ScopeVersion` String CODEC(ZSTD(1)),    `ScopeAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),    `LogAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1))) ENGINE = Null

Null 表引擎:日志处理优化

Null 表引擎是一个强大的优化工具,可以理解为 /dev/null。该表不会存储任何数据,但所有关联的物化视图会在插入数据时执行相应的操作,随后这些数据会被丢弃。

查询示例

以下查询将行数据转换为所需的格式,从 LogAttributes 中提取所有列(假设这些列由采集器通过 json_parser 操作符设置),并根据简单条件和列定义生成或提取 SeverityTextSeverityNumber。在该示例中,我们仅选择已知会被填充的列,忽略例如 TraceId、SpanId 和 TraceFlags 这样的列。

SELECT        Body,     Timestamp::DateTime AS Timestamp,    ServiceName,    LogAttributes['status'] AS Status,    LogAttributes['request_protocol'] AS RequestProtocol,    LogAttributes['run_time'] AS RunTime,    LogAttributes['size'] AS 

路过

雷人

握手

鲜花

鸡蛋

版权声明:本文为 clickhouse 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
返回顶部