ClickHouseInc

2025年7月 ClickStack 有哪些新变化?

图片

本文字数:9779;估计阅读时间:25 分钟

作者:The ClickStack team

Image

本月初,我们发布了 ClickStack,这是一款基于 ClickHouse 构建的开源可观测性 (observability) 堆栈,让日志、指标、追踪和会话重放的统一变得比以往更加简单。其背后是同一款被 Netflix、eBay 等企业信赖的高性能引擎。

ClickStack 最大的优势之一在于,它将高基数、宽表事件数据的快速搜索与快速聚合结合在一个开源、低成本的解决方案中。通过采用 OpenTelemetry 进行数据采集,并以 HyperDX 作为 UI 层,ClickStack 提供了从数据摄取到强大可视化探索的完整开箱即用可观测性方案。

Image

在 ClickHouse 的世界里,一个月就能带来巨大的变化。自首次发布以来,我们持续不断地为 ClickStack 增添新功能和改进,致力于让它更强大、更易用。从本月开始,我们会定期分享最新进展,帮助你快速在 ClickStack 部署中应用这些最新改进。

Image

贡献者

首先,我们要感谢本月的新开源贡献者。开源堆栈的构建离不开社区的共同努力,感谢每一位为 ClickStack 贡献力量的朋友——无论是通过 OpenTelemetry collector、Helm chart,还是 HyperDX。我们对你们的付出表示衷心感谢。

Huynguyen-anduin, OhJuhun, mGolestan98

Image

ClickStack 支持 JSON 类型,查询速度更快

本月 ClickStack 的最大更新虽简单,但意义重大。我们为 ClickStack 新增了对 ClickHouse 原生 JSON 列类型的测试版支持。这一功能为基于 ClickHouse 的可观测性 (observability) 工作负载带来了全新的扩展性、性能和压缩能力——在我们的测试中,查询速度相比 Map 类型最高提升了 9 倍。

为什么需要支持 JSON?

在构建可观测性解决方案时,用户往往希望能够直接“发送事件”,而不必受限于严格的 Schema。实际情况是,可观测性数据来源复杂多样——不同的应用、团队,甚至组织,各自的结构不断演变。尽管结构化日志和 OpenTelemetry 等标准有所帮助,但团队依然需要捕获任意数量、类型和层级的标签与字段。

此前,ClickStack 中用于 OpenTelemetry 数据的 Schema 主要依赖 Map 类型,来处理如 LogAttributes、ResourceAttributes 和 SpanAttributes 这类包含动态属性的列。尽管这种方案可行,但也存在明显的不足:

  • 类型精度的损失:Map 的键和值都被存储为字符串,所有数据被强制转为单一类型。数值比较只能在查询时转换类型,既冗长又影响性能,同时占用更多内存。

  • 单列顺序扫描:使用 Map 类型时,所有 JSON 路径都存储在一个列中,查询某个键时需要加载并扫描整个 Map。当键数量众多时,导致大量不必要的 I/O,查询效率低下。用户通常通过物化列来预先提取需要的值以规避这一问题。

  • 不支持原生嵌套:Map 类型在处理原始值时尚可,但对嵌套的 Map 和数组支持较差,所有内容都被转成字符串。复杂数据需序列化成 JSON 字符串,查询繁琐且低效。例如,LogAttributes 结构中:

{  "service.name": "user-service",  "service.version": "1.2.3",  "http.status_code": "200",  "user.preferences": "{\"theme\":\"dark\",\"notifications\":{\"email\":true,\"push\":false}}",  "error.stack": "[\"AuthError: Invalid token\",\"  at validateToken (auth.js:45)\",\"  at middleware (app.js:23)\"]"}

像 service.name 和 http.status_code 这样的原始值虽然能被自然存储,但失去了类型信息,无法直接进行数值或日期计算。而类似 user.preferences 和 error.stack 的复杂嵌套数据则被转为 JSON 字符串,查询和分析十分不便。如果要从 user.preferences 中提取布尔值,必须在 Map 的字符串值上使用 JSONExtractBool。

SELECT JSONExtractBool(LogAttributes['user.preferences'], 'notifications', 'email')

通过 ClickHouse 25.3 中正式推出的 JSON 类型,ClickStack 现在可以原生摄取和存储真正的半结构化数据。每个独立的 JSON 路径都对应自己的子列,类型得以保留,I/O 显著减少,查询性能大幅提升。即使是深度嵌套或不断变化的 Schema,也能高效处理。

想了解 JSON 类型在可观测性领域的高层次应用,可以参考我们去年发布的文章[https://clickhouse.com/blog/evolution-of-sql-based-observability-with-clickhouse]。如果你对底层实现细节感兴趣,欢迎阅读我们最初发布的博客,里面有详细介绍[https://clickhouse.com/blog/a-new-powerful-json-data-type-for-clickhouse]。

Image

这意味着你可以直接将从 OpenTelemetry collectors 采集的增强型日志、追踪和指标发送至 ClickStack,无需担心严格的 Schema 或手动展开数据。ClickStack 的 UI 层 HyperDX 充分发挥了这一优势,支持更智能的过滤、JSON 路径的自动补全,以及更快速的搜索和可视化。

回到之前的示例——现在我们可以将所有数据以原始形式存储,完整保留其自然的结构与数据类型:

{  "service": {    "name": "user-service",    "version": "1.2.3"  },  "http": {    "status_code": 200  },  "user": {    "preferences": {      "theme": "dark",      "notifications": {        "email": true,        "push": false      }    }  },  "error": {    "stack": [      "AuthError: Invalid token",      "  at validateToken (auth.js:45)",      "  at middleware (app.js:23)"    ]  }}

查询也变得更加直观:

SELECT LogAttributes.user.preferences.notifications.email

返回的仍然是之前的布尔值,而且我们无需再去关心数据的嵌套层级或类型!

为 OpenTelemetry 添加支持

在支持 HyperDX 之前,我们首先需要确保 ClickStack 的每一层都能正确支持 JSON。要为 ClickHouse 的 OpenTelemetry exporter 增加支持,首先需要让 clickhouse-go 客户端支持 JSON 类型——exporter 在插入数据时使用这一客户端。客户端会将 JSON 列以序列化的 JSON 字符串插入,ClickHouse 再解析该字符串,自动识别新列及其类型。

此外,我们还对数据插入的逻辑进行了多项优化。我们更新了 Go 驱动,使其支持通过 ClickHouse 的原生 API 进行 TCP 和 HTTP 连接,这样在将 OpenTelemetry 的数据结构转换为 ClickHouse 的原生列式格式时,性能大幅提升。我们还借助 pprof 对主插入循环进行了性能分析和优化,大多数优化点集中在对 OpenTelemetry 结构中 getter 的冗余调用。

ClickHouse exporter 对 JSON 的支持最近已通过 feature flag 合并[https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/40547]。通过这一改动,所有原本使用 Map 类型的列现已全面切换为 JSON 类型。

为 HyperDX 添加支持

在 OpenTelemetry Collector 支持 JSON 类型后,我们将注意力转向了 UI 层 HyperDX 的适配。

为了在 HyperDX 中实现对 JSON 的完整支持,我们需要更新 UI 以支持嵌套对象,这在过去的 Map 类型中是难以实现的。因此,我们对前端的多个组件进行了重构,以便能够正确渲染和交互深度结构化的 JSON 数据。同时,部分查询也需要重写,采用新的 JSON 路径符号来高效访问嵌套字段。当前阶段,部分操作仍然需要将 JSON 值转换为字符串或浮点数,但未来我们计划引入更智能的元数据缓存机制,这样在字段类型始终一致的情况下,就可以完全跳过类型转换。

展望未来,我们还将继续优化 Schema,增设更多针对 JSON 工作负载的索引和性能优化,进一步提升 ClickStack 架构下的查询性能。

如何启用 JSON 支持?

目前,JSON 支持仍处于测试阶段,可以在部署任一堆栈模式时,通过 feature flag 启用[https://clickhouse.com/docs/use-cases/observability/clickstack/getting-started]。

docker run -e BETA_CH_OTEL_JSON_SCHEMA_ENABLED=true -e OTEL_AGENT_FEATURE_GATE_ARG='--feature-gates=clickhouse.json' -p 8080:8080 -p 4317:4317 -p 4318:4318 docker.hyperdx.io/hyperdx/hyperdx-all-in-one:2-nightly

启用 JSON 类型支持需要配置两个环境变量:

BETA_CH_OTEL_JSON_SCHEMA_ENABLED=true 用于在 ClickHouse 中启用 JSON 支持,OTEL_AGENT_FEATURE_GATE_ARG='--feature-gates=clickhouse.json' 用于在 OTel collector 中开启 JSON 数据摄取。

如果你想通过示例数据体验 JSON 类型,可以参考我们的入门指南[https://clickhouse.com/docs/use-cases/observability/clickstack/getting-started/sample-data],里面提供了公共数据集。我们非常期待听到你的反馈,欢迎在我们的公共仓库提交 issue 或建议[https://github.com/hyperdxio/hyperdx/tree/main]。

现在,JSON 已在整个堆栈中实现了支持,用户几乎无需感知就能立即受益。

需要注意的是,JSON 类型的引入带来了全新的 Schema,与旧的基于 Map 的 Schema 不兼容。希望使用 JSON 的用户,可以选择在 HyperDX 中为基于 JSON 的 Schema 配置单独的数据源,或通过简单的 INSERT INTO ... SELECT 操作来迁移现有数据。

整体的用户体验几乎没有变化,不过在查看如 LogAttributes 等属性列时,视觉上会有一些细微的不同。以前的层级结构会被扁平化,子层级通过点符号表示(例如 k8s.pod.name),而现在,属性结构以完整的层级形式呈现。

Image

基于 Map 类型的属性渲染

Image

基于 JSON 类型的层级属性渲染

那么 JSON 类型到底有多大提升?

得益于 HyperDX 对列自动补全和类 Lucene 查询构造的支持,用户最直观的收益就是 OTel 列(如 LogAttributes 等属性)上的查询速度大幅提升,I/O 明显减少。更重要的是,属性的真实数据类型得以保留,不再像之前那样全部转换为字符串。这意味着,当你传入数值或日期类型的属性时,类型信息不会丢失,能够直接高效地进行比较。

为了让这种优势更直观,我们以 ClickStack playground(play-clickstack.clickhouse.com,底层是 sql.clickhouse.com 的 ClickHouse 实例)中的示例日志 OTel 数据集为例,该数据集大约有 9000 万行。以下是基于 Map 类型的 Schema:

CREATE TABLE otel_v2.otel_logs(    `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)),    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_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_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 8)ENGINE = MergeTreePARTITION BY toDate(TimestampTime)PRIMARY KEY (ServiceName, TimestampTime)ORDER BY (ServiceName, TimestampTime, Timestamp)

对应的基于 JSON 类型的 Schema:

CREATE TABLE otel_json.otel_logs(    `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 CODEC(ZSTD(1)),    `ScopeSchemaUrl` LowCardinality(String) CODEC(ZSTD(1)),    `ScopeName` String CODEC(ZSTD(1)),    `ScopeVersion` LowCardinality(String) CODEC(ZSTD(1)),    `ScopeAttributes` JSON CODEC(ZSTD(1)),    `LogAttributes` JSON CODEC(ZSTD(1)),    INDEX idx_body Body TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 8)ENGINE = MergeTreePARTITION BY toDate(TimestampTime)PRIMARY KEY (ServiceName, TimestampTime)ORDER BY (ServiceName, TimestampTime, Timestamp)

你会发现,新的 Schema 明显简洁了不少,主要是因为 Map 类型的键和值不再需要布隆过滤器。ResourceAttributes、ScopeAttributes 和 LogAttributes 中的各个字段现在都作为独立的子列存储,布隆过滤器因此变得不再必要。

比如我们希望查看每个 pod 的日志流量。虽然 HyperDX 会自动生成查询语句,但底层的 SQL(这里是简化版)本质上是一次聚合操作。使用 Map 类型时:

SELECT    ResourceAttributes['k8s.pod.name'] AS pod_name,    count() AS cFROM otel_v2.otel_logs WHERE pod_name !=''GROUP BY pod_nameORDER BY c DESCLIMIT 10

这里有几个关键点:查询耗时 2.7 秒,扫描了整个数据集,内存占用 1.6GB,读取的数据量高达 30GB!

这种巨量的数据读取正是 Map 类型的瓶颈所在。因为所有子字段都存储在同一个 LogAttributes 列中,哪怕只要 JSON 字段 'k8s.pod.name',也不得不读取整列。

相比之下,使用 JSON 类型查询同一字段时,'k8s.pod.name' 已经作为单独的列存在。

SELECT    CAST(ResourceAttributes.k8s.pod.name, 'String') AS pod_name,    count() AS cFROM otel_json.otel_logs WHERE pod_name !=''GROUP BY pod_nameORDER BY c DESCLIMIT 10

性能提升极其显著——查询速度快了 9 倍。虽然处理的行数一样,但实际读取的数据量减少了超过 10 倍,仅读取了所需列的对应值。

Image

其他改进

虽然 JSON 类型是本月 ClickStack 更新的明星功能,但我们的优化远不止于此。我们在 ClickStack 的多个细节上都做了精心打磨,虽然每项改动都不算巨大,但整体体验却因此变得更加流畅和愉悦。以下是一些亮点更新。

Helm chart 的增强

本月,我们对 HyperDX 的 Helm chart 进行了多项优化,帮助用户部署更灵活、更易掌控。主要更新包括:支持接入自有的 MongoDB 服务[https://github.com/hyperdxio/helm-charts/pull/29]、配置自定义的 ClickHouse 用户凭据[https://github.com/hyperdxio/helm-charts/pull/26]、优化 PVC 配置[https://github.com/hyperdxio/helm-charts/pull/32]。此外,还增加了对 nodeSelector 和 tolerations 的支持[https://github.com/hyperdxio/helm-charts/pull/56]、服务类型及注解的配置能力[https://github.com/hyperdxio/helm-charts/pull/57]、HyperDX [https://github.com/hyperdxio/helm-charts/pull/44]及应用[https://github.com/hyperdxio/helm-charts/pull/41]的 ingress 能力增强,以及 OpenTelemetry collector 支持设置自定义环境变量[https://github.com/hyperdxio/helm-charts/pull/46]。这些改进让用户可以根据自身基础设施与工作负载的特点,更好地定制 HyperDX。

搜索结果支持 CSV 下载

针对用户呼声较高的需求,我们新增了搜索结果的导出功能。基于性能考虑,目前支持导出最多 4000 行,但这对于需要进一步分析或共享数据的用户而言,已非常实用。导出文件为 CSV 格式,方便直接在表格或其他工具中使用。

Image

Elastic 迁移指南

应社区的需求,我们发布了 Elastic Stack 迁移指南[https://clickhouse.com/docs/use-cases/observability/clickstack/migration/elastic],帮助希望从 Elastic(前称 Elastic Stack 或 ELK Stack)迁移的用户。迁移后的优势显而易见:更好的压缩率、更高的资源利用率,以及更低的总体拥有成本。尽管迁移过程中可能存在一定复杂性,但我们力求让过程尽可能平滑。指南中详细对照了两种技术的概念映射,并提供了语言 SDK 和代理迁移的清晰路径,帮助用户快速完成切换。未来几个月,我们还会陆续推出针对其他传统可观测性方案的迁移指南,敬请期待。

Docker 镜像体积优化

为了让大家的入门体验更轻松,我们也致力于缩减容器镜像的体积。最早的用户反馈之一就是 all-in-one 镜像体积太大。现在,我们已经将其从 3GB 缩减至 2.2GB,应用本身的体积也从约 2GB 减至 1.2GB,几乎瘦身一半。我们仍在不断优化,持续让镜像更轻、更快,助你几乎无障碍地快速上手。

/END/

试用阿里云 ClickHouse企业版

轻松节省30%云资源成本?阿里云数据库ClickHouse 云原生架构全新升级,首次购买ClickHouse企业版计算和存储资源组合,首月消费不超过99.58元(包含最大16CCU+450G OSS用量)了解详情:https://t.aliyun.com/Kz5Z0q9G

图片
图片

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]

图片图片