V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
RedisMasterNode
V2EX  ›  数据库

VictoriaMetrics 开发者笔记: Traces 的查询性能与优化倾向

  •  2
     
  •   RedisMasterNode · 30 天前 · 2280 次点击

    数月之前我们公布了关于用 VictoriaLogs 充当 Traces 数据的存储的调研,从 PoC 的角度看,这是一次很不错的尝试,但是我们意识到,这离真正可用的产品形态还差很远。

    因此在这段时间内,这个 PoC 项目发生了如下的变化:

    1. VictoriaLogs 从 VictoriaMetrics 项目分离。而我们的 Traces 解决方案,作为 VictoriaLogs 的一个下游分支,也拥有了属于它的新名字和仓库:VictoriaTraces
    2. 完善查询场景的性能测试和优化,发布了第一个版本 v0.1.0

    同时,我们还收到了很多用户关于查询性能的疑问,因为在上一篇博客中,查询性能只被简单地提及过 —— 是的,那是一轮不够严谨的测试。我们通过简单观察不同 Traces 后端的响应速度,得出 VictoriaLogs 的查询性能不逊色于竞争对手,这当然没有说服力

    所以,这篇博客中,让我们一起来探索一下 Traces 在不同的后端中是如何查询的。

    Traces 的典型查询场景

    回想一下,开发者们是如何使用 Traces 的:

    1. 有人向你报告了一个 Bug 以及对应的 TraceID ,然后你打开 Traces 平台,通过 TraceID 查询 Trace
    2. 有人向你报告系统变慢了,但不知道原因。你打开 Traces 平台,搜索一段时间内所有耗时超过 3000ms ,或者包含错误的 Traces

    这是 Traces 中两个最常见的查询场景,那么数据 Schema 的设计就应该围绕着它们进行。

    想要加速 TraceID 的查询,那数据就应该按照 TraceID 进行排列,而其余数据可以分多列存储,也可以编码成 Binary 或者 JSON 存入一列。另一方面,想要在时间范围内按照 Span 的 Attributes (如耗时、状态)进行查询,那这些数据应该按时间排序,并且相关 Attributes 就应该作为单独的列,提供检索的能力。

    不同 Traces 后端的优化倾向

    如果我们观察 VictoriaTraces 和其他 Traces 后端的 Schema ,可以看出来它们的优化倾向各不相同。

    Jaeger

    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 中取出完整数据。

    ClickStack ( ClickHouse )

    作为 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 表的查询速度。

    <details>
    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

    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 查询的概念验证,它有很多显而易见的缺点:

    1. Index Stream 数据按照时间排序,因此按 TraceID 的查询是遍历而二分查找,效率不高。
    2. TraceID 的起始结束时间是不确定的,查询 90 秒的数据既可能浪费(如 Trace 耗时仅为 1 秒),也可能不足(如 Trace 跨越数分钟)。

    不过,聪明的读者一定也知道所有的设计都有其长处和短板,问题在于是否值得:

    • 用更多的空间换更快的时间。
    • 用更昂贵的写入换更快的查询。

    我们一定会在后续版本保持探索,调整这些设计,将它变得更加适合不同的使用场景。不过在那之前,不如先回到今天的主题——它们到底查询性能如何?

    查询性能对比

    改良 Traces 数据生成

    为了生成大量更贴合生产环境的测试数据,我们一开始打算部署多个 OpenTelemetry Demo。该 Demo 是基于 14 个微服务的分布式系统,覆盖了不同的编程语言、不同的插桩方式,产生的数据比过往使用的 Jaeger tracegen 更具有代表性。

    但是在运行一段时间后,我们发现 OpenTelemetry Demo 需要消耗较多的资源,并且产生的压力有限。因此,我们又基于流量录制回放的思路编写了 vtgen,它可以:

    1. 反复发送预先录制好的 OpenTelemetry Demo 的 Trace 请求到多个 OTLP HTTP Endpoints ,这些 Trace 请求中 TraceID 和部分字段会被按需修改。
    2. 记录 HTTP 请求耗时指标。

    vtgen 既可以用于 Traces 后端的写入性能 Benchmark ,也可以为不同 Traces 后端写入完全一致的数据,并随机记录一定量的 TraceID ,用于查询性能 Benchmark 。

    Benchmark 设计

    我们在对 VictoriaTraces 的 Benchmark 中仍然选用了 Grafana Tempo 及 Jaeger & ClickHouse 作为对比,写入相同的的数据,其中数据写入过程的监控监控记录如下图,读者也可以在 Grafana Dashboard 快照中查阅。

    我们在第一节中介绍过,最常见的 Traces 查询场景包括:

    1. 根据 TraceID 查询。
    2. 根据属性在特定时间段搜索出多个 Traces 。

    因此对应地:

    1. 通过 vtgen 在 Ingestion 过程中记录下 63000 个 TraceID ,逐一向 3 个 Traces 后端进行请求。
    2. 手动构造 15 组 Traces 属性查询的参数模板,以及 50 个时长为 10-60 分钟的时间段,逐一向 VictoriaTraces 和 Jaeger 进行共计 750 次请求。

    {{<admonition type=note title="为什么属性搜索对比中没有 Tempo ?">}}

    VictoriaTraces 、ClickHouse 均可以支持 Jaeger 的 Search API ,而 Tempo 同样提供 Search API ,但是两个 Search APIs 的返回数据格式并不一致:

    • Jaeger 的 Search API 需要提供完整的 Traces 数据,包含所有 Spans 。换句话说,Jaeger 的 Search API 就是 List 版本的 Trace API 。
    • Tempo 的 Search API 只需返回 Traces 的部分数据,因而无需额外查找每个 Trace 的其余 Spans 。

    因此,它们无法直接对比查询性能。

    不过,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 ,希望它能够更早抵达稳定版本。

    所以,在性能上:

    • VictoriaTraces 的底层数据结构将与 VictoriaLogs 进一步分叉,让 Traces 场景得到更多的关爱。引入更合适的 IndexDB 和 Cache 来优化数据写入和查询。
    • Profiling 结果显示,VictoriaTraces 还有很多糟糕的代码,它还有很大的进步空间。

    同时,在功能上,我们希望:

    • 提供 Tempo HTTP APIs ,这允许用户更灵活地查询 Traces 数据。
    • 完善 Kubernetes 支持,提供 Operator 和 Helm Chart 。
    • 完善 Cluster 版本的设计。

    期待能在博客评论区或 VictoriaTraces 的 Issues 中收到你的反馈,也期待与你在下一期开发者笔记中再会!

    29 条回复    2025-08-05 11:41:39 +08:00
    DLOG
        1
    DLOG  
       30 天前
    牛!
    encro
        2
    encro  
       30 天前   ❤️ 1
    比 signoz 与 greptimedb 呢
    RedisMasterNode
        3
    RedisMasterNode  
    OP
       30 天前
    @encro 希望未来有时间测试,但是现在开发时间不够用,没有多余的时间对比更多的产品
    tuimaochang
        4
    tuimaochang  
       30 天前
    牛逼
    qW7bo2FbzbC0
        5
    qW7bo2FbzbC0  
       30 天前
    这个可以查询展示关系图吗
    Nanosk
        6
    Nanosk  
       30 天前   ❤️ 1
    @encro signoz 不也是基于 Clickhouse 吗
    RedisMasterNode
        7
    RedisMasterNode  
    OP
       30 天前
    > 这个可以查询展示关系图吗
    @qW7bo2FbzbC0 如果说的是 Trace 查询展示 -> 现在有提供 Jaeger 的接口,可以在 Grafana 展示,或者代替 Jaeger 后端,在 Jaeger UI 展示。

    如果说的是所有服务之间的调用关系总览、实时流量 -> 现在还不行,VictoriaTraces 目前只是个非常简单的 Traces Storage 。我们讨论过不同的 Service Map/Dependency Graph 的实现方案,但是还没有定论,具体或许看看哪种方案更高效才能继续推进。
    RedisMasterNode
        8
    RedisMasterNode  
    OP
       30 天前   ❤️ 1
    @Nanosk 是滴,不过如文中所说,不同产品设计的 ClickHouse 的 schema 不同,所以在不同场景里的查询性能也不同,取决于产品希望往什么方向优化。

    Signoz 的博客介绍的 schema 是: https://signoz.io/docs/userguide/writing-clickhouse-traces-query/
    这也是配合 Signoz 里所需要的图表来设计的,具体性能相互比较一下也无妨 :) 如果后面有时间的话
    NikaidoIsAGod
        9
    NikaidoIsAGod  
       30 天前
    想请问下:
    1. trace 插入的吞吐量如何。clickhouse 在大量数据插入时会发生 too many part 的问题,但是如果使用 async insert 的话会导致一定的数据延迟,影响吞吐量,是否有关于 batch insert 相关的 benchmark?
    2. 对于物化表的支持如何?
    RedisMasterNode
        10
    RedisMasterNode  
    OP
       30 天前
    @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 里还没有进行过优化,还有很大的提升空间。
    RedisMasterNode
        11
    RedisMasterNode  
    OP
       30 天前
    @NikaidoIsAGod 在第一个链接中,ClickHouse 收到的请求是由 Jaeger 分了 Batch 的,而 VictoriaTraces 的请求是直接来自于 Client 的并发请求。
    NikaidoIsAGod
        12
    NikaidoIsAGod  
       30 天前
    @RedisMasterNode opentelmetry collector 有个 servicegraph connector ,将 trace span 转换成 servicemap 的 edge metric 这个思路其实还不错
    RedisMasterNode
        13
    RedisMasterNode  
    OP
       30 天前
    @NikaidoIsAGod 实时的分析会很耗费资源,因为它需要 buffer 一个 trace 的所有 span 一段时间,并且也不确定这个 trace 是否已经完结。servicegraphconnector 如果需要承接数千万的 span 肯定会很困难。不过不管哪种方案,用 connector 分析,还是用持久化的数据异步分析,都各有优劣吧没有说优先用哪种。
    takanashisakura
        14
    takanashisakura  
       30 天前   ❤️ 1
    牛逼,dalao 居然在站内。先前给自己 nas 做监控就是用的 VictoriaMetrics+grafana
    NikaidoIsAGod
        15
    NikaidoIsAGod  
       30 天前   ❤️ 1
    @RedisMasterNode 确实,但是对于服务地图的场景来说其实并不用 buffer 所有的 trace span ,只需要 buffer “一对 span“ 即可。即 server span - client span 。并且在 buffer 的时候 drop 掉不需要的 attribute ,所需要的内存就会小很多了。亲测在 20w span qps 的压力下。servicegraph 也就吃个大概 10g 不到的内存。当然这个也和业务有关
    RedisMasterNode
        16
    RedisMasterNode  
    OP
       30 天前
    @NikaidoIsAGod 或许 service map 和 tail sampling 可以实现在未来的 vtagent 里面,看能不能比 otel collector 更小巧高效。不过短期内团队还是优先关注读写性能,毕竟项目还新,一下子做不好太多事情
    NikaidoIsAGod
        17
    NikaidoIsAGod  
       30 天前
    @RedisMasterNode 加个 v 吗,有些问题想请教下
    RedisMasterNode
        18
    RedisMasterNode  
    OP
       30 天前
    RedisMasterNode
        19
    RedisMasterNode  
    OP
       30 天前
    NikaidoIsAGod
        20
    NikaidoIsAGod  
       30 天前
    flamingooo
        21
    flamingooo  
       30 天前
    但实际上我选择 ck 主要考虑, 是为了保证 '查询性能可用的前提', 得到尽可能多的写入性能跟压缩能力, 对于 log 跟 trace 其实相比 metric 我会更偏向这里.(
    RedisMasterNode
        22
    RedisMasterNode  
    OP
       30 天前
    @flamingooo 上一篇博客已经介绍过 Ingestion 的性能对比,这篇博客是续集,关注查询性能。
    不太明白您的意思,因为 ClickHouse 的写入不是更好的那个。
    RedisMasterNode
        23
    RedisMasterNode  
    OP
       30 天前
    @flamingooo BTW 如果选择的是 Jaeger 的 ClickHouse schema ,它们的磁盘用量几乎是 2x 之于 VictoriaTraces ,用来换取特定场景的查询性能。
    xingxing09
        24
    xingxing09  
       29 天前
    其实真的想问下,大厂里流量大的业务,真的有在用 TraceID 这一套吗?日志存储费用每月花了多少 money
    RedisMasterNode
        25
    RedisMasterNode  
    OP
       29 天前
    @xingxing09 之前在富途呆过,基础框架统一得早,所以 trace 都有。
    他们存了多少我不知道,但是更早之前在其他公司工作,流量更小,一个月也要存好几 pb 的数据,可以去阿里云查查这个磁盘用量+冗余空间一共值多少钱😆
    flamingooo
        26
    flamingooo  
       29 天前
    @RedisMasterNode 我知道的, ck 不是压缩性能最好的, 我只是看你上面回复其他人写入还没有专门优化, 我只是觉得在链路场景里, 写入性能的重要性应该要大于查询性能... 选择 ck 是刨除了一些架构上的考虑后, 因为压缩性能的考虑最终选择了 ck
    RedisMasterNode
        27
    RedisMasterNode  
    OP
       29 天前
    @flamingooo 其实我想表达的意思是即使在还没完全优化好的前提下写入性能比 CK 方案已经更好...
    zjiajun
        28
    zjiajun  
       29 天前
    实际上,正常的链路数据毫无价值,如何在出问题时,定位到异常或者耗时长的链路才是关键。
    我了解业务上,更多的是根据 entry 类型来查询链路列表,比如 web 容器的 path ,mvc 接口,dubbo 服务的 interface 等,这是基本的。对于核心业务场景链路,需要根据业务打标,分场景查看链路调用。
    查询链路列表的 ck 表结构和根据 traceId 查询的表结构,order by 和索引是不一样的,没有两全其美的一张表,我们查询链路列表是单独的一张 ck 表
    flamingooo
        29
    flamingooo  
       29 天前
    @RedisMasterNode get 到了, 那麻烦当我没讲过吧
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4363 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 10:08 · PVG 18:08 · LAX 03:08 · JFK 06:08
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.