一、容器化环境里追踪为什么这么难

想象一下,你在一个微服务架构里开发应用,一个用户请求从网关进来,可能要经过十几个服务才能最终返回结果。在传统部署模式下,你还能靠日志串联一下请求路径,但在 Kubernetes 这种容器编排环境里,情况就完全不一样了。Pod 随时可能被重启、销毁、重新调度到不同的节点上,日志分散在各个容器里,想要搞清楚一个请求到底经过了哪些服务、每个服务花了多少时间、哪一步出了问题,简直是头大。

分布式追踪就是为了解决这个问题而生的技术。它给每一个请求打上一个唯一的追踪ID,然后跟着这个请求一路走,把经过的每一个服务、每一个操作都记录下来,形成一个完整的链路。Jaeger 是目前最流行的开源分布式追踪系统之一,它基于 OpenTracing 标准,采集、处理和可视化追踪数据的能力都非常强。

但把 Jaeger 真正部署到 Kubernetes 里,并且让你的应用能正常上报追踪数据,中间的坑可不少。很多同学直接照着官方文档抄了一遍配置,结果发现 Jaeger 的界面打开是空的,一条链路都看不到。这通常不是 Jaeger 本身的问题,而是配置层面的细节没有处理好。

下面这套内容,我会从零开始,手把手教你怎么在 Kubernetes 里把 Jaeger 跑起来,再把你的 Java 应用接进去,让整个链路追踪真正落地。所有示例都使用 Java (Spring Boot) + Kubernetes 这个技术栈,配置项我会逐行解释,保证你看完就能上手。

二、整体架构和核心组件

在动手之前,先搞清楚 Jaeger 在 Kubernetes 里到底是怎么工作的,这决定了你后面配置的思路。

2.1 Jaeger 的核心组件

Jaeger 系统主要由四个部分组成:

Collector(收集器):它是追踪数据的入口。你的应用把追踪数据通过网络发给 Collector,Collector 负责接收数据、处理数据、然后把数据写到后端存储里。

Query(查询服务):这是用户看到的那个 Web 界面背后的服务。你在浏览器里打开 Jaeger 界面,输入查询条件,Query 服务会从存储里把追踪数据捞出来返回给你。

Storage(存储后端):用于持久化存储追踪数据。常见选择有 Elasticsearch、Cassandra、Badger(Jaeger 自带的轻量级存储)等。生产环境通常用 Elasticsearch,测试环境可以用 Badger。

Agent(代理):部署在每个节点上,本地收集追踪数据然后批量转发给 Collector。不过在全容器化环境下,很多团队选择跳过 Agent,让应用直接发数据给 Collector。

2.2 Kubernetes 中的部署拓扑

在 Kubernetes 集群里,典型的 Jaeger 部署方式如下:

  • Jaeger All-in-One 或分拆部署为 Deployment
  • Collector 暴露为 ClusterIP 类型的 Service,供集群内应用访问
  • Query 暴露为 ClusterIP 或 Ingress 类型,供用户浏览器访问
  • 存储后端通过 StatefulSet 部署,挂载持久卷保证数据不丢失
  • 可选的 Agent 以 DaemonSet 方式部署在每个节点上

对于学习和中小规模场景,Jaeger 提供了一个 All-in-One 镜像,把所有组件打包在一起,非常适合快速上手。生产环境则建议使用官方的 Helm Chart 做分拆部署。

三、从零开始部署 Jaeger

3.1 使用 Helm Chart 部署生产级 Jaeger

Helm 是 Kubernetes 的包管理工具,用 Helm 部署 Jaeger 是最推荐的方式,因为 Helm Chart 已经帮你处理好了大部分细节配置。

首先安装 Jaeger 官方 Helm Chart 的依赖工具:

# 添加 Jaeger 的 Helm 仓库
helm repo add jaeager https://jaegertracing.github.io/helm-charts

# 更新仓库索引
helm repo update

# 查看可用版本
helm search repo jaeger/jaeger --versions

然后执行安装命令,这里我选择带 Elasticsearch 存储的方案:

# 创建用于安装 Jaeger 的命名空间
kubectl create namespace jaeger

# 安装 Jaeger(带 Elasticsearch 存储后端)
helm install jaeger jaeger/jaeger \
  --namespace jaeger \
  --set "jaeger.collector.enabled=true" \
  --set "jaeger.query.enabled=true" \
  --set "elasticsearch.master.enabled=true" \
  --set "elasticsearch.data.enabled=true" \
  --set "jaegerQuery.ingress.enabled=true" \
  --set "jaegerQuery.ingress.hosts[0]=jaeger.example.com" \
  --set "jaegerCollector.ingress.enabled=true" \
  --set "jaegerCollector.ingress.hosts[0]=collector.example.com"

3.2 验证部署结果

部署完成后,检查所有 Pod 是否正常运行:

# 查看 jaeger 命名空间下的所有 Pod
kubectl -n jaeger get pods

# 查看 Service 的暴露情况
kubectl -n jaeger get svc

# 确认 Ingress 是否创建成功
kubectl -n jaeger get ingress

正常情况下,你应该能看到以下 Pod 都在 Running 状态:

  • elasticsearch-master-*:Elasticsearch 主节点
  • elasticsearch-data-*:Elasticsearch 数据节点
  • jaeger-collector-*:收集器
  • jaeger-query-*:查询服务

如果一切正常,打开你配置的 Ingress 域名(比如 jaeger.example.com),就能看到 Jaeger 的 Web 界面了。

3.3 快速测试:All-in-One 模式

如果你只是想快速验证追踪功能是否生效,不想折腾 Elasticsearch 这些重资源组件,可以用 All-in-One 模式:

# 创建一个 All-in-One 模式的 Deployment
cat <<EOF | kubectl -n jaeger apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jaeger-allinone
  namespace: jaeger
  labels:
    app: jaeger-allinone
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jaeger-allinone
  template:
    metadata:
      labels:
        app: jaeger-allinone
    spec:
      containers:
        - name: jaeger
          image: jaegertracing/all-in-one:1.52.0
          ports:
            - containerPort: 16686   # 查询界面端口
              name: query
            - containerPort: 14250   # gRPC collector 端口
              name: grpc-collector
            - containerPort: 14268   # HTTP collector 端口
              name: http-collector
            - containerPort: 5775    # UDP 接收 Zipkin 数据
              name: zipkin-udp
            - containerPort: 5778    # UDP 接收 OpenTracing 数据
              name: otel-udp
          env:
            - name: COLLECTOR_ZIPKIN_HTTP_PORT
              value: "9411"
            - name: SPAN_STORAGE_TYPE
              value: "badger"         # 使用内置 Badger 存储
          resources:
            requests:
              memory: "256Mi"
              cpu: "200m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /
              port: 16686
            initialDelaySeconds: 15
            periodSeconds: 20
          readinessProbe:
            httpGet:
              path: /
              port: 16686
            initialDelaySeconds: 5
            periodSeconds: 10
---
# 创建 Service 暴露 Jaeger 的查询界面
apiVersion: v1
kind: Service
metadata:
  name: jaeger-query
  namespace: jaeger
spec:
  selector:
    app: jaeger-allinone
  ports:
    - port: 16686
      targetPort: 16686
      name: query
    - port: 14268
      targetPort: 14268
      name: http-collector
    - port: 14250
      targetPort: 14250
      name: grpc-collector
  type: ClusterIP
EOF

3.4 通过端口转发访问界面

在没有 Ingress 的配置环境(比如本地 K3s 或 Minikube)下,可以用端口转发访问 Jaeger 查询界面:

# 转发查询端口到本地 16686
kubectl -n jaeger port-forward svc/jaeger-query 16686:16686

# 转发收集器端口到本地 14268
kubectl -n jaeger port-forward svc/jaeger-query 14268:14268

然后浏览器访问 http://localhost:16686 就能看到 Jaeger 界面。

四、Java 应用接入 Jaeger 的完整配置

4.1 Spring Boot 应用添加依赖

技术栈:Java 17 + Spring Boot 3.x + Kubernetes

在你的 pom.xml 中添加 OpenTelemetry 和 Jaeger 相关的依赖:

<!-- 技术栈:Java 17 + Spring Boot 3.2 + OpenTelemetry + Jaeger -->
<!-- Spring Boot 已原生集成 OpenTelemetry,这是目前推荐的方式 -->

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- OpenTelemetry API -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-api</artifactId>
        <version>1.35.0</version>
    </dependency>

    <!-- OpenTelemetry SDK -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-sdk</artifactId>
        <version>1.35.0</version>
    </dependency>

    <!-- Jaeger Remote Reporter(直连 Jaeger Collector) -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
        <version>1.35.0</version>
    </dependency>

    <!-- Spring Boot Actuator(健康检查和监控端点) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- 服务发现(假设使用 Spring Cloud) -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

4.2 配置追踪上报地址

在 application.yml 中配置追踪相关的参数:

# 应用名称(Jaeger 中会以这个名称显示服务)
spring:
  application:
    name: order-service

server:
  port: 8080

# OpenTelemetry 配置
management:
  tracing:
    # 采样策略:始终采样(生产环境建议改为概率采样)
    sampling:
      probability: 1.0
    # OpenTelemetry 导出配置
    otlp:
      # Jaeger Collector 的 gRPC 地址
      # 在 Kubernetes 集群内,使用 Service 名称即可
      endpoint: http://jaeger-collector.jaeger.svc.cluster.local:4317
      # 导出协议
      protocol: grpc

# Jaeger 特定的配置(如果使用 Jaeger Agent)
# 下面这组配置说明如何让应用通过 Jaeger Agent 上报数据
jaeger:
  # Agent 地址(如果启用了 Agent)
  agent:
    # 本地节点上 Jaeger Agent 的 UDP 端口
    # 在 DaemonSet 模式下,Agent 监听在本机的这些端口
    host: jaeger-agent.jaeger.svc.cluster.local
    collector:
      enabled: true
      udp:
        host: jaeger-agent.jaeger.svc.cluster.local
        port: 6831

4.3 自定义 Trace 代码示例

在实际业务中,你可能需要手动创建 Span 来追踪特定操作的耗时:

/**
 * 订单服务 - 展示如何在 Spring Boot 中手动创建分布式追踪 Span
 * 技术栈:Java 17 + Spring Boot 3.2 + OpenTelemetry + Jaeger
 */
package com.example.orderservice.service;

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.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.TracerProvider;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.config.Sampler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class OrderTraceService {

    private final Tracer tracer;

    // 从配置中读取 Jaeger Collector 地址
    @Value("${management.tracing.otlp.endpoint}")
    private String collectorEndpoint;

    // Spring 自动注入 OpenTelemetry 实例
    public OrderTraceService(OpenTelemetry openTelemetry) {
        // 创建 Tracer,"订单服务" 是追踪标识
        this.tracer = openTelemetry.getTracer("OrderService", "1.0.0");
    }

    /**
     * 创建一个简单的 Span 用于追踪
     * 方法调用开始时创建 Span,方法结束时关闭
     */
    public void traceSimpleOperation() {
        // 创建一个新的 Span
        Span span = tracer.spanBuilder("创建订单")
                .setSpanKind(SpanKind.INTERNAL)
                .setAttribute(AttributeKey.stringKey("order.type"), "standard")
                .startSpan();

        try (Scope scope = span.makeCurrent()) {
            // 模拟业务操作
            System.out.println("正在处理订单...");
            Thread.sleep(100); // 模拟处理耗时

            // 标记操作成功
            span.setStatus(StatusCode.OK);
        } catch (Exception e) {
            // 标记操作失败,并记录错误信息
            span.setStatus(StatusCode.ERROR, "处理失败");
            span.recordException(e);
            throw new RuntimeException("订单创建失败", e);
        } finally {
            // 关闭 Span,上报追踪数据
            span.end();
        }
    }

    /**
     * 嵌套 Span 示例:展示如何追踪调用链
     * 父 Span 代表一个上层操作,子 Span 代表下层调用
     */
    public void traceNestedOperations() {
        // 父 Span:整个订单处理流程
        Span parentSpan = tracer.spanBuilder("订单处理流程")
                .setSpanKind(SpanKind.INTERNAL)
                .setAttribute(AttributeKey.longKey("order.id"), 12345L)
                .startSpan();

        try (Scope parentScope = parentSpan.makeCurrent()) {
            // 子 Span 1:验证订单
            Span validateSpan = tracer.spanBuilder("订单验证")
                    .setSpanKind(SpanKind.INTERNAL)
                    .setAttribute(AttributeKey.stringKey("step", "validate"))
                    .startSpan();
            validateSpan.setStatus(StatusCode.OK);
            validateSpan.end(); // 子 Span 独立关闭

            // 子 Span 2:扣减库存(模拟外部调用)
            Span inventorySpan = tracer.spanBuilder("库存扣减")
                    .setSpanKind(SpanKind.CLIENT)  // 标记为客户端调用
                    .setAttribute(AttributeKey.stringKey("remote.service"), "inventory-service")
                    .startSpan();

            try {
                // 模拟远程调用耗时
                Thread.sleep(200);
                inventorySpan.setStatus(StatusCode.OK);
            } catch (Exception e) {
                inventorySpan.setStatus(StatusCode.ERROR);
                inventorySpan.recordException(e);
            } finally {
                inventorySpan.end();
            }

            // 子 Span 3:支付处理
            Span paymentSpan = tracer.spanBuilder("支付处理")
                    .setSpanKind(SpanKind.CLIENT)
                    .setAttribute(AttributeKey.stringKey("payment.method"), "alipay")
                    .setAttribute(AttributeKey.doubleKey("payment.amount"), 299.99)
                    .startSpan();
            paymentSpan.setStatus(StatusCode.OK);
            paymentSpan.end();

            // 父 Span 标记完成
            parentSpan.setStatus(StatusCode.OK);
        } finally {
            parentSpan.end();
        }
    }

    /**
     * 通过 OpenTelemetry 自动配置创建 Tracer 的替代方案
     * 适合不想使用 Spring Boot 自动配置的情况
     */
    private Tracer createTracerManually() {
        // 创建 OTLP 导出器,指向 Jaeger Collector
        OtlpGrpcSpanExporter exporter = OtlpGrpcSpanExporter.builder()
                .setEndpoint(collectorEndpoint)
                .build();

        // 配置采样策略
        Sampler sampler = Sampler.parentBased(
                Sampler.traceIdRatioBased(0.5) // 50% 的追踪请求会被采样
        );

        // 构建 TracerProvider
        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
                .setResource(
                        io.opentelemetry.sdk.resources.Resource.builder()
                                .put(AttributeKey.stringKey("service.name"), "order-service")
                                .put(AttributeKey.stringKey("service.version"), "1.0.0")
                                .build()
                )
                .setSampler(sampler)
                .addSpanProcessor(BatchSpanProcessor.builder(exporter).build())
                .build();

        return tracerProvider.getTracer("manual-tracer", "1.0.0");
    }
}

4.4 Spring Boot Controller 示例

/**
 * 订单服务 Controller - 展示分布式追踪的完整请求链路
 * 技术栈:Java 17 + Spring Boot 3.2 + OpenTelemetry + Jaeger
 */
package com.example.orderservice.controller;

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;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.util.Map;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final Tracer tracer;
    private final RestTemplate restTemplate;

    public OrderController(OpenTelemetry openTelemetry, RestTemplate restTemplate) {
        this.tracer = openTelemetry.getTracer("OrderController");
        this.restTemplate = restTemplate;
    }

    /**
     * 创建订单接口 - 会调用库存服务和支付服务
     */
    @PostMapping("/create")
    public Map<String, Object> createOrder(@RequestBody Map<String, Object> request) {
        // 手动创建根 Span,代表整个创建订单的请求
        Span rootSpan = tracer.spanBuilder("POST /api/orders/create")
                .setSpanKind(SpanKind.SERVER)
                .startSpan();

        try (Scope scope = rootSpan.makeCurrent()) {
            // 设置追踪属性
            rootSpan.setAttribute("order.product", request.get("product"));
            rootSpan.setAttribute("order.quantity", (int) request.get("quantity"));

            // 调用库存服务
            Object inventoryResult = restTemplate.getForObject(
                    "http://inventory-service/api/inventory/check",
                    String.class
            );

            // 调用支付服务
            Object paymentResult = restTemplate.postForObject(
                    "http://payment-service/api/payments",
                    request,
                    String.class
            );

            rootSpan.setStatus(StatusCode.OK);
            return Map.of(
                    "orderId", "ORD-" + System.currentTimeMillis(),
                    "status", "created",
                    "inventoryResult", inventoryResult,
                    "paymentResult", paymentResult
            );
        } catch (Exception e) {
            rootSpan.setStatus(StatusCode.ERROR);
            rootSpan.recordException(e);
            throw new RuntimeException("创建订单失败", e);
        } finally {
            rootSpan.end();
        }
    }

    /**
     * 查询订单接口
     */
    @GetMapping("/{orderId}")
    public Map<String, Object> getOrder(@PathVariable String orderId) {
        // Spring Boot Actuator 会自动为 HTTP 请求创建 Span
        // 如果需要额外属性,可以获取当前 Span 并添加
        Span currentSpan = Span.current();
        currentSpan.setAttribute("order.id", orderId);

        // 模拟查询数据库
        Thread.yield();

        return Map.of(
                "orderId", orderId,
                "status", "pending",
                "product", "示例商品",
                "quantity", 1
        );
    }
}

五、Kubernetes 配置详解

5.1 Deployment 配置

# 订单服务的 Kubernetes Deployment 配置
# 技术栈:Java 17 + Spring Boot 3.2 + OpenTelemetry + Jaeger
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: default
  labels:
    app: order-service
    version: v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
      annotations:
        # PodDisruptionBudget 相关注解
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/actuator/prometheus"
    spec:
      containers:
        - name: order-service
          image: registry.example.com/order-service:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
              name: http
              protocol: TCP
          env:
            # 应用配置
            - name: SPRING_PROFILES_ACTIVE
              value: "kubernetes"

            # 应用名称(Jaeger 中显示的名称)
            - name: SPRING_APPLICATION_NAME
              value: "order-service"

            # Jaeger Collector 地址
            # 在集群内通过 Service DNS 解析
            - name: JAEGER_COLLECTOR_ENDPOINT
              value: "http://jaeger-collector.jaeger.svc.cluster.local:14268"

            # 如果使用 Jaeger Agent 模式
            - name: JAEGER_AGENT_HOST
              value: "jaeger-agent.jaeger.svc.cluster.local"

            # OpenTelemetry 端点
            - name: OTEL_EXPORTER_OTLP_ENDPOINT
              value: "http://jaeger-collector.jaeger.svc.cluster.local:4317"

            # 采样比例(生产环境建议降低)
            - name: OTEL_TRACES_SAMPLER_ARG
              value: "0.5"

            # Java 内存配置
            - name: JAVA_OPTS
              value: "-Xms512m -Xmx1g -XX:+UseG1GC"

          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "500m"

          # 健康检查探针
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 15
            timeoutSeconds: 5
            failureThreshold: 3

          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3

          # 启动探针(针对启动较慢的 Java 应用)
          startupProbe:
            httpGet:
              path: /actuator/health
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 30

      # 可选:Sidecar 模式注入 Jaeger Agent
      # 通过 DaemonSet 方式部署 Agent,这里不需要 Sidecar
      # 如果使用 Sidecar,配置如下(推荐 DaemonSet 方式)
      # initContainers:
      #   - name: jaeger-agent-init
      #     image: jaegertracing/agent:1.52.0
      #     command: ["sleep", "5"]  # 等待网络就绪

5.2 Jaeger Agent 的 DaemonSet 部署

如果你希望使用 Jaeger Agent 来收集追踪数据(推荐在生产环境使用),需要以 DaemonSet 方式部署:

# Jaeger Agent 的 DaemonSet 配置
# 每个节点上都会运行一个 Agent 实例
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: jaeger-agent
  namespace: jaeger
  labels:
    app: jaeger-agent
spec:
  selector:
    matchLabels:
      app: jaeger-agent
  template:
    metadata:
      labels:
        app: jaeger-agent
    spec:
      tolerations:
        - key: "node-role.kubernetes.io/master"
          operator: "Exists"
          effect: "NoSchedule"
      containers:
        - name: jaeger-agent
          image: jaegertracing/agent:1.52.0
          ports:
            # UDP 接收 OpenTracing 数据
            - containerPort: 5775
              protocol: UDP
              name: otel-udp
            # UDP 接收 Jaeger Thrift 数据
            - containerPort: 6831
              protocol: UDP
              name: jaeger-thrift-udp
            # UDP 接收 Jaeger Thrift 压缩数据
            - containerPort: 6832
              protocol: UDP
              name: jaeger-thrift-compressed
            # UDP 接收 Zipkin 数据
            - containerPort: 9411
              protocol: UDP
              name: zipkin-udp
            # TCP 接收 gRPC 数据
            - containerPort: 5778
              protocol: TCP
              name: admin
          env:
            # Collector 地址(Agent 将数据转发到 Collector)
            - name: COLLECTOR_HOSTPORT
              value: "jaeger-collector.jaeger.svc.cluster.local:14268"
            # 上报间隔
            - name: REPORTER_BATCH_QUEUE_SIZE
              value: "10000"
            - name: REPORTER_BATCH_FLUSH_INTERVAL
              value: "100"  # 毫秒
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "200m"
          livenessProbe:
            tcpSocket:
              port: 5775
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            tcpSocket:
              port: 5775
            initialDelaySeconds: 5
            periodSeconds: 10
      hostNetwork: true  # 使用主机网络,确保本地应用可以直接发送数据

5.3 Service 和 Ingress 配置

# Jaeger 相关的 Service 配置
apiVersion: v1
kind: Service
metadata:
  name: jaeger-agent
  namespace: jaeger
spec:
  selector:
    app: jaeger-agent
  ports:
    - port: 5775
      protocol: UDP
      name: otel-udp
      targetPort: 5775
    - port: 6831
      protocol: UDP
      name: jaeger-thrift-udp
      targetPort: 6831
    - port: 9411
      protocol: UDP
      name: zipkin-udp
      targetPort: 9411
  type: ClusterIP
---
# Jaeger Query 的 Ingress 配置(Nginx Ingress Controller)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: jaeger-query
  namespace: jaeger
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
spec:
  ingressClassName: nginx
  rules:
    - host: jaeger.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: jaeger-query
                port:
                  number: 16686
---
# Collector 的 Service 配置
apiVersion: v1
kind: Service
metadata:
  name: jaeger-collector
  namespace: jaeger
spec:
  selector:
    app: jaeger-collector
  ports:
    - port: 14250
      name: grpc-collector
      targetPort: 14250
    - port: 14268
      name: http-collector
      targetPort: 14268
  type: ClusterIP

六、高级配置技巧

6.1 采样策略配置

在生产环境中,不是所有请求都需要全量追踪。Jaeger 支持多种采样策略,合理配置可以显著降低存储压力:

# 采样策略示例配置(Jaeger 配置)
# 方式一:固定概率采样
sampling:
  strategies:
    default:
      type: probabilistic
      param: 0.1  # 10% 的请求会被采样

    # 方式二:基于服务名的差异化采样
    service:
      "payment-service":
        type: probabilistic
        param: 0.5  # 支付服务采样率高一些,因为重要
      "health-check-service":
        type: probabilistic
        param: 0.0  # 健康检查完全不采样

    # 方式三:自适应采样(Jaeger 1.36+ 支持)
    # 根据负载自动调整采样率
    default:
      type: jaegerremote
      options:
        local_sampling_probability: 0.01  # 本地兜底采样率
        decision_wait_timeout: 10000  # 等待决策的超时时间(毫秒)

在 Spring Boot 配置中对应如下:

# Spring Boot 中的采样配置
management:
  tracing:
    sampling:
      # 本地采样率
      probability: 0.1
      # 采样器类型:parentbased_traceidratio
      # 表示如果父 Span 被采样,子 Span 也被采样
      sampler: parentbased_traceidratio

# 如果使用远程采样决策(推荐生产使用)
opentelemetry:
  sampler:
    type: jaegerremote
    remote:
      endpoint: http://jaeger-collector.jaeger.svc.cluster.local:14250
      local-configuration:
        sampling-server-check-delay: 60000  # 60秒检查一次
        use-down-sampling: true              # 启用降采样

6.2 标签和元数据丰富化

为了让追踪数据更有价值,你可以在 Span 中添加自定义标签,方便在 Jaeger 界面上做过滤和聚合:

/**
 * 为 Span 添加业务标签的实用工具类
 * 技术栈:Java 17 + Spring Boot 3.2 + OpenTelemetry + Jaeger
 */
package com.example.orderservice.util;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;

/**
 * 追踪标签工具类
 * 封装常用的标签操作,让业务代码更简洁
 */
public class TraceTagUtil {

    // 预定义常用标签键
    public static final AttributeKey<String> USER_ID = AttributeKey.stringKey("user.id");
    public static final AttributeKey<String> REQUEST_ID = AttributeKey.stringKey("request.id");
    public static final AttributeKey<String> DATABASE_NAME = AttributeKey.stringKey("db.name");
    public static final AttributeKey<String> HTTP_METHOD = AttributeKey.stringKey("http.method");
    public static final AttributeKey<String> HTTP_STATUS = AttributeKey.stringKey("http.status_code");
    public static final AttributeKey<Long> CACHE_HIT = AttributeKey.booleanKey("cache.hit");

    /**
     * 为当前 Span 添加用户上下文信息
     * 通常在请求进入的入口点调用
     */
    public static void addUserContext(String userId) {
        Span currentSpan = Span.current();
        if (currentSpan != null && currentSpan.isRecording()) {
            currentSpan.setAttribute(USER_ID, userId);
        }
    }

    /**
     * 标记缓存命中情况
     * 用于分析缓存命中率,优化性能
     */
    public static void markCacheHit(boolean hit) {
        Span currentSpan = Span.current();
        if (currentSpan != null && currentSpan.isRecording()) {
            currentSpan.setAttribute(CACHE_HIT, hit);
        }
    }

    /**
     * 为数据库操作创建带标签的 Span
     */
    public static Scope traceDatabaseOperation(Tracer tracer, String operationName,
            String tableName, String queryType) {
        Span span = tracer.spanBuilder(operationName)
                .setAttribute(DATABASE_NAME, tableName)
                .setAttribute("db.operation", queryType)  // SELECT, INSERT, UPDATE, DELETE
                .startSpan();
        return span.makeCurrent();
    }

    /**
     * 记录异常到当前 Span
     */
    public static void recordError(Exception e) {
        Span currentSpan = Span.current();
        if (currentSpan != null && currentSpan.isRecording()) {
            currentSpan.setStatus(
                    io.opentelemetry.api.trace.StatusCode.ERROR,
                    e.getMessage()
            );
            currentSpan.recordException(e);
        }
    }
}

6.3 Kubernetes 环境变量自动注入

一个实用技巧是利用 Kubernetes 的 Downward API,让应用自动获取自身的 Pod 信息,这些信息会自动成为追踪标签:

# 利用 Downward API 注入 Pod 信息作为追踪上下文
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service-with-pod-info
  namespace: default
spec:
  template:
    spec:
      containers:
        - name: order-service
          image: registry.example.com/order-service:latest
          env:
            # 自动注入 Pod 名称
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            # 自动注入命名空间
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            # 自动注入节点名称
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            # 自动注入 Pod IP
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            # 自动注入服务账号
            - name: SERVICE_ACCOUNT
              valueFrom:
                fieldRef:
                  fieldPath: spec.serviceAccountName
            # OpenTelemetry 资源自动检测
            - name: OTEL_RESOURCE_ATTRIBUTES
              value: "k8s.pod.name=$(POD_NAME),k8s.namespace.name=$(POD_NAMESPACE),k8s.node.name=$(NODE_NAME)"
            - name: OTEL_SERVICE_NAME
              value: "order-service"

七、应用场景分析

7.1 微服务性能瓶颈定位

在一个典型的用户下单场景中,请求链路可能是这样的:

用户 -> API网关 -> 订单服务 -> 库存服务 -> 数据库
                                -> 支付服务 -> 第三方支付API
                                -> 通知服务 -> 短信/邮件服务

当用户反馈"下单特别慢"时,如果没有分布式追踪,你只能挨个查日志,很难快速定位是哪一步出了问题。有了 Jaeger 之后,你在界面上选择用户对应的订单ID,就能看到完整的时间线:

  • 库存服务查询数据库耗时 3 秒
  • 支付服务调用第三方支付 API 耗时 5 秒
  • 其他环节都在毫秒级

这样你就能精准地知道,优化应该从支付服务的第三方支付调用入手,而不是盲目地优化数据库或者加机器。

7.2 故障排查与根因分析

假设某天上午,大量用户收到"订单创建失败"的告警。传统的排查方式是登录每个服务看日志,然后试图关联时间线。这个过程既慢又容易出错。

使用 Jaeger 后,排查流程变成:

  1. 在 Jaeger 界面上筛选错误状态的请求(Status: Error)
  2. 按时间范围过滤(比如告警触发前后 10 分钟)
  3. 找到具体的错误链路
  4. 展开链路,看到错误发生在哪一步
  5. 查看错误详情和堆栈信息
  6. 定位到是库存服务的数据库连接池耗尽

整个过程从小时级缩短到分钟级,而且不需要在多个系统间切换上下文。

7.3 服务依赖关系可视化

Jaeger 的依赖图(Dependencies)功能可以自动分析服务之间的调用关系。你不需要手动维护架构图,只要服务正常运行并上报追踪数据,Jaeger 就会自动生成依赖关系图。

这对以下场景特别有用:

  • 新团队接手老系统时,快速了解服务间的调用关系
  • 评估某个服务下线的影响范围
  • 发现意料之外的服务依赖(比如服务A不应该调用服务B,但实际上调用了)
  • 设计服务拆分方案时评估影响

7.4 性能基线建立与监控

通过持续收集追踪数据,你可以为每个关键接口建立性能基线。比如:

  • 订单创建接口 P50 耗时 200ms,P99 耗时 1.5s
  • 商品查询接口 P50 耗时 50ms,P99 耗时 200ms
  • 用户登录接口 P50 耗时 100ms,P99 耗时 500ms

当性能指标超过基线时,可以自动触发告警。这比传统的 QPS 和响应时间监控更精准,因为你可以知道具体是哪个 Span 导致了性能下降。

八、技术优缺点分析

8.1 优势

降低故障排查成本:分布式追踪让故障定位从"大海捞针"变成"按图索骥"。开发者不需要在多个日志系统间来回切换,一个界面就能看完整个请求链路。这对于微服务架构来说,是降本增效的核心工具。

性能优化有据可依:传统的性能优化往往是"感觉哪里慢就优化哪里",没有数据支撑。有了追踪数据后,每个操作的实际耗时都清晰可见,优化目标明确,投入产出比高。

服务依赖关系自动维护:随着业务演进,服务间的调用关系会不断变化。手动维护的架构图很快就会过时。Jaeger 的依赖图基于实际流量数据生成,永远是最新的。

与 Kubernetes 生态无缝集成:Jaeger 的 Helm Chart 提供了丰富的配置选项,支持各种 Kubernetes 特性(如自动扩缩、节点调度等),且社区活跃,持续更新。

8.2 劣势

存储成本随流量线性增长:每一条追踪数据都要存储,流量越大,存储压力越大。虽然采样策略可以缓解这个问题,但高流量系统仍然需要投入相当的资源。

采样率与完整性的权衡:高采样率能获得完整的链路信息,但存储成本高;低采样率存储成本低,但可能丢失关键链路。自适应采样虽然能平衡两者,但配置复杂度较高。

对开发者的要求较高:合理的埋点需要理解业务逻辑和系统架构。如果埋点过多或过少,都会影响追踪效果。同时,开发者需要理解 OpenTracing/OpenTelemetry 的基本概念。

Jaeger 自身的运维成本:Jaeger 本身也是一个需要运维的系统。它的 Collector、Query、Agent 组件都需要关注健康状态、资源使用、版本升级等。

8.3 与替代方案的对比

与 Zipkin 对比:Zipkin 也是主流的分布式追踪系统,上手更简单,但功能上不如 Jaeger 丰富。Jaeger 在 UI 体验、依赖图生成、自适应采样等方面有明显优势。

与 Prometheus + Tempo 对比:Grafana 生态中的 Tempo 更轻量,与 Grafana 集成度更高,适合已经使用 Grafana 的团队。但 Jaeger 在分布式追踪的专业功能上更成熟。

与 AWS X-Ray 对比:X-Ray 是 AWS 原生的追踪服务,免运维,但如果你的基础设施在 AWS 以外,使用 X-Ray 的成本和复杂度都会显著增加。Jaeger 的开源特性使其更适合多云或混合云环境。

九、注意事项与最佳实践

9.1 不要在追踪中记录敏感信息

追踪数据可能被持久化存储,如果 Span 中包含了用户的密码、身份证号、银行卡号等敏感信息,会造成数据泄露风险。一定要建立规范,禁止在追踪标签中记录敏感数据。

/**
 * 脱敏处理工具 - 避免在追踪数据中记录敏感信息
 * 技术栈:Java 17 + Spring Boot 3.2 + OpenTelemetry + Jaeger
 */
public class SensitiveDataFilter {

    // 敏感字段关键词列表
    private static final List<String> SENSITIVE_KEYS = List.of(
            "password", "secret", "token", "credit_card",
            "id_number", "bank_card", "ssn", "private_key"
    );

    /**
     * 检查属性名是否包含敏感信息
     * @param key 属性名
     * @return true 表示该属性不应被记录
     */
    public static boolean isSensitive(String key) {
        String lowerKey = key.toLowerCase();
        return SENSITIVE_KEYS.stream().anyMatch(lowerKey::contains);
    }

    /**
     * 对值进行脱敏处理
     * @param value 原始值
     * @return 脱敏后的值
     */
    public static String maskValue(String value) {
        if (value == null || value.length() <= 4) {
            return "****";
        }
        // 只保留最后4位
        return "****" + value.substring(value.length() - 4);
    }

    /**
     * 安全的标签设置
     * 自动过滤敏感信息
     */
    public static void setSafeAttribute(Span span, String key, String value) {
        if (!isSensitive(key)) {
            span.setAttribute(AttributeKey.stringKey(key), value);
        } else {
            // 敏感字段只记录是否设置了值,不记录实际内容
            span.setAttribute(AttributeKey.stringKey(key + ".present"),
                    value != null);
        }
    }
}

9.2 控制追踪数据量

在高流量场景下,全量追踪会产生海量的数据。建议:

  1. 生产环境默认采样率不超过 10%,关键服务可以适当提高
  2. 健康检查、心跳请求完全不采样
  3. 使用自适应采样,让 Jaeger 根据负载自动调整采样率
  4. 设置合理的数据保留策略(比如追踪数据保留 7 天,依赖图数据保留 30 天)

9.3 注意网络通信方式的配置

这是最容易踩坑的地方之一。在 Kubernetes 中,应用和 Jaeger Collector 之间的网络通信需要特别注意:

如果使用 gRPC(推荐):

  • 应用端配置:OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger-collector.jaeger.svc.cluster.local:4317
  • 注意 gRPC 端口是 4317,不是 4318

如果使用 HTTP:

  • 应用端配置:OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger-collector.jaeger.svc.cluster.local:4318
  • HTTP 端口是 4318

如果使用 Jaeger Agent(UDP):

  • 应用端配置:JAEGER_AGENT_HOST=jaeger-agent.jaeger.svc.cluster.local
  • UDP 端口默认是 5775(OpenTracing)或 6831(Jaeger Thrift)

9.4 DNS 解析和 Service 名称确认

Kubernetes 中 Service 的 DNS 解析遵循 service-name.namespace.svc.cluster.local 的格式。常见错误包括:

  1. 忘记加 namespace:应该用 jaeger-collector.jaeger.svc.cluster.local,而不是 jaeger-collector.svc.cluster.local
  2. 使用了 Deployment 名称而不是 Service 名称
  3. 使用了 Pod IP 而不是 Service 名称(Pod IP 会变化)

在配置前,先在集群内验证 DNS 解析是否正常:

# 在集群中创建一个临时 Pod 来测试 DNS 解析
kubectl run dns-test --rm -it --restart=Never --image=busybox:1.36 -- sh

# 在 shell 中测试解析
nslookup jaeger-collector.jaeger.svc.cluster.local

# 测试连接
nc -zv jaeger-collector.jaeger.svc.cluster.local 14268

9.5 版本兼容性

Jaeger 和 OpenTelemetry 之间有版本兼容性问题。以下是已知兼容的版本组合:

# 推荐使用的版本组合(截至 2024 年)
# Jaeger 版本: 1.52+
# OpenTelemetry Java SDK 版本: 1.35+
# Spring Boot 版本: 3.2+

# 检查当前 Jaeger 版本
kubectl -n jaeger exec deploy/jaeger-collector -- jaeger-collector --version

# 查看 OpenTelemetry Java SDK 版本
mvn dependency:tree | grep opentelemetry

9.6 持久化存储配置

Jaeger 默认使用 Badger 存储(嵌入式,仅适合测试),生产环境必须配置外部存储。以下是 Elasticsearch 存储的配置:

# Jaeger Collector 的 Elasticsearch 存储配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: jaeger-collector-config
  namespace: jaeger
data:
  # Elasticsearch 连接配置
  STORAGE_TYPE: "elasticsearch"
  ES_SERVER_URLS: "http://elasticsearch-data.jaeger.svc.cluster.local:9200"
  # 是否启用 SSL
  ES_TLS_SKIP_HOST_VERIFY: "false"
  # 索引名称模板
  ES索引_PREFIX: "jaeger-v1"
  # 创建索引时的模板
  ES_CREATE_INDEX: "true"
  # 批量写入配置
  ES_BATCH_INDEX: "true"
  ES_BATCH_EMIT: "100"
  ES_NUM_SHARDS: "3"
  ES_NUM_REPLICAS: "1"
  ES_REFRESH_INTERVAL: "10s"
  # 健康检查超时
  ES_HEALTHCHECK_TIMEOUT: "5s"

十、调试和排错指南

10.1 常见问题排查清单

当追踪数据没有出现在 Jaeger 界面上时,按以下顺序排查:

# 第一步:确认应用 Pod 运行正常
kubectl get pods -l app=order-service

# 第二步:确认应用能否连接到 Jaeger Collector
kubectl exec -it <order-service-pod-name> -- sh
# 在容器内执行:
wget -qO- http://jaeger-collector.jaeger.svc.cluster.local:14268/

# 第三步:确认 Jaeger Collector 日志中没有错误
kubectl -n jaeger logs deploy/jaeger-collector --tail=100

# 第四步:确认存储后端正常
kubectl -n jaeger logs statefulset/elasticsearch-master --tail=50

# 第五步:直接在 Jaeger 界面查看是否有数据
kubectl -n jaeger port-forward svc/jaeger-query 16686:16686
# 然后浏览器访问 http://localhost:16686

# 第六步:检查网络策略是否阻止了通信
kubectl -n jaeger get networkpolicy

# 第七步:查看应用的追踪配置是否正确加载
kubectl exec -it <order-service-pod-name> -- env | grep -i jaeger
kubectl exec -it <order-service-pod-name> -- env | grep -i otlp
kubectl exec -it <order-service-pod-name> -- env | grep -i otelemetry

10.2 验证追踪数据的完整性

确认数据上报正常后,还需要验证追踪数据的完整性:

/**
 * 追踪数据验证工具 - 用于自测追踪功能是否正常
 * 技术栈:Java 17 + Spring Boot 3.2 + OpenTelemetry + Jaeger
 */
package com.example.orderservice.test;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class TracingHealthIndicator implements HealthIndicator {

    private final Tracer tracer;

    public TracingHealthIndicator(io.opentelemetry.api.OpenTelemetry openTelemetry) {
        this.tracer = openTelemetry.getTracer("TracingHealthIndicator");
    }

    @Override
    public Health health() {
        // 创建测试 Span 来验证追踪功能
        Span testSpan = tracer.spanBuilder("healthcheck-tracing-verification")
                .setAttribute("healthcheck", true)
                .startSpan();

        try (Scope scope = testSpan.makeCurrent()) {
            // 检查 Span 是否处于活跃状态
            boolean isRecording = testSpan.isRecording();

            if (isRecording) {
                // Span 正在记录数据,说明追踪配置正常
                testSpan.setStatus(
                        io.opentelemetry.api.trace.StatusCode.OK,
                        "Tracing is working"
                );
                return Health.up()
                        .withDetail("tracing", "active")
                        .withDetail("spanId", testSpan.getSpanContext().getSpanId())
                        .build();
            } else {
                // Span 没有处于记录状态,追踪可能未正确配置
                return Health.down()
                        .withDetail("tracing", "inactive")
                        .withDetail("reason", "Span is not recording")
                        .build();
            }
        } finally {
            testSpan.end();
        }
    }
}

10.3 使用 Kubernetes Debug 工具排查网络问题

# 使用 kubectl debug 创建调试 Pod
# 这个 Pod 拥有主机网络权限,适合排查网络问题
kubectl debug node/<node-name> -it --image=nicolaka/netshoot -- sh

# 在调试 Pod 中测试连接
# 测试到 Jaeger Collector 的 TCP 连接
nc -zv jaeger-collector.jaeger.svc.cluster.local 14250

# 测试 DNS 解析
nslookup jaeger-collector.jaeger.svc.cluster.local

# 使用 curl 测试 HTTP 端点
curl -v http://jaeger-collector.jaeger.svc.cluster.local:14268/

# 查看网络命名空间信息
ip addr show
ip route show
cat /etc/resolv.conf

# 测试 UDP 端口(Jaeger Agent 使用 UDP)
nc -zu jaeger-agent.jaeger.svc.cluster.local 5775

十一、完整实战:从零搭建追踪系统

下面是一个完整的实战流程,展示如何在 30 分钟内搭建一个可用的分布式追踪环境。

11.1 创建命名空间

# 创建专用的追踪命名空间
kubectl create namespace jaeger

# 给命名空间打上标签,方便管理
kubectl label namespace jaeger app.kubernetes.io/part-of=observability
kubectl label namespace jaeger app.kubernetes.io/managed-by=helm

11.2 部署 Jaeger

# 使用 Helm 部署 All-in-One 版本(快速验证用)
helm install jaeger jaeger/jaeger \
  --namespace jaeger \
  --set "jaegerCollector.enabled=true" \
  --set "jaegerQuery.enabled=true" \
  --set "elasticsearch.master.enabled=false" \
  --set "elasticsearch.data.enabled=false" \
  --set "jaeger.allInOne.enabled=true" \
  --set "jaeger.allInOne.ports.query.containerPort=16686" \
  --set "jaeger.allInOne.ports.grpcCollector.containerPort=14250" \
  --set "jaeger.allInOne.ports.httpCollector.containerPort=14268" \
  --set "jaeger.allInOne.ports.zipkinUdp.containerPort=9411" \
  --set "jaeger.allInOne.ports.otlpGrpc.containerPort=4317" \
  --set "jaeger.allInOne.env.SPAN_STORAGE_TYPE=badger" \
  --set "jaeger.allInOne.ports.grpcQuery.containerPort=16685" \
  --wait \
  --timeout 5m

# 验证部署状态
kubectl -n jaeger rollout status deploy/jaeger-allinone
kubectl -n jaeger get svc jaeger-query

# 创建端口转发,访问 Jaeger 界面
kubectl -n jaeger port-forward svc/jaeger-query 16686:16686 &

11.3 部署示例应用

/**
 * 完整的 Spring Boot 应用入口
 * 技术栈:Java 17 + Spring Boot 3.2 + OpenTelemetry + Jaeger
 */
package com.example.orderservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
        System.out.println("OrderService started - Jaeger tracing enabled!");
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

11.4 部署到 Kubernetes

# 构建并推送镜像
docker build -t registry.example.com/order-service:1.0.0 .
docker push registry.example.com/order-service:1.0.0

# 应用 Kubernetes 配置
kubectl apply -f order-service-deployment.yaml
kubectl apply -f order-service-service.yaml

# 等待 Pod 就绪
kubectl rollout status deploy/order-service --timeout=120s

# 验证 Pod 状态
kubectl get pods -l app=order-service
kubectl describe pod -l app=order-service

# 发送测试请求
# 先在集群内创建一个临时 Pod
kubectl run curl-test --rm -it --image=curlimages/curl -- sh

# 在 curl-test 中发送请求
curl -X POST http://order-service/api/orders/create \
  -H "Content-Type: application/json" \
  -d '{"product": "测试商品", "quantity": 1, "userId": "user123"}'

11.5 验证追踪数据

# 确认 Jaeger 中有数据
kubectl -n jaeger logs deploy/jaeger-allinone --tail=20

# 打开 Jaeger 界面(http://localhost:16686)
# 在 Services 下拉框选择 "order-service"
# 应该能看到刚才测试请求产生的 Span 数据

# 查看依赖图
# 点击 Jaeger 界面右上角的 Dependencies 按钮
# 可以看到 order-service 到 inventory-service、payment-service 的调用关系

十二、总结

在容器化环境中落地分布式追踪,本质上就是在 Kubernetes 的网络、存储、调度这些特性基础上,把 Jaeger 的各组件和应用串联起来。这个过程看似复杂,但只要理清组件之间的关系,按照步骤配置,其实并没有想象中那么难。

从实战经验来看,以下几个要点最为关键:

第一,网络连通性是一切的基础。在 Kubernetes 中,Service 名称的拼写、namespace 的正确性、端口号的选择,任何一个细节错了都会导致追踪数据丢失。所以配置完成后一定要做网络连通性验证。

第二,采样策略要合理。不是所有流量都需要追踪。生产环境全量追踪不仅浪费存储资源,还可能影响系统性能。建议默认 10% 采样率起步,关键服务适当提高。

第三,标签体系要规范。Span 的标签设计直接影响追踪数据的可用性。建议团队制定统一的标签规范,包括必须包含的字段(如用户ID、请求ID、服务名称)和禁止包含的字段(如密码、身份证号等敏感信息)。

第四,把追踪作为长期工程来做。Jaeger 不是一个装完就不用管的工具。存储的容量规划、版本的升级维护、采样策略的调优,都需要持续关注。建议把 Jaeger 相关的运维工作纳入日常巡检流程。

分布式追踪的价值在于,它把一个黑箱变成了一本透明的操作手册。每一次请求从哪里来、经过哪里、到哪里去,都一目了然。这种透明度带来的不仅是故障排查效率的提升,更是整个团队对系统健康状况的掌控力。

如果你的项目正在 Kubernetes 上运行,还在为"请求慢在哪一步"而头疼,那分布式追踪就是你需要的工具。从 Jaeger 开始,投入的时间会给你回报很多倍的效率提升。