分布式链路追踪实战:OpenTelemetry、Jaeger 与 W3C Trace Context

从 Java Agent 埋点到 OTel Collector 聚合:OpenTelemetry API/SDK、Jaeger/Tempo 存储后端、W3C Trace Context 传播规范与采样策略最佳实践。

在微服务架构中,单个用户请求可能跨越数十个服务实例,调用链路的复杂性使得问题定位变得异常困难。一次 API 超时,究竟是底层数据库慢查询、Redis 网络抖动,还是下游第三方服务异常?没有链路追踪,排查这类问题无异于大海捞针。本文从核心理论出发,深入讲解 OpenTelemetry 的完整技术栈、Jaeger 与 Grafana Tempo 的部署实践、W3C Trace Context 的传播规范,以及生产环境中的采样策略与最佳实践。


1. 链路追踪核心概念:Trace、Span 与 Baggage

1.1 Trace:一次完整的分布式调用链

Trace(链路) 是端到端请求的完整视图,由唯一标识 TraceId 贯穿始终。当用户在电商 App 点击"下单"按钮时,从前端网关到订单服务、库存服务、支付服务、通知服务的整段调用,共同组成一条 Trace。

一条 Trace 的生命周期涵盖:请求进入系统、各服务间 RPC 调用、异步消息发送、数据库访问、缓存操作以及最终响应返回。Trace 的核心价值在于将原本散落在各服务日志中的孤立事件串联成有机整体。

1.2 Span:链路中的最小工作单位

Span(跨度) 是 Trace 的基本组成单元,代表一个命名且计时的操作。每个 Span 包含以下关键字段:

字段说明示例值
spanIdSpan 唯一标识,16字节十六进制a1b2c3d4e5f6a7b8
parentSpanId父 Span 标识,用于构建父子关系9f8e7d6c5b4a3920
name操作名称POST /api/orders
kindSpan 类型(Server/Client/Producer/Consumer/Internal)SERVER
startTime / endTime起止时间戳Unix 纳秒时间
attributes键值对形式的元数据db.system=mysql
events带时间戳的日志事件异常堆栈、调试信息
statusSpan 状态(Unset/Ok/Error)ERROR

Span 之间存在两种基本关系:Parent-Child(父子)Follows-From(跟随)。同步 RPC 调用通常形成父子关系;异步消息消费则常用 Follows-From,表示因果关联但无执行等待关系。

1.3 Baggage:跨进程透传的上下文数据

Baggage(行李) 是一种与 Trace 绑定的键值对集合,会随着 Trace Context 自动传播到所有下游服务。与 Span Attributes 不同,Baggage 的生命周期贯穿整条链路,适合传递租户 ID、用户会话信息、A/B 实验标记等需要在全局上下文中访问的数据。

需要注意的是,Baggage 存在性能与隐私风险:人多的链路中,Baggage 数据会被反复序列化和反序列化,应避免存储敏感信息和大体积数据。

1.4 Java 代码:手动创建 Span

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;

public class ManualSpanDemo {
    // 获取 Tracer, instrumentationScopeName 标识埋点来源
    private static final Tracer tracer = OpenTelemetry.noop().getTracer("order-service");

    public Order createOrder(CreateOrderRequest request) {
        // 创建根 Span,代表整个下单操作
        Span parentSpan = tracer.spanBuilder("OrderService.createOrder")
                .setSpanKind(SpanKind.SERVER)
                .setAttribute("order.user_id", request.getUserId())
                .setAttribute("order.source", request.getSource())
                .startSpan();

        // 必须将 Span 放入当前上下文,后续创建的 Span 才能自动成为子 Span
        try (Scope scope = parentSpan.makeCurrent()) {
            // 模拟校验库存
            validateInventory(request);

            // 模拟扣减库存(创建子 Span)
            deductInventory(request);

            // 模拟保存订单
            Order order = saveOrder(request);
            parentSpan.setAttribute("order.id", order.getId());
            return order;
        } catch (Exception e) {
            // 标记 Span 为错误状态并记录异常详情
            parentSpan.setStatus(StatusCode.ERROR, e.getMessage());
            parentSpan.recordException(e);
            throw e;
        } finally {
            // 必须结束 Span,否则数据不会上报
            parentSpan.end();
        }
    }

    private void deductInventory(CreateOrderRequest request) {
        // 由于 parentSpan 已经 makeCurrent(),这里自动成为子 Span
        Span childSpan = tracer.spanBuilder("InventoryClient.deduct")
                .setSpanKind(SpanKind.CLIENT)
                .setAttribute("inventory.sku_id", request.getSkuId())
                .setAttribute("inventory.quantity", request.getQuantity())
                .startSpan();

        try (Scope scope = childSpan.makeCurrent()) {
            // 调用库存服务 RPC
            inventoryClient.deduct(request.getSkuId(), request.getQuantity());
        } catch (Exception e) {
            childSpan.setStatus(StatusCode.ERROR);
            childSpan.recordException(e);
            throw e;
        } finally {
            childSpan.end();
        }
    }
}

上述代码展示了最核心的手动埋点模式:通过 spanBuilder 创建 Span,使用 makeCurrent() 将当前 Span 注入上下文,try-with-resources 确保上下文正确关闭,以及 finally 块确保 Span 一定被结束。


2. OpenTelemetry 架构全景

2.1 三大支柱的统一标准

OpenTelemetry(简称 OTel)是 CNCF 孵化项目,源于 OpenTracing 与 OpenCensus 的合并,目标是提供 Vendor-Neutral(厂商无关)的统一可观测性标准。它覆盖三大信号:

  • Traces(链路):请求在分布式系统中的完整路径
  • Metrics(指标):可聚合的数值度量,如延迟分位值、QPS、错误率
  • Logs(日志):结构化的事件记录

2.2 OTel 分层架构

OpenTelemetry 的架构自上而下可分为五个层次:

API 层(Application Programming Interface)
应用代码直接依赖的接口层,定义了 Tracer、Meter、Logger 等创建方法。API 层设计为零依赖,不引入性能开销。即使未绑定 SDK,调用 API 也会返回 Noop 实现,不会抛出异常。

SDK 层(Software Development Kit)
API 的具体实现层,负责 Span 的创建、批量导出、采样决策和资源属性附加。SDK 层包含可插拔的 Exporter 接口,支持将数据发送到不同后端。

Collector(收集器)
可选但强烈推荐的独立代理进程,接收 OTLP 数据,经过处理(批处理、过滤、富化、格式转换)后转发到一个或多个后端存储。Collector 分为 Agent(本机部署)和 Gateway(集群级汇聚)两种模式。

Exporter(导出器)
负责将数据从 SDK 或 Collector 发送到存储后端的插件。常见 Exporter 包括 OTLP、gRPC、HTTP/JSON、Jaeger Thrift、Zipkin、Prometheus Remote Write 等。

Backend(存储后端)
最终持久化并查询可观测性数据的服务,如 Jaeger、Tempo、Zipkin、Prometheus、Loki、Elasticsearch 等。

2.3 Java SDK 初始化配置

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.semconv.ResourceAttributes;

public class OpenTelemetryInit {

    public static OpenTelemetry initOpenTelemetry() {
        // 定义服务资源属性,标识当前实例
        Resource resource = Resource.getDefault()
                .merge(Resource.builder()
                        .put(ResourceAttributes.SERVICE_NAME, "order-service")
                        .put(ResourceAttributes.SERVICE_VERSION, "1.2.0")
                        .put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, "production")
                        .put(ResourceAttributes.HOST_NAME, System.getenv("HOSTNAME"))
                        .build());

        // 创建 OTLP gRPC Exporter,指向 Collector
        OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
                .setEndpoint("http://otel-collector.monitoring.svc.cluster.local:4317")
                .setTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                .build();

        // 配置 BatchSpanProcessor:批量导出以提升性能
        BatchSpanProcessor batchSpanProcessor = BatchSpanProcessor.builder(spanExporter)
                .setMaxQueueSize(2048)          // 待导出 Span 队列最大长度
                .setMaxExportBatchSize(512)     // 单次批量导出的 Span 数量
                .setScheduleDelay(5000)         // 导出调度间隔,单位毫秒
                .setExporterTimeout(30000)      // 单次导出超时
                .build();

        // 构建 TracerProvider
        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
                .addSpanProcessor(batchSpanProcessor)
                .setResource(resource)
                .build();

        // 构建并注册全局 OpenTelemetry 实例
        OpenTelemetrySdk openTelemetry = OpenTelemetrySdk.builder()
                .setTracerProvider(tracerProvider)
                .build();

        // 注册 JVM 关闭钩子,优雅关闭并刷新缓冲区
        Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::close));

        return openTelemetry;
    }
}

此初始化代码展示了生产级 SDK 配置的要点:资源属性附加用于多租户识别和环境区分,BatchSpanProcessor 平衡实时性与导出吞吐量,addShutdownHook 确保进程退出时缓存数据被 flush,避免 Span 丢失。


3. 手动埋点:精细控制每一层

3.1 数据库操作埋点

数据库查询通常是延迟的主要贡献者,对其进行细粒度埋点能快速定位慢查询。

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import io.opentelemetry.semconv.SemanticAttributes;

public class InstrumentedOrderRepository {
    private final Tracer tracer;
    private final DataSource dataSource;

    public Order selectById(Long orderId) {
        Span span = tracer.spanBuilder("OrderRepository.selectById")
                .setSpanKind(SpanKind.CLIENT)
                .setAttribute(SemanticAttributes.DB_SYSTEM, "mysql")
                .setAttribute(SemanticAttributes.DB_NAME, "trade_db")
                .setAttribute(SemanticAttributes.DB_OPERATION, "SELECT")
                .setAttribute(SemanticAttributes.DB_SQL_TABLE, "t_order")
                .setAttribute(SemanticAttributes.DB_STATEMENT,
                        "SELECT * FROM t_order WHERE id = ?")
                .startSpan();

        try (Scope scope = span.makeCurrent();
             Connection conn = dataSource.getConnection();
             PreparedStatement ps = conn.prepareStatement(
                     "SELECT * FROM t_order WHERE id = ?")) {

            ps.setLong(1, orderId);
            long startTime = System.currentTimeMillis();

            try (ResultSet rs = ps.executeQuery()) {
                long duration = System.currentTimeMillis() - startTime;
                // 记录实际执行耗时,便于发现慢查询
                span.setAttribute("db.execution_time_ms", duration);

                if (rs.next()) {
                    return mapToOrder(rs);
                }
                return null;
            }
        } catch (SQLException e) {
            span.setStatus(StatusCode.ERROR);
            span.recordException(e);
            // 记录 SQL 错误码,辅助问题分类
            span.setAttribute("db.error_code", e.getErrorCode());
            throw new RuntimeException(e);
        } finally {
            span.end();
        }
    }
}

3.2 HTTP Client 调用埋点

对外部 HTTP 服务的调用埋点应遵循 Semantic Conventions,统一属性键名。

public class InstrumentedHttpClient {
    private final Tracer tracer;
    private final HttpClient httpClient;

    public HttpResponse<String> callExternalApi(String url, String body) {
        Span span = tracer.spanBuilder("HTTP POST")
                .setSpanKind(SpanKind.CLIENT)
                .setAttribute(SemanticAttributes.HTTP_REQUEST_METHOD, "POST")
                .setAttribute(SemanticAttributes.URL_FULL, url)
                .setAttribute(SemanticAttributes.HTTP_REQUEST_BODY_SIZE, body.getBytes().length)
                .startSpan();

        try (Scope scope = span.makeCurrent()) {
            HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .header("Content-Type", "application/json")
                    .timeout(Duration.ofSeconds(10));

            // 将当前 Trace Context 注入到 HTTP Header 中,供下游服务继续追踪
            // W3C Trace Context 格式的传播由 instrumentation 自动完成
            io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator.getInstance()
                    .inject(io.opentelemetry.context.Context.current(), requestBuilder,
                            (carrier, key, value) -> carrier.header(key, value));

            HttpRequest request = requestBuilder
                    .POST(HttpRequest.BodyPublishers.ofString(body))
                    .build();

            HttpResponse<String> response = httpClient.send(
                    request, HttpResponse.BodyHandlers.ofString());

            span.setAttribute(SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, response.statusCode());
            span.setAttribute(SemanticAttributes.HTTP_RESPONSE_BODY_SIZE,
                    response.body().getBytes().length);

            if (response.statusCode() >= 400) {
                span.setStatus(StatusCode.ERROR, "HTTP " + response.statusCode());
            }
            return response;
        } catch (Exception e) {
            span.setStatus(StatusCode.ERROR);
            span.recordException(e);
            throw new RuntimeException(e);
        } finally {
            span.end();
        }
    }
}

3.3 异步任务埋点

在 CompletableFuture 或 reactive 编程模型中,上下文可能跨越线程边界,需要显式传递。

import io.opentelemetry.context.Context;

public class AsyncTracingDemo {
    private final Tracer tracer;

    public CompletableFuture<Order> createOrderAsync(CreateOrderRequest request) {
        Span span = tracer.spanBuilder("OrderService.createOrderAsync")
                .setSpanKind(SpanKind.SERVER)
                .startSpan();

        // 捕获当前上下文
        Context parentContext = Context.current().with(span);

        return validateAsync(request)
                // 使用 wrappedExecutor 确保上下文跨线程传播
                .thenComposeAsync(valid -> processPaymentAsync(request, parentContext),
                        io.opentelemetry.context.Context.taskWrapping(Executors.newFixedThreadPool(4)))
                .thenApplyAsync(paymentResult -> {
                    // 在新线程中恢复上下文,继续创建子 Span
                    try (Scope scope = parentContext.makeCurrent()) {
                        Span childSpan = tracer.spanBuilder("OrderService.saveOrder")
                                .setParent(parentContext)
                                .startSpan();
                        try {
                            return saveOrder(request, paymentResult);
                        } finally {
                            childSpan.end();
                        }
                    }
                })
                .whenComplete((result, ex) -> {
                    // 无论成功失败,根 Span 都必须结束
                    if (ex != null) {
                        span.setStatus(StatusCode.ERROR);
                        span.recordException(ex);
                    }
                    span.end();
                });
    }

    private CompletableFuture<PaymentResult> processPaymentAsync(
            CreateOrderRequest request, Context context) {
        try (Scope scope = context.makeCurrent()) {
            Span span = tracer.spanBuilder("PaymentService.charge")
                    .setSpanKind(SpanKind.CLIENT)
                    .setAttribute("payment.amount", request.getAmount())
                    .startSpan();
            return paymentClient.chargeAsync(request)
                    .whenComplete((res, ex) -> {
                        if (ex != null) {
                            span.setStatus(StatusCode.ERROR);
                            span.recordException(ex);
                        }
                        span.end();
                    });
        }
    }
}

异步场景的核心在于 Context 的捕获与恢复。Context.taskWrapping() 可以将线程池包装为上下文感知的执行器,而手动 makeCurrent() 则适用于更细粒度的控制。


4. 自动埋点:零侵入的全景观测

4.1 Java Agent 原理

OpenTelemetry Java Agent 基于 Java Instrumentation API,通过 -javaagent 参数在类加载时修改字节码,自动为常见框架生成 Span:

  • Web 框架:Spring Web MVC、Spring WebFlux、JAX-RS、Servlet
  • HTTP 客户端:Apache HttpClient、OkHttp、JDK HttpClient、gRPC
  • 数据库:JDBC、R2DBC、Redis(Lettuce/Jedis)、MongoDB
  • 消息队列:Kafka、RabbitMQ、RocketMQ、Pulsar
  • RPC 框架:gRPC、Dubbo、Apache Thrift

Agent 将 Exporter 配置、采样率、服务名等参数外部化,应用代码无需任何改动即可接入链路追踪。

4.2 启动参数配置

# OpenTelemetry Java Agent 启动脚本示例
java \
  -javaagent:/opt/otel/opentelemetry-javaagent.jar \
  -Dotel.service.name=order-service \
  -Dotel.resource.attributes=deployment.environment=production,service.version=2.1.0 \
  -Dotel.traces.exporter=otlp \
  -Dotel.exporter.otlp.endpoint=http://otel-collector.monitoring.svc.cluster.local:4317 \
  -Dotel.exporter.otlp.protocol=grpc \
  -Dotel.metrics.exporter=prometheus \
  -Dotel.logs.exporter=otlp \
  -Dotel.instrumentation.common.default.enabled=true \
  -Dotel.instrumentation.spring-webmvc.enabled=true \
  -Dotel.instrumentation.jdbc.enabled=true \
  -Dotel.instrumentation.kafka.enabled=true \
  -Dotel.instrumentation.redis.enabled=true \
  -Dotel.bsp.schedule.delay=5000 \
  -Dotel.bsp.max.queue.size=2048 \
  -Dotel.bsp.max.export.batch.size=512 \
  -jar /app/order-service.jar

4.3 Spring Boot 自动配置

如果不想使用 Java Agent,也可以通过 Spring Boot Starter 以依赖方式接入。

// build.gradle 依赖配置
dependencies {
    implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:1.40.0'
    implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.40.0'
    implementation 'io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.40.0'
}
# application.yml 配置
otel:
  service:
    name: order-service
  resource:
    attributes:
      deployment.environment: production
      service.version: 2.1.0
  exporter:
    otlp:
      endpoint: http://otel-collector.monitoring.svc.cluster.local:4317
      protocol: grpc
      timeout: 30s
  instrumentation:
    spring-webmvc:
      enabled: true
    jdbc:
      enabled: true
    kafka:
      enabled: true

比起 Java Agent,Spring Boot Starter 的优点是更容易与现有依赖管理和配置体系集成,缺点是需要改代码并需要显式引入各框架的 instrumentation 库。


5. 采样策略:在成本与精度之间权衡

采样(Sampling)决定哪些 Trace 被保留和上报。100% 采样在流量高峰期间会产生巨量数据,推高存储成本和 Collector 负载;采样过低又可能错过关键异常链路。

5.1 Head-Based Sampling(头部采样)

在 Trace 的起点即做出是否采样的决策,后续所有服务遵循同一决策。优点是实现简单、一致性高;缺点是无法根据 Trace 整体是否出错来决策,可能丢弃包含错误但起初看起来"正常"的链路。

import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.sdk.trace.samplers.SamplingDecision;
import io.opentelemetry.sdk.trace.samplers.SamplingResult;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;

public class HeadBasedSamplingConfig {

    public static Sampler createSampler() {
        // 固定比率采样:保留 10% 的 Trace
        Sampler traceIdRatio = Sampler.traceIdRatioBased(0.1);

        // 父级优先采样:如果父 Span 已采样,则子 Span 一定采样
        // 如果父级未采样,则使用 traceIdRatio 进行决策
        Sampler parentBased = Sampler.parentBased(
                traceIdRatio,           // 根 Span 采样策略
                Sampler.alwaysOn(),     // 远程父 Span 已采样时的策略
                Sampler.alwaysOff(),    // 远程父 Span 未采样时的策略
                Sampler.alwaysOn(),     // 本地父 Span 已采样时的策略
                Sampler.alwaysOff()     // 本地父 Span 未采样时的策略
        );

        return parentBased;
    }

    public static SdkTracerProvider createTracerProvider() {
        return SdkTracerProvider.builder()
                .setSampler(createSampler())
                .build();
    }
}

5.2 Tail-Based Sampling(尾部采样)

在 Trace 完全结束后,根据整条链路的特征(如是否包含错误、延迟是否超过阈值)再做采样决策。能精准保留异常和慢链路,缺点是需要缓冲待决策的 Trace,内存开销较大。

尾部采样通常在 OpenTelemetry Collector 层实现:

# otel-collector-config.yaml 尾部采样处理器配置
processors:
  tail_sampling:
    decision_wait: 10s           # 等待所有 Span 到达的最长时间
    num_traces: 100000           # 内存中待决策 Trace 的最大数量
    expected_new_traces_per_sec: 10000
    policies:
      # 策略1:保留所有错误状态的 Trace
      - name: errors
        type: status_code
        status_code: { status_codes: [ ERROR ] }
      # 策略2:保留响应时间大于 2 秒的 Trace
      - name: slow_requests
        type: latency
        latency: { threshold_ms: 2000 }
      # 策略3:对满足特定属性(如 vip 用户)的 Trace 进行 50% 采样
      - name: vip_users
        type: probabilistic
        probabilistic: { sampling_percentage: 50 }
        # 此策略需配合 and 条件限制特定属性
      # 策略4:保留特定服务的 Trace(关键路径)
      - name: payment_service
        type: service_name
        service_name: { services: [ "payment-service" ] }

尾部采样需要评估内存容量:num_traces 乘以平均 Trace 的 Span 数量再乘以单个 Span 的平均大小,即为峰值内存占用。在 Kubernetes 中部署时,应为 Collector 分配充足的内存限制。

5.3 Rate Limiting Sampler(速率限制采样)

以每秒配额限制采样数量,而非固定比例,适合流量波动剧烈的场景。

import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.extension.trace.jaeger.sampler.JaegerRemoteSampler;

public class RateLimitingSamplerConfig {

    public static Sampler createSampler() {
        // 使用 Jaeger 自适应采样:通过 Agent 从 Jaeger Collector 获取采样策略
        // 支持按服务名和操作方法名动态调整采样率
        return JaegerRemoteSampler.builder()
                .setEndpoint("http://jaeger-agent.monitoring.svc.cluster.local:5778")
                .setPollingInterval(60) // 每 60 秒从后端拉取最新策略
                .setInitialSampler(Sampler.traceIdRatioBased(0.1))
                .build();
    }
}
采样策略决策时机优点缺点适用场景
AlwaysOn请求开始时保留全部数据数据量巨大,成本高测试环境、核心小额流量
TraceIdRatio请求开始时实现简单,性能高可能丢失异常链路通用生产环境
ParentBased请求开始时保证链路一致性依赖父级决策与 Ratio 组合使用
JaegerRemote请求开始时动态调整,智能化依赖 Jaeger 后端多服务动态管理
TailBasedTrace 结束时精准保留异常/慢链路Collector 内存压力大追求精准问题排查

5.4 采样与业务保活策略

在生产环境中,建议采用分层采样:核心支付链路使用较低的采样率或尾部采样保留异常;普通查询接口使用较高比例采样;健康检查、监控探针等无关流量使用 AlwaysOff 完全丢弃。

Sampler customSampler = Sampler.parentBased(
        Sampler.traceIdRatioBased(0.05),  // 根采样 5%
        Sampler.alwaysOn(),
        Sampler.alwaysOff(),
        Sampler.alwaysOn(),
        Sampler.alwaysOff()
);

6. W3C Trace Context:跨服务传播标准

6.1 为什么需要标准化传播

在微服务中,Trace 信息需要在 HTTP Header、MQ Message Property、gRPC Metadata 中传播。如果没有统一标准,服务 A 使用自定义 X-Request-Id,服务 B 使用 B3 Header,服务 C 又使用 Jaeger 的 Uber-Trace-Id,链路将无法串联。

W3C Trace Context 是 W3C 制定的官方标准(Recommendation),定义了两个核心 Header:

  • traceparent:承载 trace-id、parent-id、trace-flags
  • tracestate:承载厂商扩展的键值对(如租户路由信息)

6.2 traceparent 格式解析

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             |--|--|------------------------------|----------------|--|
                ver           trace-id                parent-id      flags

各字段含义:

  • version(2位十六进制):当前版本为 00
  • trace-id(32位十六进制):整条链路的唯一标识,如 4bf92f3577b34da6a3ce929d0e0e4736
  • parent-id(16位十六进制):当前 Span 的 SpanId,下游服务会将其作为 parent-span-id
  • trace-flags(2位十六进制):最低位为 sampled 标志,01 表示已采样,00 表示未采样

6.3 在 Java 中手动传播

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.context.Context;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import java.util.function.BiConsumer;

public class W3CPropagationDemo {

    // 从传入的 HTTP 请求中提取 Trace Context,构建父 Span
    public Span extractFromHttpHeaders(Map<String, String> headers) {
        // 定义 Getter,告诉 Propagator 如何从 Map 中读取 Header
        io.opentelemetry.context.propagation.TextMapGetter<Map<String, String>> getter =
                new io.opentelemetry.context.propagation.TextMapGetter<>() {
                    @Override
                    public Iterable<String> keys(Map<String, String> carrier) {
                        return carrier.keySet();
                    }
                    @Override
                    public String get(Map<String, String> carrier, String key) {
                        return carrier.get(key);
                    }
                };

        // 提取上下文
        Context extractedContext = W3CTraceContextPropagator.getInstance()
                .extract(Context.current(), headers, getter);

        // 基于提取的上下文创建 Span,自动建立父子关系
        return tracer.spanBuilder("OrderService.receiveOrder")
                .setParent(extractedContext)  // 关键:设置为提取的上下文
                .setSpanKind(SpanKind.SERVER)
                .startSpan();
    }

    // 向发出的 HTTP 请求注入 Trace Context
    public void injectIntoHttpHeaders(Span currentSpan, BiConsumer<String, String> headerSetter) {
        // 将当前 Span 的上下文注入到 Header 中
        W3CTraceContextPropagator.getInstance().inject(
                Context.current().with(currentSpan),
                headerSetter,
                (carrier, key, value) -> carrier.accept(key, value)
        );
    }

    // 使用示例:HTTP 服务端入口
    public void handleHttpRequest(HttpExchange exchange) {
        Map<String, String> headers = new HashMap<>();
        exchange.getRequestHeaders().forEach((k, v) -> headers.put(k, v.get(0)));

        Span span = extractFromHttpHeaders(headers);
        try (Scope scope = span.makeCurrent()) {
            // 处理业务逻辑
            processOrder(exchange);
        } finally {
            span.end();
        }
    }

    // 使用示例:HTTP 客户端出口
    public void callDownstreamService(Span span) {
        Map<String, String> outgoingHeaders = new HashMap<>();
        outgoingHeaders.put("Content-Type", "application/json");

        injectIntoHttpHeaders(span, outgoingHeaders::put);

        // outgoingHeaders 现在包含 traceparent 和 tracestate
        httpClient.post("http://payment-service/api/charge", outgoingHeaders, body);
    }
}

6.4 与 B3 Propagation 的对比

在由 Zipkin 或旧版 Spring Cloud Sleuth 演进而来的系统中,可能仍在使用 B3 格式。OpenTelemetry Collector 支持格式转换,可以实现 B3 与 W3C 的互操作。

# otel-collector-config.yaml 多格式传播器配置
processors:
  # propagator 自动处理入站请求的格式,并以配置格式出站
  # 在 Java Agent 中通过参数指定
  # -Dotel.propagators=tracecontext,baggage,b3,b3multi,jaeger

# Java Agent 启动参数示例:
# -Dotel.propagators=tracecontext,baggage
# tracecontext = W3C Trace Context
# baggage    = W3C Baggage
# jaeger     = Jaeger 格式
# b3         = B3 单 Header 格式
# b3multi    = B3 多 Header 格式

在迁移期间,建议同时启用 tracecontext,b3,让新旧服务都能兼容。待所有服务升级完成后,再移除 B3 支持。

6.5 Baggage 传播示例

public class BaggagePropagationDemo {

    public void setUserContext(String tenantId, String userId) {
        // 在当前上下文中设置 Baggage,随 Trace Context 自动传播
        io.opentelemetry.api.baggage.Baggage.current()
                .toBuilder()
                .put("tenant.id", tenantId)
                .put("user.id", userId)
                .put("experiment.group", "variant-a")
                .build()
                .storeInContext(Context.current())
                .makeCurrent();
    }

    public void readUserContextInDownstream() {
        // 在下游服务中读取 Baggage
        io.opentelemetry.api.baggage.Baggage baggage =
                io.opentelemetry.api.baggage.Baggage.fromContext(Context.current());

        String tenantId = baggage.getEntryValue("tenant.id");
        String userId = baggage.getEntryValue("user.id");

        // 将租户 ID 附加到当前 Span 属性中,便于按租户维度查询
        Span.current().setAttribute("tenant.id", tenantId);
        Span.current().setAttribute("user.id", userId);

        // 执行租户隔离的数据库查询
        executeWithTenant(tenantId, () -> processUserRequest(userId));
    }
}

Baggage 的最大价值在于跨层透传业务上下文,但要注意规范要求 Baggage 的总长度不应过大(建议键值对数量和总长度有节制),否则会增加网络开销。


7. Jaeger 部署与 UI 分析

7.1 Jaeger 架构组件

Jaeger 是 Uber 开源、CNCF 毕业的分布式追踪系统,核心组件包括:

  • Agent:本机守护进程,接收 UDP 数据并批量转发给 Collector
  • Collector:接收追踪数据,进行验证、处理、转换后写入存储
  • Query:提供 API 和 UI 查询接口
  • Ingester:从 Kafka 消费数据写入存储(可选项)
  • Storage:支持 Elasticsearch、Cassandra、Badger(本地存储)

7.2 Kubernetes 部署

# jaeger-deployment.yaml — All-in-One 模式,适合测试环境
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jaeger
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jaeger
  template:
    metadata:
      labels:
        app: jaeger
    spec:
      containers:
        - name: jaeger
          image: jaegertracing/all-in-one:1.57
          env:
            - name: COLLECTOR_OTLP_ENABLED
              value: "true"
          ports:
            - name: otlp-grpc
              containerPort: 4317
            - name: otlp-http
              containerPort: 4318
            - name: ui
              containerPort: 16686
            - name: collector
              containerPort: 14268
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "2Gi"
              cpu: "1000m"
---
apiVersion: v1
kind: Service
metadata:
  name: jaeger
  namespace: monitoring
spec:
  selector:
    app: jaeger
  ports:
    - name: ui
      port: 16686
      targetPort: 16686
    - name: otlp-grpc
      port: 4317
      targetPort: 4317
    - name: otlp-http
      port: 4318
      targetPort: 4318
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: jaeger-ui
  namespace: monitoring
spec:
  rules:
    - host: jaeger.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: jaeger
                port:
                  number: 16686

All-in-One 模式将所有组件打包为一个进程,数据存储于内存,进程退出即丢失,仅适用于开发和测试。

7.3 生产环境部署:Elasticsearch 后端

# jaeger-production.yaml — 独立 Collector + Query + Elasticsearch
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jaeger-collector
  namespace: monitoring
spec:
  replicas: 3
  selector:
    matchLabels:
      app: jaeger-collector
  template:
    metadata:
      labels:
        app: jaeger-collector
    spec:
      containers:
        - name: collector
          image: jaegertracing/jaeger-collector:1.57
          args:
            - "--es.server-urls=http://elasticsearch.monitoring.svc.cluster.local:9200"
            - "--es.num-shards=5"
            - "--es.num-replicas=1"
            - "--collector.otlp.enabled=true"
          ports:
            - containerPort: 4317   # OTLP gRPC
            - containerPort: 4318   # OTLP HTTP
            - containerPort: 14250  # Agent 上报端口
          resources:
            requests:
              memory: "1Gi"
              cpu: "500m"
            limits:
              memory: "4Gi"
              cpu: "2000m"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jaeger-query
  namespace: monitoring
spec:
  replicas: 2
  selector:
    matchLabels:
      app: jaeger-query
  template:
    metadata:
      labels:
        app: jaeger-query
    spec:
      containers:
        - name: query
          image: jaegertracing/jaeger-query:1.57
          args:
            - "--es.server-urls=http://elasticsearch.monitoring.svc.cluster.local:9200"
            - "--query.base-path=/jaeger"
          ports:
            - containerPort: 16686
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: jaeger-collector-hpa
  namespace: monitoring
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: jaeger-collector
  minReplicas: 3
  maxReplicas: 12
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

生产环境中 Collector 应配置多副本和 HPA,Elasticsearch 作为存储后端时需要注意索引生命周期管理(ILM),定期将过期 Trace 数据迁移或删除。

7.4 Jaeger UI 核心功能

打开 Jaeger UI(http://jaeger.example.com),可进行以下分析操作:

  • Trace 搜索:按服务名、操作名、标签、持续时间范围、是否包含错误等条件过滤
  • Trace 时序图(Gantt Chart):直观展示各 Span 的开始时间、持续时间、父子关系
  • Trace 拓扑图:服务间的调用依赖关系,可发现循环依赖和热点服务
  • Span 详情:查看 Attributes、Events(日志)、References(父子/跟随关系)、Process 资源信息
  • 系统架构视图:基于 Trace 数据自动生成的服务依赖拓扑,可查看 QPS 和延迟统计

8. Grafana Tempo:与 Loki、Prometheus 的无缝集成

8.1 Tempo 的设计哲学

Grafana Tempo 是 Grafana Labs 推出的开源分布式追踪后端,设计目标是:仅通过 TraceId 即可快速检索完整 Trace,将搜索索引成本降至最低。Tempo 不索引 Span 的属性和标签,而是将 Trace 以对象形式存储在低成本对象存储(S3、GCS、Azure Blob)中,查询时通过 TraceId 直接定位。

与 Jaeger 的对比:

  • Jaeger:功能全面,支持丰富的搜索条件,依赖 Elasticsearch/Cassandra 索引,存储成本较高
  • Tempo:极简存储,超低成本,依赖 TraceId 查询,与 Grafana Stack 深度集成

大量云原生团队选择 Tempo + Loki + Prometheus + Grafana 组成完整的可观测性栈。

8.2 Tempo 部署配置

# tempo-deployment.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: tempo-config
  namespace: monitoring
data:
  tempo.yaml: |
    server:
      http_listen_port: 3200
      grpc_listen_port: 9095

    distributor:
      receivers:
        otlp:
          protocols:
            grpc:
              endpoint: 0.0.0.0:4317
            http:
              endpoint: 0.0.0.0:4318

    ingester:
      trace_idle_period: 10s
      max_block_bytes: 1_000_000
      max_block_duration: 5m

    compactor:
      compaction:
        compaction_window: 1h
        max_compaction_objects: 1000000
        block_retention: 168h          # Trace 数据保留 7 天
        compacted_block_retention: 1h

    storage:
      trace:
        backend: s3
        s3:
          bucket: tempo-traces
          endpoint: s3.ap-northeast-1.amazonaws.com
          region: ap-northeast-1
          insecure: false
        wal:
          path: /var/tempo/wal
        local:
          path: /var/tempo/blocks
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: tempo
  namespace: monitoring
spec:
  serviceName: tempo
  replicas: 3
  selector:
    matchLabels:
      app: tempo
  template:
    metadata:
      labels:
        app: tempo
    spec:
      containers:
        - name: tempo
          image: grafana/tempo:2.5.0
          args:
            - "-config.file=/etc/tempo/tempo.yaml"
          ports:
            - containerPort: 3200   # HTTP API(Query)
            - containerPort: 9095   # gRPC
            - containerPort: 4317   # OTLP gRPC
            - containerPort: 4318   # OTLP HTTP
          volumeMounts:
            - name: config
              mountPath: /etc/tempo
            - name: wal
              mountPath: /var/tempo/wal
            - name: blocks
              mountPath: /var/tempo/blocks
          resources:
            requests:
              memory: "2Gi"
              cpu: "500m"
            limits:
              memory: "8Gi"
              cpu: "2000m"
      volumes:
        - name: config
          configMap:
            name: tempo-config
        - name: wal
          emptyDir: {}
        - name: blocks
          emptyDir: {}

8.3 Grafana 数据源配置

在 Grafana 中同时配置 Tempo、Prometheus、Loki 三个数据源后,可以实现 Trace-to-LogsTrace-to-Metrics 的联动跳转:

# grafana-datasources.yaml - Tempo 数据源
apiVersion: 1
datasources:
  - name: Tempo
    type: tempo
    uid: tempo
    url: http://tempo.monitoring.svc.cluster.local:3200
    jsonData:
      tracesToLogs:
        datasourceUid: loki              # 点击 Span 可跳转到相关 Loki 日志
        tags: ['pod', 'namespace', 'service.name']
        mappedTags: [{ key: 'service.name', value: 'service' }]
        mapTagNamesEnabled: false
        spanStartTimeShift: '1h'
        spanEndTimeShift: '1h'
        filterByTraceID: true
        filterBySpanID: false
      tracesToMetrics:
        datasourceUid: prometheus         # 点击 Span 可查看关联 Prometheus 指标
        tags: [{ key: 'service.name', value: 'service' }]
        spanStartTimeShift: '1h'
        spanEndTimeShift: '1h'
      serviceMap:
        datasourceUid: prometheus
      nodeGraph:
        enabled: true
      search:
        hide: false
      lokiSearch:
        datasourceUid: loki

8.4 TraceID 驱动的观测工作流

当收到告警(由 Prometheus 触发)时,典型排查流程如下:

  1. 在 Grafana Alert 面板查看指标异常的时间点
  2. 在关联的 Loki 日志中搜索该时段的错误日志
  3. 在日志中发现 trace_id=abc123 的字段
  4. 点击 TraceID 直接跳转到 Tempo,查看完整的分布式链路
  5. 在链路图中锁定延迟最高或报错的 Span
  6. 从该 Span 再跳转回 Loki,查看该服务在相同时间窗口的详细日志

这种 Metrics -> Logs -> Traces 的联动被称为可观测性三角,是云原生排障的黄金路径。


9. 微服务链路追踪实战

9.1 多服务调用链示例

假设一个简化版电商下单流程涉及四个服务:

  • 网关(Gateway):接收用户 HTTP 请求,路由到下游
  • 订单服务(Order Service):创建订单、校验参数
  • 库存服务(Inventory Service):扣减库存
  • 支付服务(Payment Service):发起支付

9.2 网关层 Span 创建

@Component
public class TracingGatewayFilter implements GlobalFilter {
    private final Tracer tracer;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 从入站请求中尝试提取上游传递的 Trace Context
        Context parentContext = W3CTraceContextPropagator.getInstance()
                .extract(Context.current(), exchange.getRequest().getHeaders(),
                        new HttpHeadersGetter());

        Span span = tracer.spanBuilder("Gateway.route")
                .setParent(parentContext)
                .setSpanKind(SpanKind.SERVER)
                .setAttribute(SemanticAttributes.HTTP_REQUEST_METHOD,
                        exchange.getRequest().getMethodValue())
                .setAttribute(SemanticAttributes.URL_PATH,
                        exchange.getRequest().getPath().value())
                .startSpan();

        return chain.filter(exchange)
                .contextWrite(reactor.util.context.Context.of(Span.class, span))
                .doFinally(signal -> {
                    span.setAttribute(SemanticAttributes.HTTP_RESPONSE_STATUS_CODE,
                            exchange.getResponse().getStatusCode().value());
                    span.end();
                });
    }
}

9.3 服务间 Feign 客户端上下文传播

@Configuration
public class OpenTelemetryFeignConfig {

    @Bean
    public RequestInterceptor otelTraceContextInterceptor(Tracer tracer) {
        return template -> {
            // 将当前 Trace Context 注入到 Feign 请求的 Header 中
            W3CTraceContextPropagator.getInstance().inject(
                    Context.current(),
                    template,
                    (requestTemplate, key, value) -> requestTemplate.header(key, value)
            );

            // 同时注入 Baggage
            io.opentelemetry.api.baggage.Baggage baggage =
                    io.opentelemetry.api.baggage.Baggage.fromContext(Context.current());
            baggage.forEach(entry ->
                    template.header("baggage-" + entry.getKey(), entry.getValue().getValue())
            );
        };
    }
}

9.4 基于 Kafka 的异步消息追踪

@Configuration
public class KafkaTracingConfig {

    // 为 Kafka Producer 注入 Trace Context
    @Bean
   public ProducerFactory<String, String> producerFactory() {
        Map<String, Object> config = new HashMap<>();
        config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092");
        config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

        DefaultKafkaProducerFactory<String, String> factory =
                new DefaultKafkaProducerFactory<>(config);
        return factory;
    }

    // KafkaTemplate 发送消息时自动携带 traceparent header
    @Bean
    public KafkaTemplate<String, String> kafkaTemplate() {
        KafkaTemplate<String, String> template = new KafkaTemplate<>(producerFactory());
        template.setProducerListener(new TracingProducerListener<>(GlobalOpenTelemetry.get()));
        return template;
    }

    // 消费者端提取 Trace Context 并创建 Consumer Span
    @KafkaListener(topics = "order-events", groupId = "notification-service")
    public void consumeOrderEvent(ConsumerRecord<String, String> record) {
        // 从 Kafka Record Header 中提取 traceparent
        Headers headers = record.headers();
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach(h -> headerMap.put(h.key(), new String(h.value())));

        Context parentContext = W3CTraceContextPropagator.getInstance()
                .extract(Context.current(), headerMap,
                        new io.opentelemetry.context.propagation.TextMapGetter<Map<String, String>>() {
                            public Iterable<String> keys(Map<String, String> carrier) {
                                return carrier.keySet();
                            }
                            public String get(Map<String, String> carrier, String key) {
                                return carrier.get(key);
                            }
                        });

        Span span = GlobalOpenTelemetry.getTracer("notification-service")
                .spanBuilder("KafkaListener.processOrderEvent")
                .setParent(parentContext)
                .setSpanKind(SpanKind.CONSUMER)
                .setAttribute("messaging.system", "kafka")
                .setAttribute("messaging.destination", record.topic())
                .setAttribute("messaging.operation", "process")
                .setAttribute("messaging.message_id", String.valueOf(record.offset()))
                .startSpan();

        try (Scope scope = span.makeCurrent()) {
            sendNotification(record.value());
        } catch (Exception e) {
            span.setStatus(StatusCode.ERROR);
            span.recordException(e);
        } finally {
            span.end();
        }
    }
}

9.5 自定义 Span 处理与数据富化

在实际场景中,可能需要对自动生成的 Span 进行增强,比如附加业务订单号、用户级别等信息。

import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;

public class BusinessAttributeSpanProcessor implements SpanProcessor {

    @Override
    public void onStart(Context parentContext, ReadWriteSpan span) {
        // 从当前业务上下文中提取信息并附加到 Span
        BusinessContext businessContext = BusinessContextHolder.get();
        if (businessContext != null) {
            span.setAttribute("biz.order_no", businessContext.getOrderNo());
            span.setAttribute("biz.user_level", businessContext.getUserLevel());
            span.setAttribute("biz.merchant_id", businessContext.getMerchantId());
            span.setAttribute("biz.channel", businessContext.getChannel());
        }

        // 附加 JVM 运行时信息
        span.setAttribute("jvm.gc_count", GcMetrics.getCount());
    }

    @Override
    public boolean isStartRequired() {
        return true;
    }

    @Override
    public void onEnd(ReadableSpan span) {
        // Span 结束时无需处理
    }

    @Override
    public boolean isEndRequired() {
        return false;
    }
}

在初始化 SdkTracerProvider 时注册此 Processor:

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
        .addSpanProcessor(new BusinessAttributeSpanProcessor())
        .addSpanProcessor(BatchSpanProcessor.builder(otlpExporter).build())
        .setResource(resource)
        .build();

10. 生产环境最佳实践

10.1 高可用 Collector 架构

# otel-collector-production.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-collector-config
  namespace: monitoring
data:
  collector.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
            max_recv_msg_size_mib: 16
          http:
            endpoint: 0.0.0.0:4318

    processors:
      batch:
        timeout: 2s
        send_batch_size: 1024
        send_batch_max_size: 2048

      memory_limiter:
        limit_mib: 1500
        spike_limit_mib: 300
        check_interval: 5s

      tail_sampling:
        decision_wait: 15s
        num_traces: 200000
        expected_new_traces_per_sec: 5000
        policies:
          - name: errors
            type: status_code
            status_code: { status_codes: [ERROR] }
          - name: slow
            type: latency
            latency: { threshold_ms: 3000 }
          - name: probabilistic
            type: probabilistic
            probabilistic: { sampling_percentage: 5 }

      resource:
        attributes:
          - key: environment
            value: production
            action: upsert
          - key: collector.node
            from_attribute: k8s.node.name
            action: upsert

    exporters:
      otlp/jaeger:
        endpoint: jaeger-collector.monitoring.svc.cluster.local:4317
        tls:
          insecure: true
        sending_queue:
          enabled: true
          num_consumers: 10
          queue_size: 5000

      otlp/tempo:
        endpoint: tempo.monitoring.svc.cluster.local:4317
        tls:
          insecure: true

      prometheusremotewrite:
        endpoint: http://prometheus.monitoring.svc.cluster.local:9090/api/v1/write

    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, tail_sampling, batch, resource]
          exporters: [otlp/jaeger, otlp/tempo]
        metrics:
          receivers: [otlp]
          processors: [memory_limiter, batch, resource]
          exporters: [prometheusremotewrite]
---
# Deployment 配置多副本 + HPA
apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-collector
  namespace: monitoring
spec:
  replicas: 3
  selector:
    matchLabels:
      app: otel-collector
  template:
    metadata:
      labels:
        app: otel-collector
    spec:
      containers:
        - name: collector
          image: otel/opentelemetry-collector-contrib:0.104.0
          args:
            - "--config=/etc/otel/collector.yaml"
          ports:
            - containerPort: 4317
            - containerPort: 4318
            - containerPort: 8888   # 自身 metrics 暴露端口
          volumeMounts:
            - name: config
              mountPath: /etc/otel
          resources:
            requests:
              memory: "2Gi"
              cpu: "500m"
            limits:
              memory: "4Gi"
              cpu: "2000m"
      volumes:
        - name: config
          configMap:
            name: otel-collector-config

生产部署的核心要点:

  • memory_limiter:防止 Collector OOM,保护网关稳定
  • 发送队列(sending_queue):Exporter 后端短暂不可用时缓存数据
  • 多后端双写:Trace 同时写入 Jaeger 和 Tempo,实现冗余和不同场景查询
  • HPA + 多副本:根据 CPU 和内存自动扩缩容
  • 分离采集与处理:DaemonSet 模式的 Agent 负责轻量采集,Gateway 负责汇聚和复杂处理

10.2 性能优化清单

  • BatchSpanProcessor 参数调优:根据应用 QPS 和 99 分位延迟调整 max_queue_sizeschedule_delay,平衡实时性和导出吞吐量
  • 避免高频 Span:循环内部的操作不要每个迭代都创建 Span,应在外层统一包裹
  • 精简 Attributes:单个 Span 的 Attributes 数量建议控制在 32 个以内,每个 Attribute 值不宜过长
  • 采样策略分层:核心交易链路保留 100% 或尾部采样;日志查询、报表导出等低频高量流量大幅降采样
  • Exporter 超时与重试:配置合理的超时和退避策略,避免重试风暴压垮 Collector
  • 连接池复用:OTLP Exporter 使用 gRPC 持久连接,避免频繁建立 TCP 连接的开销

10.3 安全与隐私

  • 敏感数据脱敏:切勿在 Span Attributes 或 Baggage 中记录密码、Token、信用卡号等敏感信息
  • PII 过滤处理器:在 Collector 层面配置 attributes 处理器,正则匹配并删除或替换敏感字段
  • 传输加密:生产环境必须为 OTLP gRPC 配置 TLS,insecure: true 仅限测试使用
  • 访问控制:Jaeger UI 和 Grafana 需要接入统一认证(SSO、OAuth2),防止 Trace 数据泄露业务信息

10.4 监控你的监控系统

链路追踪系统本身也需要被监控:

  • Collector 队列深度otelcol_exporter_queue_size 持续增长表明后端接收能力不足
  • 导出失败率otelcol_exporter_send_failed_spans 上升需要排查网络或后端健康状态
  • 采样丢弃率otelcol_processor_tail_sampling_count 对比实际流量,评估采样策略合理性
  • SDK 缓冲区otel_trace_sdk_span_processor_queue_size 反映应用侧是否积压
  • 端到端延迟:从 Span 产生到在 UI 中可查询的全链路耗时,应在 10 秒以内

FAQ 常见问题

Q1:开启链路追踪后应用性能下降明显,如何排查?

A1:首先检查 SDK 配置,BatchSpanProcessor 的队列和线程参数是否合理;其次检查是否创建了过多细粒度 Span,例如每个 SQL 行操作都埋点;接着确认 Exporter 的网络连通性,如果 Collector 不可达,Span 会在内存中堆积;最后使用 JFR 或 Arthas 分析 CPU 热点,通常 Span.end() 和序列化是主要消耗点。

Q2:异步线程池中的 Span 无法自动关联,怎么办?

A2:Java Agent 能自动处理常见框架的上下文传播,但对于自定义线程池需要使用 Context.taskWrapping() 包装线程池,或在提交任务前捕获 Context 并在执行时 makeCurrent()。Reactor/WebFlux 场景应使用 contextWrite() 将 Span 绑定到响应式上下文。

Q3:已经用了 SkyWalking / Zipkin,如何迁移到 OpenTelemetry?

A3:OpenTelemetry Collector 支持接收 Zipkin 和 Jaeger 格式的数据并转换为 OTLP,可以分阶段迁移:第一阶段在各应用部署 Collector Sidecar,将旧格式数据转发到 OTel Collector;第二阶段逐步将应用埋点改为 OTel SDK 或 Agent;第三阶段统一后端存储到 Jaeger / Tempo。迁移期间使用多 propagator(-Dotel.propagators=tracecontext,b3,jaeger)保证格式兼容。

Q4:尾部采样导致 Collector 内存暴涨,如何优化?

A4:减小 decision_waitnum_traces 参数,降低单实例待决策的 Trace 数量;将 Collector 扩容为更多副本分散内存压力;如果仍无法满足,可改用智能头部采样(如 Jaeger Remote Sampler 的 per-operation 策略),或仅对高优先级服务开启尾部采样,其他服务使用固定比例采样。

Q5:Trace 数据存储成本过高,有什么降本方案?

A5:可以采用以下组合策略:一是提高采样率,对非关键服务从 10% 降到 1% 或更低;二是缩短数据保留时间,Trace 存储 3-7 天通常足够,冷数据可归档至对象存储;三是使用 Tempo 替代 Jaeger+ES,Tempo 的对象存储成本远低于 Elasticsearch 索引;四是在 Collector 中配置 filter 处理器,丢弃健康检查、静态资源等无价值 Span;五是对 Metrics 和 Logs 分离存储,避免将可聚合指标以 Span Event 形式存储。


总结

分布式链路追踪是破解微服务可观测性迷雾的关键工具。本文从 Trace、Span、Baggage 等基础概念出发,详细演示了 OpenTelemetry Java SDK 的手动埋点与自动埋点方式,深入讲解了头部采样、尾部采样、速率限制采样的配置策略,完整覆盖了 W3C Trace Context 的跨服务传播机制,并提供了 Jaeger 和 Grafana Tempo 在 Kubernetes 上的生产级部署方案。

建立一套成熟的链路追踪体系并非一蹴而就,建议遵循以下路径推进:

  1. 第一阶段:在测试环境部署 All-in-One Jaeger,为单个核心服务接入 OTel Java Agent,验证数据通路
  2. 第二阶段:推广至预发环境所有服务,配置 W3C Trace Context 传播,验证全链路串联
  3. 第三阶段:生产环境部署 OTLP Collector + Jaeger/Tempo 集群,启用比率采样,观察性能影响
  4. 第四阶段:引入尾部采样保留异常链路,结合 Grafana 实现 Metrics-Logs-Traces 联动排障
  5. 第五阶段:建立 Trace 数据治理规范,包括命名约定、 Attribute 白名单、采样策略生命周期管理

链路追踪只是可观测性的一个维度。当它与时序指标(Prometheus)、结构化日志(Loki)、性能剖析(Pyroscope/Parca)有机融合时,才能真正构建起面向云原生时代的全方位观测能力,让每一次故障排查都有据可循、每一次性能优化都精准有效。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「distributed-systems」更多文章

  1. 分布式高可用架构模式:多活、容灾、降级与 K8s 编排高可用
  2. 分布式缓存深度策略:Redis Cluster、一致性哈希与多级缓存架构
  3. 分布式消息队列深度选型:Kafka、RocketMQ、Pulsar 与 RabbitMQ 多维对比