Jaeger ClickHouse 存储后端基准测试指南:1 亿 Span 数据集下的压缩率、写入吞吐与查询延迟
【免费下载链接】jaegerCNCF Jaeger, a Distributed Tracing Platform项目地址: https://gitcode.com/GitHub_Trending/ja/jaeger
本篇指南完整解读 Jaeger 官方仓库中 BENCHMARKING.md 记录的 ClickHouse 存储后端基准测试:在 1000 万 Span(100 万 Trace)真实数据集上,该后端取得了 8.6x 的数据压缩率、约 5.2 万 spans/sec 的写入吞吐,以及毫秒级服务/操作检索和百毫秒级按 Trace ID 取数的查询延迟。读完本文,你将理解这些数字背后的表结构与索引设计、掌握复现基准测试的方法,并学会如何为你的 ClickHouse 集群预估容量与查询性能。
基准测试概览
本基准测试面向 Jaeger 新一代存储架构(internal/storage/v2)中的 ClickHouse 存储后端(internal/storage/v2/clickhouse)。它回答了一个关键问题:当 ClickHouse 作为 Jaeger 的追踪数据仓库时,在海量 Span 数据下能获得怎样的存储效率与查询性能?
测试结果分为三部分:
| 维度 | 关键结果 |
|---|---|
| 压缩率 | spans 表 5.99 GiB 未压缩数据被压缩至 722.09 MiB,压缩比 8.6x |
| 写入吞吐 | 1000 万 Span 总计耗时 191.8 s,平均 52,129 spans/sec |
| 查询延迟 | 服务/操作检索 3–4 ms,Trace ID 取数 101 ms,属性搜索 1,769 ms |
以下各节将结合仓库源码逐项解读这些数字的由来与含义。
测试环境与数据集
测试环境
基准测试在一台 Oracle Cloud 的 VM.Standard2.4 实例上执行,硬件配置如下:
| 组件 | 详情 |
|---|---|
| VM | Oracle Cloud VM.Standard2.4(4 OCPUs,Intel Xeon Platinum 8167M) |
| 内存 | 60 GB |
| 磁盘 | 47 GB 块存储 |
| 操作系统 | Oracle Linux 9 |
| ClickHouse | 26(单节点) |
需要注意,这是单节点部署:4 个 vCPU 的规格意味着基准结果反映的是中小型实例上的性能表现,生产环境通过水平扩展(多节点分片/副本)可获得更优吞吐。
数据集
| 参数 | 值 |
|---|---|
| Trace 总数 | 1,000,000 |
| 每个 Trace 的 Span 数 | 10(1 个父 Span + 9 个子 Span) |
| Span 总数 | 10,000,000 |
| 服务数 | 10 |
| 分区数(按天) | 10 |
| 每个 Span 的属性数 | 11(分布于 97 个不同 key、1000 个不同值) |
数据集覆盖了 10 天时间窗口,对应 spans 表PARTITION BY toDate(start_time)的按天分区设计(见 create_spans_table.sql)。97 个属性 key 与 1000 个属性值的规模,对属性元数据缓存与类型化属性查询构成真实压力。
存储压缩率:8.6x 从何而来
实测结果
| 指标 | 值 |
|---|---|
| 未压缩大小 | 5.99 GiB |
| 压缩后大小 | 722.09 MiB |
| 压缩比 | 8.6x |
压缩率主要来自三个设计选择:
- 列式存储天然压缩友好:ClickHouse MergeTree 按列压缩,同类数据(如
service_name)聚集在一起,重复值压缩效率极高。 - 类型化属性数组:Jaeger 的 ClickHouse 后端没有像 OTel-contrib 实现那样把所有属性值统一转成字符串,而是将 key/value 拆分并按类型分列存储——
bool_attributes、double_attributes、int_attributes、str_attributes、complex_attributes各自使用Array(Bool)、Array(Float64)、Array(Int64)、Array(String)等列(见 create_spans_table.sql)。同类型数值列可比混合类型字符串列获得更好的压缩比。 - 复合类型 JSON 序列化:slice/map 等复杂类型统一序列化为 JSON 字符串存入
complex_attributes,避免深层嵌套结构的存储开销。
从源码看表结构
spans 表是追踪数据的主表,其核心设计(create_spans_table.sql):
CREATE TABLE IF NOT EXISTS spans ( id String, trace_id String, ... start_time DateTime64(9), duration Int64, bool_attributes Nested (key String, value Bool), double_attributes Nested (key String, value Float64), int_attributes Nested (key String, value Int64), str_attributes Nested (key String, value String), complex_attributes Nested (key String, value String), events Nested (...), links Nested (...), ... INDEX idx_trace_id trace_id TYPE bloom_filter GRANULARITY 1, INDEX idx_duration duration TYPE minmax GRANULARITY 1 ) ENGINE = MergeTree PARTITION BY toDate(start_time) ORDER BY (service_name, name, toDateTime(start_time))两个跳过索引直接服务于基准测试中的两类查询:
idx_trace_id(bloom_filter)让「按 Trace ID 取数」不必全表扫描;idx_duration(minmax)让「按持续时间范围搜索」能快速裁剪数据块。
排序键(service_name, name, toDateTime(start_time))则与「按服务搜索」「按操作搜索」的查询模式对齐。这一表结构与 OTel-contrib 的 ClickHouse exporter 相比有显著差异,详见 README.md 中的说明。
写入吞吐:52,129 spans/sec
| 指标 | 值 |
|---|---|
| Span 总数 | 10,000,000 |
| 总写入耗时 | 191.8 s |
| 吞吐量 | 52,129 spans/sec |
写入路径的高吞吐依赖两个关键机制:
ch-go批量插入:写操作使用ch-go的chpool以 batch 模式写入(见 README.md)。在 writer.go 中,WriteTraces首先PrepareBatch,将ResourceSpans → ScopeSpans → Spans逐层遍历后逐条batch.Append,最后一次性batch.Send,将网络往返次数压缩到最低。- 按类型拆分属性列:
dbmodel.ToRow将 span 的属性、Resource 属性、Scope 属性、Event 属性、Link 属性分别转换为类型化 key/value 数组(见 dbmodel/from.go),写入时直接对齐InsertSpan语句中 60 余个占位符列(见 sql/queries.go),避免了类型转换开销。
写入侧的连接专门用于写、不开启埋点插桩(见 writer.go 注释),防止追踪自身产生递归写入流量。
查询延迟:Retrieval 与 Search 两个维度
基准测试将查询分为两类,每个查询运行 3 次取平均:
Retrieval Queries(按 ID/名称直取)
| 查询 | 平均耗时 |
|---|---|
| 获取服务列表(Retrieve services) | 3 ms |
| 获取操作列表(Retrieve operations) | 4 ms |
| 按 Trace ID 取 Trace(Get trace by ID) | 101 ms |
服务与操作检索的毫秒级延迟来自两张物化视图表:
- services +
create_services_mv.sql物化视图,配合SELECT name FROM services GROUP BY name查询(queries.go); - operations +
create_operations_mv.sql物化视图,按service_name与span_kind过滤(queries.go)。
按 Trace ID 取数耗时 101 ms,由SelectSpansByTraceID(SELECT ... FROM spans s WHERE s.trace_id = ?,见 queries.go)配合idx_trace_idbloom_filter 索引支撑。取数后还需在 Go 侧完成 span 行反序列化与 trace 组装(reader.go)。
Search Queries(带过滤条件的搜索)
| 查询 | 平均耗时 |
|---|---|
| 按服务搜索 | 37 ms |
| 按操作搜索 | 38 ms |
| 按持续时间范围搜索 | 43 ms |
| 按时间戳范围搜索 | 47 ms |
| 按属性搜索 | 1,769 ms |
| 按全部条件组合搜索 | 139 ms |
搜索路径先执行buildFindTraceIDsQuery构造「查 Trace ID」的子查询,再用buildFindTracesQuery将子查询嵌入WHERE s.trace_id IN (...)外层取数(query_builder.go)。内部子查询基于SearchTraceIDsBase(WHERE 1=1便于无条件下追加AND条件,见 queries.go),并以trace_id_timestamps表 JOIN 补全 Trace 的起止时间(queries.go)。
属性搜索(1,769 ms)显著慢于其他查询,原因是属性查询需要对嵌套数组执行arrayExists((key, value) -> key = ? AND value = ?, ...)的逐行扫描(query_builder.go),且需依次检查 span、resource、scope、events、links 五个层级(buildSimpleAttributeCondition,query_builder.go)。若读者关注属性检索性能,这是当前实现的主要瓶颈所在。
值得注意,组合全部条件反而快于单独属性搜索(139 ms vs 1,769 ms):额外的过滤条件(服务名、时间范围等)借助排序键与分区裁剪大幅缩小了需要扫描的 Span 集合。这提示一个实用调优思路——属性搜索应尽量与其他条件组合使用。
字符串属性的类型还原机制
基准数据集中 11 个属性/span 分布于 97 个 key,字符串属性的查询走buildStringAttributeCondition路径:查询服务以AsString()形式传入所有属性,后端需要查 attribute_metadata 表获知该 key 实际存储的类型(bool/double/int/str/bytes/map/slice)与层级(resource/scope/span),再通过strconv反向解析为正确类型生成条件(query_builder.go)。属性元数据本身带有 LRU 缓存,TTL 与容量分别由配置项attribute_metadata_cache_ttl(默认 1h)与attribute_metadata_cache_max_size(默认 1000)控制(config.go)。
复现基准测试
原文档给出如下复现指引:
See the clickhouse-benchmarking repository for setup and reproduction instructions.
(该仓库为 Jaeger 团队成员的独立 benchmark 工程,包含环境搭建、数据集生成、schema 初始化与查询脚本。)
在复现前,你需要先在本仓库中启动 ClickHouse 存储后端。最直接的方式是使用官方示例配置 config-clickhouse.yaml,它演示了jaeger_storage扩展的完整配置:
extensions: jaeger_storage: backends: some-storage: clickhouse: addresses: - localhost:9000 database: jaeger auth: basic: username: default password: password create_schema: true其中create_schema: true会执行本仓库 sql 目录 下全部建表与物化视图脚本(spans、services、operations、trace_id_timestamps、attribute_metadata、dependencies 及其物化视图),这些 SQL 通过go:embed嵌入二进制(queries.go)。
关键配置项
ClickHouse 后端的完整配置项定义在 config.go,复现基准时可重点关注:
| 配置项 | 默认值 | 说明 |
|---|---|---|
protocol | native | 连接协议,可选native/http |
addresses | 必填 | ClickHouse 服务器地址列表 |
database | jaeger | 目标数据库 |
auth.basic | 无 | 用户名/密码认证 |
create_schema | false | 启动时自动建表 |
default_search_depth | 1000 | 未指定 limit 时的默认搜索深度(返回的 Trace ID 上限) |
max_search_depth | 10000 | 允许的最大搜索深度 |
attribute_metadata_cache_ttl | 1h | 属性元数据缓存 TTL,0 表示永不过期 |
attribute_metadata_cache_max_size | 1000 | 属性元数据缓存条目上限,0 表示禁用 |
ttl | 0 | Span 数据自动删除的 TTL,0 表示禁用 |
配置校验逻辑(config.go)要求:ttl必须为非负且为整秒数;default_search_depth与max_search_depth必须为正数(否则每次搜索都会返回空结果)。搜索深度限制在 query_builder.go 中强制执行——超过max_search_depth的查询会被直接拒绝。
数据生成与写入
复现 1000 万 Span 的数据集有两种途径:
- 外部 benchmark 仓库:按其
setup/native目录指引生成原生 schema 与数据集(原文档推荐路径)。 - 仓库自带工具:使用 cmd/tracegen 生成追踪数据并经 OTLP 接收器写入 Jaeger;写入路径会经过 writer.go 的批量插入逻辑,与基准测试的写入侧行为一致。
读基准测试时应关注的要点
- 环境差异:4 vCPU/60 GB 单节点配置下测得的结果,在更大规格或集群部署下数字会变化,请勿直接外推为生产容量承诺。
- 属性搜索是当前热点:1,769 ms 的属性单独搜索耗时明显高于其他查询,若你的业务大量依赖属性过滤,应结合
attribute_metadata缓存命中率与查询组合方式做针对性优化。 - 压缩率支撑成本评估:8.6x 压缩比意味着 5.99 GiB 原始数据仅需约 0.7 GiB 存储,可作为磁盘容量规划的下限参考。
- 复现的起点在仓库内:从 config-clickhouse.yaml 出发搭建环境,再运行 benchmark 仓库的 schema 与查询脚本,即可得到可对比的数字。
参考源码索引
- 后端入口与配置:factory.go、config.go
- 建表与物化视图 SQL:sql 目录
- 写入实现:tracestore/writer.go
- 读取实现:tracestore/reader.go
- 查询构造器:tracestore/query_builder.go
- 类型化属性映射:tracestore/dbmodel/from.go
- 指标(RED 指标)存储:metricstore/reader.go
- 官方部署配置示例:cmd/jaeger/config-clickhouse.yaml、cmd/jaeger/config-spm-clickhouse.yaml
- 官方集成测试:cmd/jaeger/internal/integration/clickhouse_test.go
- 本基准测试文档:BENCHMARKING.md
【免费下载链接】jaegerCNCF Jaeger, a Distributed Tracing Platform项目地址: https://gitcode.com/GitHub_Trending/ja/jaeger
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考