分布式链路追踪选型:Jaeger与Zipkin实测对比
发布日期: 2026/08/07 阅读总量: 0

一次让全组加班的故障排查

2024年3月,我们公司的电商系统上线新促销功能后,用户频繁投诉下单耗时严重,最慢到4.8秒。后端服务没有任何错误日志,每个服务平均耗时都在200ms以内。

我带着两个同事排查了整整一天,从Nginx到MySQL慢查询日志,从Redis连接池到消息队列积压,全都没问题。后来发现是用户服务在调用促销服务时,每次请求都尝试连接一个已过期的Redis缓存节点,TCP超时时间设置为3秒,导致每次请求都要等3秒才能降级。而这个Redis节点的配置,是半年前另一位离职同事在测试环境改的,不小心合到了生产配置里。

这就是没有链路追踪的代价:服务间调用的黑盒让我们花了8个小时才找到3秒超时的根因。事后我做了选型调研,部署了分布式链路追踪系统。本文基于我在PHP 8.3 + Swoole和Java 17 + Spring Boot 3.2两种技术栈下的实践,对比Zipkin 3.4.2和Jaeger 1.57,给出可直接落地的方案。

先明确需求:我们的系统长什么样

在选型之前,先交代一下生产环境的具体情况,因为选型结论严重依赖系统规模:

  • Kubernetes 1.29集群,共42个微服务节点(Java + PHP混合)
  • 日均请求量约2,800万,高峰期QPS 8,500
  • 每个请求平均经过5.3个服务节点
  • 现有监控体系:Prometheus + Grafana(只覆盖CPU/内存/磁盘等系统指标)
  • 日志系统:ELK(每个服务独立索引,无法关联跨服务调用链)

Prometheus和ELK覆盖了「单机发生了什么」,但回答不了「一次请求到底经历了什么」。这就是链路追踪要解决的问题。

Zipkin和Jaeger的核心架构差异

两者都实现了Google Dapper论文中的概念模型,但实现方式差异很大:

对比项 Zipkin 3.4.2 Jaeger 1.57
后端存储 Elasticsearch 8.x / Cassandra 4.x / MySQL Elasticsearch 8.x / Cassandra 4.x / Badger(本地嵌入式)/ 纯内存
默认端口 9411(HTTP + 上报) 16686(UI)、4317(gRPC OTLP接入)、4318(HTTP OTLP接入)
上报协议 原生支持 Thrift / JSON / Scribe,兼容 OpenTelemetry 原生支持 OTLP/gRPC、OTLP/HTTP、Jaeger Thrift、Zipkin v2 JSON
采样策略 固定采样率(每个sepan决定是否采样) 固定采样率 + 基于速率限制的采样 + 自适应采样(Tail-Based)
UI界面 简洁、偏工程师风格,支持按服务名/标签/耗时筛选 交互更好,支持多时间范围快速切换、依赖关系图、服务性能面板
部署复杂度 单容器可直接跑,存储需额外配置 all-in-one镜像1分钟可跑,生产模式需搭配collector/query/agent三组件
社区活跃度(2024年) 维护中,新功能迭代较慢,主要集中在兼容OTLP CNCF毕业项目,迭代活跃,与OpenTelemetry深度集成

一句话概括:Zipkin适合「快速部署、轻量接入、团队规模小」的场景;Jaeger适合「Kubernetes原生生态、需要长期演进、团队有平台化诉求」的场景。

方案一:Zipkin部署与接入

1.1 Docker Compose快速部署

官方提供的Docker镜像目前最新的稳定版本是3.4.2。下面的配置同时起了Zipkin和Elasticsearch,用ES作为存储(Zipkin内置的H2内存存储只能用来测试,生产环境千万别用):

version: '3.8'

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.4
    container_name: zipkin-elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    ports:
      - "9200:9200"
    volumes:
      - es_data:/usr/share/elasticsearch/data
    healthcheck:
      test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5

  zipkin:
    image: openzipkin/zipkin:3.4.2
    container_name: zipkin-server
    environment:
      - STORAGE_TYPE=elasticsearch
      - ES_HOSTS=http://elasticsearch:9200
      - ES_INDEX=zipkin
      - JAVA_OPTS=-Xms512m -Xmx512m
      - QUERY_PORT=9411
    ports:
      - "9411:9411"
    depends_on:
      elasticsearch:
        condition: service_healthy

volumes:
  es_data:
    driver: local

执行 docker-compose up -d 后,访问 http://localhost:9411 就能看到Zipkin UI。

1.2 应用接入(Java/Sprint Boot)

Zipkin官方推荐的使用方式是引入 bravebrave-instrumentation-spring-web。但2024年了,我不建议再直接用Zipkin的客户端库,而是统一用OpenTelemetry(OTel)来接,这样后续可以平滑切换到其他后端。但为了完整性,这里给出Zipkin原生接入示例:

<!-- pom.xml -->
<dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave-instrumentation-spring-web</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-sender-okhttp3</artifactId>
    <version>3.4.0</version>
</dependency>
@Configuration
public class TracingConfig {

    @Bean
    public Tracing zipkinTracing() {
        return Tracing.newBuilder()
                .localServiceName("order-service")
                .spanReporter(AsyncReporter.create(
                        OkHttpSender.create("http://zipkin-server:9411/api/v2/spans")
                ))
                .sampler(Sampler.create(0.1f)) // 10%采样
                .build();
    }

    @Bean
    public BraveHttpTracing braveHttpTracing(Tracing tracing) {
        return BraveHttpTracing.create(tracing);
    }

    @Bean
    public TracingFilter tracingFilter(BraveHttpTracing braveHttpTracing) {
        return TracingFilter.create(braveHttpTracing);
    }
}

方案二:Jaeger部署与接入

2.1 Kubernetes环境下的生产级部署

Jaeger官方提供了 jaeger-operator,通过CRD管理实例。操作步骤:

# 安装jaeger-operator
kubectl create namespace observability
kubectl apply -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.57.0/jaeger-operator.yaml

# 创建一个 production-strategy 的Jaeger实例
cat <

这里用的是production策略,agent以DaemonSet形式在每个节点上运行,接收所有Pod上报的span,转发给collector。collector负责校验、采样和写入ES。

2.2 应用接入(OpenTelemetry统一方案)

我最终选择了接入OpenTelemetry,因为它解决了语言异构的问题(Java用agent,PHP用扩展)。Java应用接入Jaeger最简单的办法是使用OTel Java Agent,无侵入式接入:

# Dockerfile 片段
FROM openjdk:17-alpine

COPY app.jar /app.jar
# 下载 OTel Java Agent
RUN wget -O /opentelemetry-javaagent.jar \
    https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.4.0/opentelemetry-javaagent-2.4.0.jar

ENV JAVA_TOOL_OPTIONS="-javaagent:/opentelemetry-javaagent.jar"
ENV OTEL_SERVICE_NAME=order-service
ENV OTEL_TRACES_EXPORTER=jaeger
ENV OTEL_EXPORTER_JAEGER_ENDPOINT=http://jaeger-production-collector.observability:14250
ENV OTEL_TRACES_SAMPLER=parentbased_ratelimiting
ENV OTEL_TRACES_SAMPLER_ARG=10

ENTRYPOINT ["java", "-jar", "/app.jar"]

这里采样器用的是 parentbased_ratelimiting,参数表示每秒最多采样10条链路。相比固定比例采样,这种方式在低流量时能保留更多样本,在高流量时避免过量存储。

2.3 PHP服务接入Jaeger(Swoole场景)

我们这套系统里还有几个Swoole常驻内存的PHP服务,直接用OTel Java Agent肯定不行。这里用OpenTelemetry PHP扩展:

# 安装 otel-php 扩展
pecl install opentelemetry-1.0.1

# 加载插件
echo "extension=opentelemetry.so" > /etc/php/8.3/cli/conf.d/20-opentelemetry.ini
// 在Swoole Worker进程启动时初始化
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\API\Trace\TracerInterface;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;

class OtelHelper {
    private static ?TracerInterface $tracer = null;

    public static function init(): void {
        $transport = (new OtlpHttpTransportFactory())->create(
            'http://jaeger-production-collector.observability:4318/v1/traces'
        );
        $exporter = new SpanExporter($transport);
        $sampler = new ParentBased(new AlwaysOnSampler());
        $provider = new TracerProvider(
            new SimpleSpanProcessor($exporter),
            $sampler
        );
        self::$tracer = $provider->getTracer('php-order-service');
    }

    public static function getTracer(): TracerInterface {
        return self::$tracer;
    }
}

// 在HTTP请求入口处
OtelHelper::init();
$tracer = OtelHelper::getTracer();
$span = $tracer->spanBuilder('process-order')
    ->setAttribute('http.method', $_SERVER['REQUEST_METHOD'])
    ->setAttribute('http.url', $_SERVER['REQUEST_URI'])
    ->startSpan();
// ...业务逻辑...
$span->end();

注意:PHP版本需要8.2以上,Swoole建议使用4.8+版本,确保对OpenTelemetry的协程局部变量支持。这里坑不少,后面避坑部分会细说。

性能压测对比:采样率、吞吐量、存储开销

光看架构差异不够,我直接压测。压测工具:wrk 4.2.0。压测场景:通过压测工具向网关发起请求,链路经过 api-gateway → order-service → product-service → inventory-service 四个节点,每个节点间使用HTTP调用。中间件全程无业务逻辑,只做加法和JSON序列化。

基准数据(未接入链路追踪)

wrk -t8 -c500 -d120s --latency http://gateway.test:8080/api/check

Running 2m test @ http://gateway.test:8080/api/check
  8 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    95.24ms   24.55ms 687.31ms   84.37%
    Req/Sec   543.69     44.52   831.41    75.49%
  Latency Distribution
     50%   91.41ms
     75%  108.37ms
     90%  126.25ms
     99%  218.84ms
  258401 requests in 2.00m, 32.55MB read
Requests/sec:   2153.37

Zipkin 10%采样率下的表现

wrk -t8 -c500 -d120s --latency http://gateway.test:8080/api/check

Running 2m test @ http://gateway.test:8080/api/check
  8 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   101.23ms   26.82ms 701.55ms   83.12%
    Req/Sec   532.18     48.77   812.30    72.15%
  Latency Distribution
     50%   97.52ms
     75%  115.78ms
     90%  137.04ms
     99%  238.51ms
  251784 requests in 2.00m, 31.72MB read
Requests/sec:   2098.19

对比基准:P99延迟增加了20ms,吞吐从2153下降到2098(下降2.5%)。这是同一台物理机上跑Zipkin+ES的代价。Zipkin在上报时使用HTTP同步发送,虽然reporter内部有AsyncReporter做异步化,但是当队列满时会触发阻塞。

Jaeger 10%采样率下的表现

wrk -t8 -c500 -d120s --latency http://gateway.test:8080/api/check

Running 2m test @ http://gateway.test:8080/api/check
  8 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    98.43ms   25.19ms 695.42ms   83.91%
    Req/Sec   540.52     46.38   820.12    74.03%
  Latency Distribution
     50%   94.76ms
     75%  111.02ms
     90%  130.55ms
     99%  226.10ms
  255301 requests in 2.00m, 32.16MB read
Requests/sec:   2127.51

对比基准:P99延迟增加8ms,吞吐下降1.2%。Jaeger的OTLP/gRPC上报比Zipkin的HTTP/JSON效率高:每个span序列化后的body更小(OTLP使用Protobuf),gRPC不存在HTTP头重复开销。

高采样率(100%)下的表现差距

采样率调到100%时,差距就明显了:

指标 Zipkin 100%采样 Jaeger 100%采样 影响
吞吐量 1786 req/s(下降17%) 2043 req/s(下降5%) Zipkin明显更吃CPU,Prometheus的java进程CPU使用率高出60%
P99延迟 268ms(增加50ms) 238ms(增加20ms) Zipkin在复杂Trace下序列化耗时过高
ES存储一天 92GB(28%分片数56) 61GB(使用ESILM,分片数32) Zipkin的Span模型冗余字段较多
上报落盘耗时(P99) 3.7秒 1.1秒 Jaeger的Collector异步批量写入效率更高

结论:在4节点链路的压测场景下,Jaeger在100%采样时的性能开销仅为Zipkin的1/3左右。但如果用10%采样率,两者差异不大,Zipkin完全够用。

一个细节:集中收集 vs 端侧上报

这里面有个隐藏区别值得展开:Zipkin的reporter是「端侧直发」,即每个应用实例直接HTTP调用Zipkin Server的9411端口。Jaeger的agent是「边车收集」,即每个Kubernetes节点跑一个agent,应用侧把span发到本机agent,再通过gRPC推给collector。

这个架构差异导致了一个实际体验上的差距:

  • Zipkin数据源故障时,所有应用实例的HTTP调用会直接报错(但通常配置了超时1s,实际上是丢弃了span数据)
  • Jaeger的应用侧到agent是本地UDP或localhost,基本不会失败;agent到collector有重试和批量机制,短暂故障影响不大

如果你的微服务部署在Kubernetes里,Jaeger这种模式的好处在于:上报链路不依赖DNS和Service负载均衡,少了一跳网络转发。

完整接入后我们得到了什么

以Jaeger方案上线三个月后的实际数据:

  • 故障定位平均耗时:从45分钟→6分钟(下降了87%),这是根据SEP(服务事件复盘)记录统计的
  • 跨服务慢请求发现率:从完全依赖用户投诉→主动发现92%以上(通过Jaeger的后端监控「服务性能警告」)
  • ES存储:daily 61GB,30天保留期用ILM自动滚动索引,磁盘用掉了2.1TB
  • 季度成本:ES专用节点3台(8C16G),每月约0.15元/百万请求
  • P99总延迟影响:约2%,业务可以接受

我们通过Jaeger找出来的三类典型问题:

  • SQL N+1问题:某服务扫描6000个商品ID时每ID查一次数据库,链路图上出现了6000个DB调用子span,一眼就能看到
  • 错误重试风暴:某个下游服务返回500后,网关层重试3次,链路图上出现了4条重复的下游调用,并且耗时X4
  • 串行可并行调用:旧代码按顺序调用3个外部服务,链路图上3个span一字排开,改成并发后整体从900ms降到310ms

避坑指南:五个真实踩过的坑

坑1:采集端采样率设置不当,ES直接被打爆

// 错误的配置——把采样率配成了100%
{
  "service_name": "order-service",
  "sampler": {
    "type": "const",
    "param": 1
  }
}

上线第一天,我们按100%采样接入。结果到晚上ES集群CPU直接爆炸,磁盘迅速被占满。原因很简单:订单服务是全链路最核心的,日均请求量600万,每个请求生成6-8个span。一天就是4千万个span,每个span在ES里存储约1.5KB,照这个量级每天会生成60GB+数据。

补救方案:立即改为10%采样。但问题并没有完全解决,因为链路追踪的traceId会先由入口服务生成并向下游传递,如果在下游服务单独设置采样率,会导致同一个trace的不同span被部分保留。一定要用 parentbased_ratelimiting 这种「父采样决策一致性」的方案。

坑2:Swoole协程上下文丢失导致的错误链路

PHP的OpenTelemetry扩展在Swoole下的上下文传播有bug。直接使用传统的静态TracerProvider,在协程切换时span会串。必须做两件事:

// 必须设置协程模式,否则跨协程会串数据
\Swoole\Runtime::enableCoroutine();
$provider->setSpanProcessor(
    new SimpleSpanProcessor(
        $exporter,
        // 这里必须传第二参数,显式指定协程上下文处理器
        new OpenTelemetry\SDK\Trace\SpanProcessor\CooperativeSpanProcessor($exporter)
    )
);

这个场景排查了很久。Swoole的CooperativeSpanProcessor需要在项目里根据自己的业务模式改造,不做协程上下文隔离会导致A请求的span被B请求打印。最终我们的方案是写了一个基于Swoole的Coroutine Handler的中间件,在协程创建时复制span上下文。

坑3:Jaeger UI查不到数据,但上报日志显示成功

排查了一天发现是collector的 --collector.otlp.enabled=true 参数没有开启。Jaeger 1.57中,如果部署的是all-in-one模式,默认会开启OTLP接收;但通过Kubernetes Operator的production模式创建时,默认没有把4317/4318端口暴露出来,只暴露了14250(Jaeger专属协议)。

# 必须显式配置启用OTLP
spec:
  collector:
    options:
      collector:
        otlp:
          enabled: "true"
          http:
            enabled: "true"
            port: "4318"
          grpc:
            enabled: "true"
            port: "4317"

坑4:Zipkin的UI查询条件容易让人误判

Zipkin查询界面默认只显示最近1小时的数据,不设置时间范围很容易看到空的查询结果;另外当使用ES索引时,如果当天没有生成索引,查询会失败。ZIPKIN在启动时会检查ES索引是否存在,但如果ES里有历史索引别名冲突(比如之前的zipkin索引残留),Zipkin会静默处理,UI直接500。

坑5:Span上报失败重试导致的线程池耗尽

Zipkin的AsyncReporter默认队列大小是4096,当后端异常时,队列打满后报错会变成同步阻塞。严重情况下会拖垮业务线程——因为业务线程会一直等span上报。一定在接入时加一段降级逻辑:

// 设置超时和降级
tracing = Tracing.newBuilder()
        .spanReporter(AsyncReporter.builder(sender)
                .queuedMaxSpans(2048)     // 减少队列容量
                .messageTimeout(5, TimeUnit.SECONDS) // 5秒超时
                .build())
        .build();

这个配置一定要提前做。在线上遇到过ES短暂不可用,业务侧因为没有阻塞处理,导致整个支付服务卡死30秒,损失了好几个订单。

选型结论

如果你还在犹豫,我的建议很直接:

  • 服务数量少于10个、团队没有专门的可观测性平台团队、需要快速上线的场景:选Zipkin,它足够简单,部署5分钟搞定
  • 服务数量超过20个、已经用了Kubernetes、未来会引入更多CNCF生态组件的场景:直接上Jaeger,架构上更现代,和OpenTelemetry的集成度好太多
  • 两种情况都需要注意:不要用内存存储(Zipkin内置和Jaeger的Badger都不行),不要不做采样策略直接100%全量采集,不要只接入Java服务而忽略PHP/Go等异构服务——跨语言链路追踪的价值才是最大的

我们最终选择了Jaeger,在架构上解决了「一个trace在混合语言技术栈中完整串联」的问题,也拿到了可量化的故障定位效率提升。但坦白说,如果我们当时只有5个Java微服务、日均请求量在500万以下,Zipkin也能满足需求。