数月之前我们公布了关于用 VictoriaLogs 充当 Traces 数据的存储的调研,从 PoC 的角度看,这是一次很不错的尝试,但是我们意识到,这离真正可用的产品形态还差很远。
因此在这段时间内,这个 PoC 项目发生了如下的变化:
同时,我们还收到了很多用户关于查询性能的疑问,因为在上一篇博客中,查询性能只被简单地提及过 —— 是的,那是一轮不够严谨的测试。我们通过简单观察不同 Traces 后端的响应速度,得出 VictoriaLogs 的查询性能不逊色于竞争对手,这当然没有说服力。
所以,这篇博客中,让我们一起来探索一下 Traces 在不同的后端中是如何查询的。
回想一下,开发者们是如何使用 Traces 的:
这是 Traces 中两个最常见的查询场景,那么数据 Schema 的设计就应该围绕着它们进行。
想要加速 TraceID 的查询,那数据就应该按照 TraceID 进行排列,而其余数据可以分多列存储,也可以编码成 Binary 或者 JSON 存入一列。另一方面,想要在时间范围内按照 Span 的 Attributes (如耗时、状态)进行查询,那这些数据应该按时间排序,并且相关 Attributes 就应该作为单独的列,提供检索的能力。
如果我们观察 VictoriaTraces 和其他 Traces 后端的 Schema ,可以看出来它们的优化倾向各不相同。
ClickHouse 是多个 Traces 后端都选用的存储方案。在 Jaeger 的设计中,ClickHouse 有两个关键的表:
<details>CREATE TABLE spans_table
(
`timestamp` DateTime CODEC(Delta, ZSTD(1)),
`traceID` String CODEC(ZSTD(1)),
`model` String CODEC(ZSTD(3))
)
ENGINE = MergeTree
PARTITION BY toDate(timestamp)
ORDER BY traceID
SETTINGS index_granularity = 1024;
CREATE TABLE spans_index_table
(
`timestamp` DateTime CODEC(Delta, ZSTD(1)),
`traceID` String CODEC(ZSTD(1)),
`service` LowCardinality(String) CODEC(ZSTD(1)),
`operation` LowCardinality(String) CODEC(ZSTD(1)),
`durationUs` UInt64 CODEC(ZSTD(1)),
`tags` Nested(key LowCardinality(String), value String) CODEC(ZSTD(1)),
INDEX idx_tag_keys tags.key TYPE bloom_filter(0.01) GRANULARITY 64,
INDEX idx_duration durationUs TYPE minmax GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(timestamp)
ORDER BY (service, -toUnixTimestamp(timestamp))
SETTINGS index_granularity = 1024;
</details>
很显然,这是针对 TraceID 查询优化的,在 spans_table
中,数据按日分区,按 TraceID 排序,因此通过 TraceID 可以快速取出数个连续的 Spans 。而通过 TraceID 外的条件查询数据时,先在 spans_index_table
中找到 TraceID ,再回到 spans_table
中取出完整数据。
作为 ClickHouse 的亲儿子,ClickStack 的 Schema 完全按照 OpenTelemetry 定义,让每个属性都拥有对应的字段:
<details>CREATE TABLE otel_traces
(
`Timestamp` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
`TraceId` String CODEC(ZSTD(1)),
`SpanId` String CODEC(ZSTD(1)),
`ParentSpanId` String CODEC(ZSTD(1)),
`TraceState` String CODEC(ZSTD(1)),
`SpanName` LowCardinality(String) CODEC(ZSTD(1)),
`SpanKind` LowCardinality(String) CODEC(ZSTD(1)),
`ServiceName` LowCardinality(String) CODEC(ZSTD(1)),
`ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
`ScopeName` String CODEC(ZSTD(1)),
`ScopeVersion` String CODEC(ZSTD(1)),
`SpanAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)),
`Duration` Int64 CODEC(ZSTD(1)),
`StatusCode` LowCardinality(String) CODEC(ZSTD(1)),
`StatusMessage` String CODEC(ZSTD(1)),
`Events.Timestamp` Array(DateTime64(9)) CODEC(ZSTD(1)),
`Events.Name` Array(LowCardinality(String)) CODEC(ZSTD(1)),
`Events.Attributes` Array(Map(LowCardinality(String), String)) CODEC(ZSTD(1)),
`Links.TraceId` Array(String) CODEC(ZSTD(1)),
`Links.SpanId` Array(String) CODEC(ZSTD(1)),
`Links.TraceState` Array(String) CODEC(ZSTD(1)),
`Links.Attributes` Array(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_span_attr_key mapKeys(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_span_attr_value mapValues(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1,
INDEX idx_duration Duration TYPE minmax GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Timestamp)
ORDER BY (ServiceName, SpanName, toUnixTimestamp(Timestamp), TraceId);
</details>
如果需要按照 TraceID 查询怎么办呢?是不是要在所有 Partition 中都找一遍?显然太低效了。因此,ClickStack 还会将每个 Span 的时间记录到 otel_traces_trace_id_ts
表,并且创建物化视图,这样,每个 TraceID 的起始和结束时间就很容易确定了,有效加速了以 TraceID 在 otel_traces
表的查询速度。
CREATE TABLE otel_traces_trace_id_ts
(
`TraceId` String CODEC(ZSTD(1)),
`Start` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
`End` DateTime64(9) CODEC(Delta(8), ZSTD(1)),
INDEX idx_trace_id TraceId TYPE bloom_filter(0.01) GRANULARITY 1
)
ENGINE = MergeTree
ORDER BY (TraceId, toUnixTimestamp(Start));
CREATE MATERIALIZED VIEW otel_traces_trace_id_ts_mv TO otel_traces_trace_id_ts
(
`TraceId` String,
`Start` DateTime64(9),
`End` DateTime64(9)
)
AS SELECT
TraceId,
min(Timestamp) AS Start,
max(Timestamp) AS End
FROM otel_traces
WHERE TraceId != ''
GROUP BY TraceId;
</details>
VictoriaTraces 目前的设计近似于 ClickStack ,在上一篇博客中提到过,尽可能将所有 Attributes 平铺为 Fields ,而 Fields 正接近于 Column-oriented 数据库中“列”的概念。
同样,单纯这样的设计并不能应对 TraceID 查询的场景,因此,我们又增加了一个单独的 Index Stream ,VictoriaTraces 在遇见每个新的 TraceID 的时,都会在这个 Stream 中增加一条记录 (Timestamp, TraceID)
。这个 Stream 非常小,行数为 Trace 的总量,因此在这个 Stream 中按照 TraceID 查询会很快。在找到 TraceID 对应的 Timestamp 后,以此为中心,在各个 Stream 中查询 ±45 秒内的数据,获取 TraceID 对应的所有 Spans 。
这个设计只是作为加速 TraceID 查询的概念验证,它有很多显而易见的缺点:
不过,聪明的读者一定也知道所有的设计都有其长处和短板,问题在于是否值得:
我们一定会在后续版本保持探索,调整这些设计,将它变得更加适合不同的使用场景。不过在那之前,不如先回到今天的主题——它们到底查询性能如何?
为了生成大量更贴合生产环境的测试数据,我们一开始打算部署多个 OpenTelemetry Demo。该 Demo 是基于 14 个微服务的分布式系统,覆盖了不同的编程语言、不同的插桩方式,产生的数据比过往使用的 Jaeger tracegen 更具有代表性。
但是在运行一段时间后,我们发现 OpenTelemetry Demo 需要消耗较多的资源,并且产生的压力有限。因此,我们又基于流量录制回放的思路编写了 vtgen,它可以:
vtgen 既可以用于 Traces 后端的写入性能 Benchmark ,也可以为不同 Traces 后端写入完全一致的数据,并随机记录一定量的 TraceID ,用于查询性能 Benchmark 。
我们在对 VictoriaTraces 的 Benchmark 中仍然选用了 Grafana Tempo 及 Jaeger & ClickHouse 作为对比,写入相同的的数据,其中数据写入过程的监控监控记录如下图,读者也可以在 Grafana Dashboard 快照中查阅。
我们在第一节中介绍过,最常见的 Traces 查询场景包括:
因此对应地:
{{<admonition type=note title="为什么属性搜索对比中没有 Tempo ?">}}
VictoriaTraces 、ClickHouse 均可以支持 Jaeger 的 Search API ,而 Tempo 同样提供 Search API ,但是两个 Search APIs 的返回数据格式并不一致:
因此,它们无法直接对比查询性能。
不过,Tempo 的 Search API 设计实际上更简洁高效,所以,我们会在 VictoriaTraces 实现 Tempo API 后将其进行补充对比。
{{< /admonition >}}
通过上面的图表,我们可以看到 VictoriaTraces 相比一些主流 Traces 后端的性能如何。
与 Jaeger & ClickHouse 的组合相比,如第二节中所分析,因为它们的查询优化方向不同,所以在两种场景中的表现互有胜负。ClickHouse 使用了 2 倍于 VictoriaTraces 的存储空间( 162 GiB vs. 79 GiB )来换取根据 TraceID 查询的速度,舍弃了在 Traces Search 场景的性能。
通过这些测试,我们已经知道 VictoriaTraces 与不同竞品的差异 —— 既有设计上的原因,也有优化上的不足。我们当然需要继续迭代 VictoriaTraces ,希望它能够更早抵达稳定版本。
所以,在性能上:
同时,在功能上,我们希望:
期待能在博客评论区或 VictoriaTraces 的 Issues 中收到你的反馈,也期待与你在下一期开发者笔记中再会!
1
DLOG 30 天前
牛!
|
![]() |
2
encro 30 天前 ![]() 比 signoz 与 greptimedb 呢
|
![]() |
3
RedisMasterNode OP @encro 希望未来有时间测试,但是现在开发时间不够用,没有多余的时间对比更多的产品
|
4
tuimaochang 30 天前
牛逼
|
![]() |
5
qW7bo2FbzbC0 30 天前
这个可以查询展示关系图吗
|
![]() |
7
RedisMasterNode OP > 这个可以查询展示关系图吗
@qW7bo2FbzbC0 如果说的是 Trace 查询展示 -> 现在有提供 Jaeger 的接口,可以在 Grafana 展示,或者代替 Jaeger 后端,在 Jaeger UI 展示。 如果说的是所有服务之间的调用关系总览、实时流量 -> 现在还不行,VictoriaTraces 目前只是个非常简单的 Traces Storage 。我们讨论过不同的 Service Map/Dependency Graph 的实现方案,但是还没有定论,具体或许看看哪种方案更高效才能继续推进。 |
![]() |
8
RedisMasterNode OP ![]() @Nanosk 是滴,不过如文中所说,不同产品设计的 ClickHouse 的 schema 不同,所以在不同场景里的查询性能也不同,取决于产品希望往什么方向优化。
Signoz 的博客介绍的 schema 是: https://signoz.io/docs/userguide/writing-clickhouse-traces-query/ 这也是配合 Signoz 里所需要的图表来设计的,具体性能相互比较一下也无妨 :) 如果后面有时间的话 |
![]() |
9
NikaidoIsAGod 30 天前
想请问下:
1. trace 插入的吞吐量如何。clickhouse 在大量数据插入时会发生 too many part 的问题,但是如果使用 async insert 的话会导致一定的数据延迟,影响吞吐量,是否有关于 batch insert 相关的 benchmark? 2. 对于物化表的支持如何? |
![]() |
10
RedisMasterNode OP @NikaidoIsAGod See:
1. https://jiekun.dev/posts/dev-note-distributed-tracing-with-victorialogs/#4-data-ingestion-%E6%80%A7%E8%83%BD 2. https://snapshots.raintank.io/dashboard/snapshot/j4g2kxHXxpOnXe8ogCyGeBCH7WgMCvPn PS: VictoriaTraces 不是 ClickHouse ,没有物化视图的说法,也不是像 ClickHouse 那样的“数据库”。VictoriaTraces 只是一个专门设计接收和存储 OTLP Traces 数据的 Backend ,并且提供查询接口。 PPS:上面的数据写入在 VictoriaTraces 里还没有进行过优化,还有很大的提升空间。 |
![]() |
11
RedisMasterNode OP @NikaidoIsAGod 在第一个链接中,ClickHouse 收到的请求是由 Jaeger 分了 Batch 的,而 VictoriaTraces 的请求是直接来自于 Client 的并发请求。
|
![]() |
12
NikaidoIsAGod 30 天前
@RedisMasterNode opentelmetry collector 有个 servicegraph connector ,将 trace span 转换成 servicemap 的 edge metric 这个思路其实还不错
|
![]() |
13
RedisMasterNode OP @NikaidoIsAGod 实时的分析会很耗费资源,因为它需要 buffer 一个 trace 的所有 span 一段时间,并且也不确定这个 trace 是否已经完结。servicegraphconnector 如果需要承接数千万的 span 肯定会很困难。不过不管哪种方案,用 connector 分析,还是用持久化的数据异步分析,都各有优劣吧没有说优先用哪种。
|
![]() |
14
takanashisakura 30 天前 ![]() 牛逼,dalao 居然在站内。先前给自己 nas 做监控就是用的 VictoriaMetrics+grafana
|
![]() |
15
NikaidoIsAGod 30 天前 ![]() @RedisMasterNode 确实,但是对于服务地图的场景来说其实并不用 buffer 所有的 trace span ,只需要 buffer “一对 span“ 即可。即 server span - client span 。并且在 buffer 的时候 drop 掉不需要的 attribute ,所需要的内存就会小很多了。亲测在 20w span qps 的压力下。servicegraph 也就吃个大概 10g 不到的内存。当然这个也和业务有关
|
![]() |
16
RedisMasterNode OP @NikaidoIsAGod 或许 service map 和 tail sampling 可以实现在未来的 vtagent 里面,看能不能比 otel collector 更小巧高效。不过短期内团队还是优先关注读写性能,毕竟项目还新,一下子做不好太多事情
|
![]() |
17
NikaidoIsAGod 30 天前
@RedisMasterNode 加个 v 吗,有些问题想请教下
|
![]() |
18
RedisMasterNode OP Sure. See:
https://jiekun.dev/wechat/ |
![]() |
19
RedisMasterNode OP |
![]() |
20
NikaidoIsAGod 30 天前
@RedisMasterNode thx
|
21
flamingooo 30 天前
但实际上我选择 ck 主要考虑, 是为了保证 '查询性能可用的前提', 得到尽可能多的写入性能跟压缩能力, 对于 log 跟 trace 其实相比 metric 我会更偏向这里.(
|
![]() |
22
RedisMasterNode OP @flamingooo 上一篇博客已经介绍过 Ingestion 的性能对比,这篇博客是续集,关注查询性能。
不太明白您的意思,因为 ClickHouse 的写入不是更好的那个。 |
![]() |
23
RedisMasterNode OP @flamingooo BTW 如果选择的是 Jaeger 的 ClickHouse schema ,它们的磁盘用量几乎是 2x 之于 VictoriaTraces ,用来换取特定场景的查询性能。
|
24
xingxing09 29 天前
其实真的想问下,大厂里流量大的业务,真的有在用 TraceID 这一套吗?日志存储费用每月花了多少 money
|
![]() |
25
RedisMasterNode OP @xingxing09 之前在富途呆过,基础框架统一得早,所以 trace 都有。
他们存了多少我不知道,但是更早之前在其他公司工作,流量更小,一个月也要存好几 pb 的数据,可以去阿里云查查这个磁盘用量+冗余空间一共值多少钱😆 |
26
flamingooo 29 天前
@RedisMasterNode 我知道的, ck 不是压缩性能最好的, 我只是看你上面回复其他人写入还没有专门优化, 我只是觉得在链路场景里, 写入性能的重要性应该要大于查询性能... 选择 ck 是刨除了一些架构上的考虑后, 因为压缩性能的考虑最终选择了 ck
|
![]() |
27
RedisMasterNode OP @flamingooo 其实我想表达的意思是即使在还没完全优化好的前提下写入性能比 CK 方案已经更好...
|
![]() |
28
zjiajun 29 天前
实际上,正常的链路数据毫无价值,如何在出问题时,定位到异常或者耗时长的链路才是关键。
我了解业务上,更多的是根据 entry 类型来查询链路列表,比如 web 容器的 path ,mvc 接口,dubbo 服务的 interface 等,这是基本的。对于核心业务场景链路,需要根据业务打标,分场景查看链路调用。 查询链路列表的 ck 表结构和根据 traceId 查询的表结构,order by 和索引是不一样的,没有两全其美的一张表,我们查询链路列表是单独的一张 ck 表 |
29
flamingooo 29 天前
@RedisMasterNode get 到了, 那麻烦当我没讲过吧
|