博客 / 工程

ClickHouse 和 OpenTelemetry

author avatar
Spencer Torres, Ryadh Dahimene
2024 年 11 月 7 日 - 15 分钟阅读

今年早些时候,ClickHouse 团队决定开始正式支持并贡献 ClickHouse 的 OpenTelemetry 导出器,该导出器最近已升级到日志记录和追踪的 beta 版本(目前 OTel 导出器生态系统中的最高级别)。在这篇文章中,我们希望借此里程碑,重点介绍 OpenTelemetry 和 ClickHouse 的集成。

什么是 OpenTelemetry?

OpenTelemetry(简称 OTel)是云原生计算基金会 (CNCF) 的一个开源框架,它支持遥测数据的标准化收集、处理和导出。OTel 围绕主要的可观测性支柱(追踪、指标和日志)构建,提供了一种统一的、供应商中立的方法,使开发人员和运维团队能够深入了解系统运行状况并诊断分布式系统中的问题。

OTel 还为多种语言提供了检测库,这些库可以通过最少的代码更改自动收集数据。OTel 收集器管理此数据流,充当将遥测数据导出到各种后端平台的网关。通过提供一致的可观测性标准,OTel 帮助团队高效地收集遥测数据,并深入了解复杂的系统。

为什么 OpenTelemetry 很重要?

在碎片化且由供应商主导的可观测性领域,OTel 带来了一种标准化的、灵活的和开放的方法。随着软件应用程序变得越来越复杂,跨越微服务和基于云的架构,跟踪系统内部和系统之间正在发生的事情变得具有挑战性。OTel 通过支持关键遥测数据的一致收集和分析来解决这个问题。

供应商中立的方法尤其有价值,它使团队能够避免被任何单一的监控工具锁定。通过使用 OTel,组织可以轻松地在不同的可观测性后端之间切换或组合,以满足他们的需求,从而降低成本并提高灵活性。遥测数据的标准化格式简化了跨系统的集成,即使在多云和混合环境中也是如此。

OpenTelemetry + ClickHouse

在之前的博客中,我们解释了像 ClickHouse 这样的工具如何使我们能够处理大量的可观测性数据,使其成为专有系统的可行的开源替代方案。基于 SQL 的可观测性非常适合熟悉 SQL 的团队,它提供了对成本和可扩展性的控制。随着像 OTel 这样的开源工具的不断发展,这种方法对于具有大量数据需求的组织来说正变得越来越实用。

基于 SQL 的可观测性堆栈的一个关键组件是 OpenTelemetry 收集器。OTel 收集器从 SDK 或其他来源收集遥测数据,并将其转发到受支持的后端。它充当接收、处理和导出遥测数据的中心枢纽。OTel 收集器可以充当单个应用程序的本地收集器(代理),也可以充当多个应用程序的集中式收集器(网关)。

img09_4566662115.0.png

收集器包括一系列支持各种数据格式的导出器。导出器将数据发送到选定的后端或可观测性平台,如 ClickHouse。开发人员可以配置多个导出器,以根据需要将遥测数据路由到不同的目的地。

由于我们在 ClickHouse 内部将 OpenTelemetry 用于我们自己的需求,并且我们看到许多成功的用户正在采用它,因此我们决定正式支持 ClickHouse 的 OTel 导出器,并为该组件的开发做出贡献。得益于其维护者(@hanjm@dmitryax@Frapschen)的出色工作和社区贡献,ClickHouse 的 OTel 导出器已经处于良好状态,因此对我们而言,这实际上是为了确保我们能够大规模地支持关键用例。

一个适用于所有 Schema 的 Schema

Schema 问题是我们决定解决的第一个问题。没有“一刀切”的方案。在为 ClickHouse 设计导出器时,这是一个很难接受的现实。与任何大型数据库一样,您需要清楚地了解您要插入的内容以及您计划如何将其取回。当您针对您的用例优化您的 schema 时,ClickHouse 的性能最佳。

在 OpenTelemetry 数据的情况下,这一点甚至更重要。即使是 OpenTelemetry 的设计者在编写 SDK 时也面临着这个问题。我们如何将如此庞大的语言和工具生态系统融入到单个遥测管道中?每个团队都有自己搜索日志和追踪的模式,在建模表 schema 时需要考虑到这一点。数据保留多长时间?您的架构是倾向于按服务名称进行过滤,还是有另一个您必须按其分区的标识符?您是否需要用于按 Kubernetes pod 名称进行过滤的列?不可能包含所有人,我们不能为了做到这一点而牺牲性能和可用性。

“一刀切适用于大多数情况”是我们所能期望的最好结果,也是 ClickHouse 导出器的情况。为日志、追踪和指标提供了默认 schema。默认 schema 在大多数常见的遥测用例中都能很好地运行,但是如果您试图大规模构建日志记录解决方案,那么我们建议您花时间了解您的数据在 ClickHouse 中是如何存储和访问的,并选择一个相关的 primary key,就像我们为我们的内部日志记录解决方案所做的那样,该解决方案存储了超过 43 PB 的 OTel 数据(截至 2024 年 10 月)。

log_house_43pb.png

来自 LogHouse 的统计数据,ClickHouse Cloud 基于 OTel 的日志记录平台

默认情况下,导出器将为您创建所需的表,但这不建议用于生产工作负载。如果您想在不修改导出器代码的情况下替换表 schema,您只需自己创建表即可。配置文件仅定义数据将发送到的表名。这要求列名与导出器插入的名称匹配,并且类型与底层数据兼容。

{
  "Timestamp": "2024-06-15 21:48:06.207795400",
  "TraceId": "10c0fcd202c978d6400aaa24f3810514",
  "SpanId": "60e8560ae018fc6e",
  "TraceFlags": 1,
  "SeverityText": "Information",
  "SeverityNumber": 9,
  "ServiceName": "cartservice",
  "Body": "GetCartAsync called with userId={userId}",
  "ResourceAttributes": {
    "container.id": "4ef56d8f15da5f46f3828283af8507ee8dc782e0bd971ae38892a2133a3f3318",
    "docker.cli.cobra.command_path": "docker%20compose",
    "host.arch": "",
    "host.name": "4ef56d8f15da",
    "telemetry.sdk.language": "dotnet",
    "telemetry.sdk.name": "opentelemetry",
    "telemetry.sdk.version": "1.8.0"
  },
  "ScopeName": "cartservice.cartstore.RedisCartStore",
  "ScopeAttributes": {},
  "LogAttributes": {
    "userId": "71155994-7b72-428a-9d51-43962a82ae43"
  }
}

OpenTelemetry 生成的日志事件示例

如果您需要的 schema 与默认提供的 schema 显著不同,您可以使​​用 ClickHouse 物化视图。默认的表 schema 提供了一个可用的起点,但它们也可以被视为关于导出器可以公开哪些数据的指南。如果您要建模自己的表,您可以选择包含或排除某些列,甚至更改它们的类型。对于我们的内部日志记录,我们以此为契机提取了与 Kubernetes 相关的列,例如 pod 名称。然后,我们将此放入表的主键中,以优化我们特定查询模式的性能。

最好在生产部署中默认禁用表创建。如果您有多个导出器进程正在运行,它们将竞争创建表(可能使用不同的版本!)。在此用户指南中,我们列出了将 ClickHouse 用作可观测性存储的最佳实践。

下面,我们展示了我们用于 LogHouse(我们的 ClickHouse Cloud 日志记录解决方案)的自定义 schema

CREATE TABLE otel.server_text_log_0
(
	`Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
	`EventDate` Date,
	`EventTime` DateTime,
	`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)),
	`Namespace` LowCardinality(String),
	`Cell` LowCardinality(String),
	`CloudProvider` LowCardinality(String),
	`Region` LowCardinality(String),
	`ContainerName` LowCardinality(String),
	`PodName` LowCardinality(String),
	`query_id` String CODEC(ZSTD(1)),
	`logger_name` LowCardinality(String),
	`source_file` LowCardinality(String),
	`source_line` LowCardinality(String),
	`level` LowCardinality(String),
	`thread_name` LowCardinality(String),
	`thread_id` LowCardinality(String),
	`ResourceSchemaUrl` 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)),
	`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_thread_id thread_id TYPE bloom_filter(0.001) GRANULARITY 1,
	INDEX idx_thread_name thread_name TYPE bloom_filter(0.001) GRANULARITY 1,
	INDEX idx_Namespace Namespace TYPE bloom_filter(0.001) GRANULARITY 1,
	INDEX idx_source_file source_file TYPE bloom_filter(0.001) GRANULARITY 1,
	INDEX idx_scope_attr_key mapKeys(ScopeAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
	INDEX idx_scope_attr_value mapValues(ScopeAttributes) TYPE bloom_filter(0.01) 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 = SharedMergeTree
PARTITION BY EventDate
ORDER BY (PodName, Timestamp)
TTL EventTime + toIntervalDay(180)
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1;

LogHouse 的 OTel Schema,ClickHouse Cloud 日志记录解决方案

关于 LogHouse schema 的一些观察

  • 我们使用排序键 (PodName, Timestamp)。这针对我们的查询访问模式进行了优化,用户通常按这些列进行过滤。用户将希望根据他们预期的工作流程修改此设置。
  • 我们对所有 String 列使用 LowCardinality(String) 类型,除了那些基数非常高的列。此字典编码我们的字符串值,并已证明可以提高压缩率,从而提高读取性能。我们当前的经验法则是对任何基数低于 10,000 个唯一值的字符串列应用此编码。
  • 我们所有列的默认压缩编解码器是级别 1 的 ZSTD。这特定于我们的数据存储在 S3 上的事实。虽然与替代方案(如 LZ4)相比,ZSTD 的压缩速度可能较慢,但这可以通过更好的压缩比和始终如一的快速解压缩(约 20% 的方差)来弥补。当使用 S3 进行存储时,这些是更可取的属性。
  • OTel schema 继承而来,我们对任何 map 的键和值使用 bloom_filters。这为基于 Bloom 过滤器数据结构的 map 的键和值提供了二级索引。Bloom 过滤器是一种数据结构,它允许以空间效率高的方式测试集合成员资格,但会略微增加误报的可能性。从理论上讲,这使我们能够快速评估 磁盘上的 granules 是否包含特定的 map 键或值。此过滤器在逻辑上可能是有意义的,因为某些 map 键和值应与 pod 名称和时间戳的排序键相关联,即特定的 pod 将具有特定的属性。但是,其他属性将为每个 pod 存在 - 当查询这些值时,我们不希望这些查询被加速,因为 granule 中至少一个行满足过滤条件的可能性非常高(在此配置中,块是 granule,因为 GRANULARITY=1)。有关为什么排序键与列/表达式之间需要关联的更多详细信息,请参阅此处。此通用规则已用于其他列,例如 Namespace。通常,这些 Bloom 过滤器已被自由应用,需要进行优化 - 这是一项待处理的任务。0.01 的误报率也尚未调整。

后续步骤

ClickHouse 导出器还有改进的空间。我们的目标是使导出器与 ClickHouse 服务器的最新发展保持同步。随着新的优化被发现,以及新的性能基准被测试,我们可以找到改进日志、追踪和指标的默认 schema 的方法。

将影响许多可观测性用例的关键功能之一是 ClickHouse 最近对 新的 JSON 数据类型 的支持,这将简化属性的存储和搜索方式,以便用于日志和追踪。除了新功能外,OTel+ClickHouse 用户还经常在存储库中提交反馈,这在过去一年中促成了许多功能改进和错误修复。

附录:为 OpenTelemetry 贡献力量

开源的魔力在于社区的协作力量:通过为 OpenTelemetry 贡献力量,您可以直接影响和塑造可观测性的未来。无论是改进代码、增强文档还是提供反馈,每一项贡献都会扩大项目的影响范围,并使世界各地的开发人员受益。在本节中,我们将分享一些技巧。

society_oss.png

为 OpenTelemetry 贡献力量与其他大多数开源项目类似;您无需成为成员即可贡献。无论是分享您对某个问题的想法,还是打开一个 pull request,项目都欢迎所有贡献。

作为组件维护者,对某人来说最有价值的贡献实际上是最容易的:反馈。了解用户面临哪些错误,或者了解哪些功能差距会改善多个用户的体验,这非常有价值。虽然我们确实在内部使用了 ClickHouse 导出器,但我们的使用情况与其他人的使用情况不同,我们有很多东西要向社区学习。

当然,我们也有熟悉 OpenTelemetry 和 ClickHouse 的用户,他们能够为导出器做出巧妙的贡献。我们想重点介绍的一个例子是最近为在将 map 属性插入 ClickHouse 之前对其进行排序所做的努力。在以前的版本中,日志和追踪属性只是按接收到的顺序插入。这并不总是能带来最佳的压缩效果,因为属性可能反映相同的数据,但顺序不同。通过按键对 map 属性进行排序,我们能够从 ClickHouse 出色的压缩中受益。这是一个在外部问题中注意到的想法,但尚未添加。社区的一位用户发现了这一点,并提交了一个包含其实现的pull request

如果您发现自己经常与 OpenTelemetry 项目交互,那么成为他们组织的成员可能是一个好主意。在社区存储库中有一个关于该过程的完整指南,但总体思路是表明您实际上已经是成员了。成员资格申请是通过在 GitHub 上创建一个 issue,并附上您的贡献列表(issue、pull request 等)来提交的。如果现有成员同意,您的成员资格将被批准,您可以开始在组织内承担更大的角色。这不是做出贡献的必要条件,但它向其他成员和访问者表明您正在积极参与 OTel 生态系统。

分享这篇文章

订阅我们的新闻通讯

随时了解功能发布、产品路线图、支持和云产品!
正在加载表单...
关注我们
X imageSlack imageGitHub image
Telegram imageMeetup imageRss image