一、什么是Dubbo集成Zipkin的全链路追踪

你有没有遇到过这种情况:一个电商下单的流程,前端点了下单没反应,查日志发现订单服务报错,再往上查是库存服务超时,再往上查是支付服务调库存时出了问题——但这些服务的日志散在不同服务器上,你得翻好几个日志文件,甚至还要找运维要权限,折腾半天才能理清楚整个调用链?

全链路追踪就是专门解决这个问题的,它能把一次请求经过的所有服务、每个服务花了多久、有没有报错,都串成一条完整的“调用链”,你一眼就能看到哪个环节出了问题。

而Dubbo是国内用得特别多的分布式服务框架,相当于把一个大系统拆成好多小服务(比如订单、库存、支付),让它们能互相调用;Zipkin是专门做全链路追踪的工具,能把这些调用链收集起来、整理好给你看。把这俩凑一块,就能轻松搞定分布式系统的调用链问题。

1.1 为什么要做集成?

举个实际的例子:你做的是一个外卖系统,用户点外卖时,请求会先到“订单服务”,订单服务会调“库存服务”扣食材,再调“支付服务”扣钱,再调“配送服务”派单。如果用户点完单页面卡住了,没有全链路追踪的话,你得分别查四个服务的日志,挨个找对应的请求ID,再拼起来看哪个环节慢;但集成了Zipkin之后,你只要在Zipkin的界面搜用户的请求ID,就能看到一条完整的链:订单服务花了20ms,库存服务花了500ms(这里超时了),支付服务没被调用,配送服务也没被调用——瞬间就知道是库存服务的问题,省了大把时间。

二、集成前的准备工作

在动手之前,你得先把几个基础的东西搞定,不然会踩很多坑。

2.1 准备Zipkin服务

Zipkin本身是一个独立的服务,需要先把它跑起来,它的作用是收集所有服务发过来的追踪数据,然后提供界面让你看。最简单的启动方式是用Docker,如果你电脑上装了Docker,直接跑下面的命令就行:

# 拉取最新的Zipkin镜像
docker pull openzipkin/zipkin
# 启动Zipkin服务,映射端口9411
docker run -d -p 9411:9411 openzipkin/zipkin

启动成功后,你打开浏览器访问http://localhost:9411,能看到Zipkin的界面,就说明Zipkin已经准备好了。

2.2 准备Dubbo项目

这里我们用Spring Boot + Dubbo的组合,因为这是目前最常用的搭配。我们会做两个简单的服务:一个是“订单服务”(作为消费者,会调用库存服务),一个是“库存服务”(作为提供者,被订单服务调用)。

先说明一下统一的技术栈:Spring Boot 2.7.14,Dubbo 3.2.15,Zipkin 2.24.1,JDK 11。

三、具体集成步骤

接下来我们一步步来集成,每个步骤都有完整的代码和注释。

3.1 库存服务(提供者)的集成

库存服务是被订单服务调用的,所以它要做的是:每次被调用时,生成追踪数据,发给Zipkin。

第一步,先在库存服务的pom.xml里加依赖:

<!-- Dubbo核心依赖 -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>3.2.15</version>
</dependency>
<!-- 注册中心,我们用Nacos,方便服务发现 -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-registry-nacos</artifactId>
    <version>3.2.15</version>
</dependency>
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>2.1.2</version>
</dependency>
<!-- Zipkin的Dubbo适配依赖,这个是核心 -->
<dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave-instrumentation-dubbo-rpc</artifactId>
    <version>5.14.1</version>
</dependency>
<!-- 把追踪数据发给Zipkin的依赖 -->
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
    <version>2.16.3</version>
</dependency>

第二步,配置库存服务的application.yml

spring:
  application:
    name: stock-service # 服务名,用来区分不同的服务
dubbo:
  registry:
    address: nacos://127.0.0.1:8848 # Nacos注册中心地址
  protocol:
    name: dubbo
    port: 20880 # Dubbo服务的端口
  provider:
    filter: brave # 核心配置:启用Brave的追踪过滤器,用来生成追踪数据
# Zipkin的地址,指定追踪数据发到哪里
zipkin:
  base-url: http://localhost:9411/api/v2/spans

第三步,写库存服务的核心代码,也就是对外提供的接口和实现:

// 先定义一个公共的接口,订单服务和库存服务都要用到这个接口
public interface StockService {
    // 扣库存的方法,参数是商品ID,返回扣减结果
    boolean deductStock(Long productId);
}

// 库存服务里的接口实现
@DubboService(version = "1.0.0") // 把这个服务暴露出去,版本号是1.0.0
public class StockServiceImpl implements StockService {
    @Override
    public boolean deductStock(Long productId) {
        // 模拟扣库存的业务逻辑,这里我们故意加个延时,方便后续看追踪数据
        try {
            Thread.sleep(100); // 模拟业务处理耗时100ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("扣减商品" + productId + "的库存成功");
        return true;
    }
}

3.2 订单服务(消费者)的集成

订单服务是调用库存服务的,所以它要做的是:每次调用库存服务时,把自己的追踪信息传给库存服务,让整个调用链连起来。

第一步,同样在订单服务的pom.xml里加依赖,和库存服务的依赖差不多,只是少了dubbo-spring-boot-starter里的提供者相关的配置,其他核心依赖一样:

<!-- 依赖和库存服务的核心依赖一致,这里就不重复写了,只加个特殊的: -->
<!-- 消费者的追踪过滤器 -->
<dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave-instrumentation-dubbo-rpc</artifactId>
    <version>5.14.1</version>
</dependency>

第二步,配置订单服务的application.yml

spring:
  application:
    name: order-service
dubbo:
  registry:
    address: nacos://127.0.0.1:8848
  consumer:
    filter: brave # 核心配置:消费者也启用Brave的追踪过滤器
    version: 1.0.0 # 调用的服务版本,和库存服务的版本一致
# Zipkin地址和库存服务的一样
zipkin:
  base-url: http://localhost:9411/api/v2/spans

第三步,写订单服务的核心代码:

// 引用库存服务的公共接口
public interface StockService {
    boolean deductStock(Long productId);
}

// 订单服务的控制器,用来接收前端的请求
@RestController
public class OrderController {
    // 引用库存服务
    @DubboReference
    private StockService stockService;

    // 模拟前端的下单请求,参数是商品ID
    @GetMapping("/createOrder")
    public String createOrder(Long productId) {
        // 模拟订单服务的业务逻辑,加个延时
        try {
            Thread.sleep(50); // 订单服务处理耗时50ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 调用库存服务扣库存
        boolean result = stockService.deductStock(productId);
        if (result) {
            return "下单成功";
        } else {
            return "下单失败";
        }
    }
}

3.3 测试集成效果

启动Nacos(注册中心,用来让Dubbo服务互相发现),然后启动库存服务和订单服务。

然后用浏览器访问订单服务的接口:http://localhost:8080/createOrder?productId=123,访问成功后,打开Zipkin的界面http://localhost:9411,点击“Find Traces”按钮,就能看到一条调用链。点击这条链,就能看到详细的信息:整个调用总耗时大概150ms,其中订单服务花了50ms,库存服务花了100ms——完美,整个调用链串起来了!

四、常见坑点及解决方案

集成的时候很容易踩坑,我把我遇到过的坑和解决方法整理出来,帮你少走弯路。

4.1 坑点一:Zipkin里看不到追踪数据

这是最常见的坑,大部分原因是配置错了。

第一个可能的原因:Dubbo的过滤器没配置。不管是提供者还是消费者,都要在application.yml里加dubbo.provider.filter: brave或者dubbo.consumer.filter: brave,如果漏了,服务就不会生成追踪数据,Zipkin自然看不到。

第二个可能的原因:Zipkin的地址配置错了。比如端口写错了(Zipkin默认是9411,不是8080),或者路径错了(必须是/api/v2/spans,不能少)。

第三个可能的原因:Nacos没启动或者地址错了。如果Dubbo服务连不上Nacos,服务就没法注册,调用会失败,自然也不会有追踪数据。

4.2 坑点二:调用链断了,看不到完整的链

有时候你能看到订单服务的追踪数据,也能看到库存服务的追踪数据,但它们是两条独立的链,没连起来。

这个坑的核心原因是:追踪数据里的“Trace ID”(用来标识整个调用链的唯一ID)没传过去。

为什么会传不过去?最常见的是序列化的问题。Dubbo在调用的时候,会把请求参数、还有追踪的上下文(里面有Trace ID)一起序列化,然后传给对方。如果序列化的方式不兼容,上下文就会丢,Trace ID就传不过去,链就断了。

比如,Dubbo默认用的是Hessian2序列化,而Brave的上下文是用Java的序列化方式,这俩不兼容,就会导致上下文丢失。

解决方法有两个: 第一个方法:统一序列化方式。把Dubbo的序列化改成和Brave兼容的,比如改成Java序列化。在application.yml里加:

dubbo:
  protocol:
    serialization: java # 改成Java序列化

不过这个方法有个缺点:Java序列化的性能比Hessian2差,适合测试环境,生产环境不推荐。

第二个方法:自定义Brave的序列化适配。这个方法是生产环境常用的,原理是把Brave的上下文转换成Dubbo能识别的格式,比如转换成字符串,再传过去。具体怎么做呢?

首先,写一个自定义的Brave上下文提取器,用来把Dubbo的请求里的上下文提取出来:

public class DubboRequestExtractor implements RequestExtractor<Invocation> {
    @Override
    public TraceContextOrSamplingFlags extract(Invocation invocation) {
        // 从Dubbo的请求里拿上下文
        Map<String, String> context = invocation.getAttachments();
        if (context == null || !context.containsKey("traceId")) {
            return TraceContextOrSamplingFlags.EMPTY;
        }
        // 把拿到的上下文转换成Brave能识别的格式
        return TraceContextOrSamplingFlags.create(
                TraceContext.newBuilder()
                        .traceId(Long.parseLong(context.get("traceId")))
                        .spanId(Long.parseLong(context.get("spanId")))
                        .build()
        );
    }
}

然后,写一个自定义的Brave上下文注入器,用来把Brave的上下文注入到Dubbo的请求里:

public class DubboRequestInjector implements RequestInjector<Invocation> {
    @Override
    public void inject(TraceContext context, Invocation invocation) {
        // 把Trace ID和Span ID放到Dubbo的请求上下文里
        invocation.getAttachments().put("traceId", String.valueOf(context.traceId()));
        invocation.getAttachments().put("spanId", String.valueOf(context.spanId()));
    }
}

最后,把这两个自定义的类配置到Brave里,替换掉原来的:

@Configuration
public class BraveConfig {
    @Bean
    public Brave brave(Tracing tracing) {
        return Brave.newBuilder(tracing)
                .build();
    }

    @Bean
    public Tracing tracing(ZipkinSender sender) {
        return Tracing.newBuilder()
                .localServiceName("dubbo-service")
                .propagationFactory(Propagation.newFactory(
                        B3Propagation.FACTORY,
                        new DubboRequestExtractor(),
                        new DubboRequestInjector()
                ))
                .spanReporter(AsyncReporter.create(sender))
                .build();
    }

    @Bean
    public ZipkinSender sender() {
        return OkHttpSender.create("http://localhost:9411/api/v2/spans");
    }
}

这样配置之后,Trace ID就能正常传过去了,调用链就不会断了。

4.3 坑点三:追踪数据太大,影响性能

生产环境里,请求量很大,每个请求都要生成追踪数据,还要发给Zipkin,很容易占用太多网络和内存资源,影响服务的性能。

解决方法: 第一个方法:采样。不是每个请求都生成追踪数据,只对一部分请求采样。比如配置成10%的采样率,就是每10个请求里,只有1个会生成追踪数据。配置方法是在application.yml里加:

zipkin:
  sampling:
    probability: 0.1 # 采样率,0到1之间,0.1就是10%

第二个方法:批量发送。把多个追踪数据攒到一起,再发给Zipkin,减少网络请求的次数。这个Brave已经默认做了,你只要调整批量发送的参数就行,比如:

// 在Brave的配置里加批量发送的配置
@Bean
public AsyncReporter<Span> spanReporter(ZipkinSender sender) {
    return AsyncReporter.builder(sender)
            .queueSize(1000) // 队列大小,最多攒1000个追踪数据
            .messageTimeout(1000) // 最多等1000ms,攒不够1000个也发
            .build();
}

五、应用场景、优缺点及注意事项

5.1 应用场景

全链路追踪适合所有分布式系统的场景,尤其是这些情况:

  • 系统拆成了多个服务,排查问题困难的场景;
  • 电商、金融、外卖等对响应时间要求高,需要优化性能的场景;
  • 多团队协作开发,每个团队负责一个服务,需要快速定位跨团队的问题的场景。

5.2 技术优缺点

优点:

  • 快速定位问题:不用再翻多个日志,一眼就能看到哪个环节出了问题;
  • 性能优化:能看到每个服务的耗时,找到性能瓶颈;
  • 可视化:Zipkin的界面很直观,不用写复杂的查询语句。

缺点:

  • 有性能开销:生成和发送追踪数据会占用少量的CPU和网络资源;
  • 依赖第三方服务:Zipkin本身是一个独立的服务,如果Zipkin挂了,追踪数据就会丢;
  • 配置复杂:尤其是序列化兼容的问题,配置不好很容易踩坑。

5.3 注意事项

  • 生产环境一定要配置采样率,不然请求量大会压垮Zipkin;
  • 一定要解决序列化兼容的问题,不然调用链会断;
  • 要监控Zipkin的状态,保证Zipkin是正常运行的;
  • 追踪数据要定期清理,不然Zipkin的存储会满。

六、总结

把Dubbo和Zipkin集成起来,能解决分布式系统里最头疼的排查问题的难题,整个流程其实不难,核心是配置好过滤器、解决序列化兼容的问题。常见的坑点主要集中在配置错误、调用链断了、性能开销大这几个方面,只要按照我们说的方法解决,就能顺利集成。

最后再提醒一下,集成的时候一定要先在测试环境验证,没问题再放到生产环境,避免影响线上业务。