一、什么是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集成起来,能解决分布式系统里最头疼的排查问题的难题,整个流程其实不难,核心是配置好过滤器、解决序列化兼容的问题。常见的坑点主要集中在配置错误、调用链断了、性能开销大这几个方面,只要按照我们说的方法解决,就能顺利集成。
最后再提醒一下,集成的时候一定要先在测试环境验证,没问题再放到生产环境,避免影响线上业务。
评论
围绕“在Dubbo服务框架中集成Zipkin实现全链路追踪的详细步骤与常见坑点及序列化兼容性解决方案总结”参与讨论