OpenTelemetry 链路追踪架构

表面上,你只是在代码中加了一个 @Traced 注解或一行 tracer.spanBuilder()。但在这背后,OTel 的链路追踪系统经历了一系列复杂的数据流转:从 SDK 的自动拦截,到 Context 的跨进程传播,再到 Collector 的聚合处理,最后到后端存储的可视化。

理解这套架构,能让你在遇到「为什么 Trace 断了」「为什么数据没上报」等问题时,快速定位根因。

整体数据流

一条 Trace 从产生到最终展示,经历以下阶段:

flowchart TB
    subgraph App["应用层"]
        Auto["自动埋点<br/>(Java Agent / SDK 自动拦截)"]
        Manual["手动埋点<br/>(业务代码)"]
    end

    subgraph SDK["OTel SDK"]
        BP["BatchSpanProcessor<br/>批量处理器"]
        Span["Span 数据"]
    end

    subgraph Collector["OTel Collector"]
        OTLPR["OTLP Receiver"]
        Proc["Processor<br/>采样/过滤/增强"]
        Exp["Exporter"]
    end

    subgraph Backend["可观测性后端"]
        Jaeger["Jaeger"]
        Tempo["Grafana Tempo"]
        Zipkin["Zipkin"]
    end

    subgraph Query["查询层"]
        UI["Trace UI<br/>瀑布图展示"]
    end

    App --> Auto & Manual
    Auto --> SDK
    Manual --> SDK
    SDK --> BP --> Collector
    Collector --> Exp --> Backend
    Backend --> UI

    style SDK fill:#4a90d9,color:#fff
    style Collector fill:#f5a623,color:#fff

埋点层:自动 vs 手动

自动埋点的工作原理

OTel Java Agent 通过 Java Instrumentation 机制,在类加载时拦截目标类的字节码,插入埋点逻辑。这个过程对业务代码完全透明。

以一个 Spring MVC 的 HTTP 请求为例,自动埋点的工作流程:

  1. Agent 拦截 DispatcherServlet.doDispatch() 方法
  2. 在方法入口创建新 Span,设置 HTTP 相关属性
  3. 在方法出口结束 Span,设置状态和耗时
  4. 通过 W3CTraceContextPropagator 将 TraceContext 注入 HTTP 响应 Header
sequenceDiagram
    participant Client as HTTP Client
    participant Servlet as DispatcherServlet
    participant Agent as OTel Java Agent
    participant Backend as 后端服务

    Client->>Servlet: GET /api/orders

    Note over Agent: 拦截点:方法入口
    Agent->>Agent: 提取 traceparent Header<br/>创建 Span

    Servlet->>Servlet: 业务逻辑<br/>调用下游服务

    Note over Agent: 拦截点:方法出口
    Agent->>Agent: 设置 Span 状态<br/>记录耗时

    Servlet-->>Client: 200 OK<br/>traceparent: updated

    Note over Agent: 自动注入更新的<br/>traceparent Header

自动埋点覆盖的常见场景:

组件拦截的方法/框架
HTTP 服务器Servlet API、Spring MVC、Spring WebFlux、Netty
HTTP 客户端OkHttp、Apache HttpClient、Feign
数据库JDBC、MyBatis、Hibernate、MongoDB Client
缓存Jedis、Lettuce、Redis Client
消息队列Kafka Client、RocketMQ、RabbitMQ Client

手动埋点的补充

自动埋点解决 80% 的场景,但以下情况必须手动埋点:

业务关键路径:自动埋点只知道「调用了哪个方法」,不知道「这个方法在做什么业务」。比如 orderService.placeOrder() 内部调用了支付服务,你需要给这个支付调用单独创建一个 Span,并带上 payment.amountpayment.method 等业务属性。

异步处理:线程池、定时任务、消息队列消费等场景,Context 不会自动传递,必须手动创建 Span 并正确设置 parent。

外部系统调用:非标准协议的调用(如自定义 TCP 协议、私有 RPC 框架),Agent 无法自动拦截。

SDK 层:SpanProcessor

Span 从业务代码产生后,不会立刻发送到后端,而是先经过 SpanProcessor 的处理。OTel 定义了两种标准处理方式:

SimpleSpanProcessor(立即发送)

每创建一个 Span 就立即发送到 Exporter。实现简单,但性能差——高 QPS 下会产生大量网络请求。不推荐生产环境使用。

BatchSpanProcessor(批量发送)

将多个 Span 积累到缓冲区,达到以下条件之一时批量发送:

条件说明推荐值
maxQueueSize缓冲区队列的最大容量2048
scheduledDelayMillis定时触发发送的间隔5000ms
exportTimeoutMillis单次批量发送的超时时间30000ms
maxExportBatchSize单次批量发送的最大 Span 数512
application.yml
otel:
  traces:
    exporter: otlp
  exporter:
    otlp:
      traces:
        endpoint: http://collector:4317
  span-processor:
    type: batch
    max-queue-size: 2048
    schedule-delay: 5000ms
    max-export-batch-size: 512

BatchSpanProcessor 是生产环境的必选配置。如果配置不当,可能导致 Span 数据丢失(队列满了会丢弃)或发送延迟(定时太长发得慢)。

Collector 层:数据处理中枢

为什么需要 Collector

在简单的测试环境中,应用可以直接向 Jaeger/Tempo 等后端发送数据。但生产环境有以下需求:

多后端复用。同一个应用的数据可能需要同时发送到多个后端(Jaeger + Tempo + 商业平台),Collector 可以配置多个 Exporter,实现一次采集、多渠道导出。

数据预处理。在数据发送到存储前做过滤、采样、属性增强。比如为所有 Span 统一添加 deployment.environment=production 标签。

协议转换。将非 OTLP 格式的数据(Jaeger Thrift、Zipkin JSON)转换为 OTLP 格式,统一处理流程。

负载缓解。Collector 作为独立网关,可以做流量控制、限流、缓冲,保护后端存储不被冲击。

Collector 部署架构

flowchart TB
    subgraph ClusterA["集群 A (生产环境)"]
        AgentA1["Agent 1<br/>K8s DaemonSet"]
        AgentA2["Agent 2<br/>K8s DaemonSet"]
        AgentA3["Agent 3<br/>K8s DaemonSet"]
    end

    subgraph ClusterB["集群 B (测试环境)"]
        AgentB1["Agent B1"]
        AgentB2["Agent B2"]
    end

    subgraph Gateway["Gateway Collector"]
        GW["OTel Collector<br/>Gateway 模式"]
    end

    subgraph Backend["后端存储"]
        J["Jaeger"]
        T["Tempo"]
        P["Prometheus"]
    end

    AgentA1 & AgentA2 & AgentA3 --> GW
    AgentB1 & AgentB2 --> GW
    GW --> J & T & P

    style GW fill:#f5a623,color:#fff
    style ClusterA fill:#4a90d9,color:#fff

Agent 模式:Collector 以 DaemonSet 部署在每个 K8s 节点上,负责收集该节点上所有 Pod 的数据。本地处理(过滤、增强),然后通过 gRPC 转发到 Gateway Collector。

Gateway 模式:独立部署的 Collector,负责接收来自多个 Agent 的数据,做全局处理后转发到存储后端。可以部署多个实例做水平扩展。

Collector 配置文件

otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

  jaeger:
    protocols:
      thrift_http:
        endpoint: 0.0.0.0:14278

  zipkin:
    endpoint: 0.0.0.0:9411

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024

  memory_limiter:
    check_interval: 1s
    limit_mib: 1000
    spike_limit_mib: 200

  # 过滤掉健康检查的请求
  filter:
    traces:
      exclude:
        match_type: strict
        services:
          - kubernetes probe

  # 统一添加环境标签
  resource:
    attributes:
      - key: deployment.environment
        value: production
        action: upsert
      - key: cloud.region
        value: cn-hangzhou
        action: upsert

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true

  otlp/tempo:
    endpoint: tempo:4317

  prometheus:
    endpoint: 0.0.0.0:8889

service:
  pipelines:
    traces:
      receivers: [otlp, jaeger, zipkin]
      processors: [memory_limiter, filter, resource, batch]
      exporters: [otlp/jaeger, otlp/tempo]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [prometheus]

后端存储:Jaeger vs Tempo

Jaeger 架构

Jaeger 是 CNCF 毕业项目,提供完整的链路追踪后端能力:

flowchart TB
    subgraph Ingest["摄入层"]
        Agent["Jaeger Agent<br/>(与 Collector 协议兼容)"]
    end

    subgraph Storage["存储层"]
        Cassandra["Cassandra"]
        Elasticsearch["Elasticsearch"]
    end

    subgraph Query["查询层"]
        Query["Jaeger Query"]
        UI["Jaeger UI"]
    end

    Agent --> Collector["Jaeger Collector"]
    Collector --> Cassandra
    Collector --> Elasticsearch
    Query --> Cassandra
    Query --> Elasticsearch
    Query --> UI

Jaeger 支持两种存储后端:Cassandra 和 Elasticsearch。Cassandra 适合超大规模场景(单集群百亿级 Span),Elasticsearch 对中小规模更友好,查询性能更好。

Grafana Tempo 架构

Tempo 是 Grafana 生态中的追踪存储,以「极低存储成本」为核心卖点。Tempo 本身不存储完整的 Trace 数据,而是存储 Trace 的索引信息(TraceID → 对象存储路径),实际数据存在对象存储(S3/GCS/Azure Blob)中。

flowchart TB
    subgraph Ingest["数据摄入"]
        OTel["OTel Collector"]
        Zipkin["Zipkin"]
        Jaeger["Jaeger"]
    end

    subgraph Index["索引层"]
        Tempo["Grafana Tempo"]
    end

    subgraph Storage["对象存储"]
        S3["S3 / GCS / Azure Blob"]
    end

    subgraph Query["查询层"]
        Grafana["Grafana"]
    end

    OTel --> Tempo
    Zipkin --> Tempo
    Jaeger --> Tempo
    Tempo --> S3
    Grafana --> Tempo

Tempo 的优势是存储成本极低——因为 Span 数据是压缩后存对象存储的。以 100 万 Span/天的规模为例,Jaeger + Elasticsearch 每月存储成本约 500-1000 美元,Tempo + S3 约 50-100 美元。

Tempo 的局限是查询能力有限——Trace 数据需要拉回 Tempo 后再做聚合分析,不适合超大规模 Trace 数据的即席查询。

选型建议

场景推荐
Grafana 生态用户,存储成本敏感Grafana Tempo + 对象存储
独立部署,需要丰富的查询能力Jaeger + Elasticsearch
需要同时支持链路追踪和 APM 高级分析SkyWalking / Datadog
AWS 托管环境AWS X-Ray

查询与可视化

Trace 数据的最终价值体现在查询和可视化上。一个好的 Trace UI 应该提供:

瀑布图(Waterfall View):以时间为横轴,展示每个 Span 的开始时间、持续时间、父子关系。这是定位延迟瓶颈的核心视图。

Span 明细:点击任意 Span,显示其所有属性(Attributes)、事件(Events)、错误信息。

Trace 统计:Top N 慢请求、错误率分布、Span 数量分布等聚合视图。

日志关联:点击 Span 或 TraceID,直接跳转到该请求的所有日志。

Grafana
# 通过 TraceID 直接查询
{__name__="traces"} |= "abc123def456"

# 查询慢 Trace(超过 2 秒)
{__name__="traces"} | json | duration > 2000

# 查询包含错误的所有 Trace
{__name__="traces"} | json | status_code = "error"

质量判断标准

读完本节后,你应该能够回答:

  1. OTel 链路追踪的数据流从业务代码到最终展示,经历哪几个关键阶段?
  2. 为什么生产环境必须使用 BatchSpanProcessor 而不是 SimpleSpanProcessor?
  3. OTel Collector 在链路追踪架构中承担哪些核心职责?Agent 模式和 Gateway 模式分别适合什么场景?
  4. Grafana Tempo 的低存储成本是如何实现的?它的核心局限是什么?
  5. 如果 Trace 出现「断裂」问题(链路不连续),应该在哪些层面排查?