自己写dubbo链路追踪工具包-实现dubbo调用中传递打印TraceId[开发及原理篇]
在近期的两篇文章中,我带领大家使用springboot2.x整合了dubbo,完成了一个基础的服务化开发框架的搭建。文章链接如下
[dubbo]springboot2.x整合dubbo之使用dubbo-spring-boot-starter
[dubbo]springboot2.x整合dubbo之基于xml整合dubbo2.6.5
但是在生产环境下还是存在一定的问题,比如我们关心的调用链追踪的问题。
本文中,我将继续带领大家基于dubbo2.6.5开发一个轻量级的TraceId获取及传递工具包,实现在dubbo服务的提供方及消费方中 设置/获取/传递 TraceId的需求。
需求描述
通过dubbo的Filter及扩展点机制,开发TraceId获取及传递的工具包,实现在服务业务的上下游中对TraceId的 设置/获取/传递 的需求。
原理讲解
在编写代码之前,先对Filter机制和扩展点机制做一个简要的讲解,以便加深我们的理解,当然如果只是简单的整合使用该工具包,可以直接跳过这段,进入编码环节。
Filter机制简述
Dubbo中的Filter与我们在javaweb中的Filter的理解是类似的,都是在请求处理前后做一些通用的逻辑,Filter可以有多个支持层层嵌套。
Dubbo官方针对Filter做了很多的原生支持,目前大致有20多个,包括我们熟知的RpcContext,accesslog等功能都是通过filter来实现的。
开发一个自定的Filter很简单,只需要编写我们的Filter类,实现com.alibaba.dubbo.rpc.Filter 接口,重写其invoke(Invoker<?> invoker, Invocation invocation) 方法即可。
具体的过程我将在下文展开讲解。
SPI机制简述
这里直接引用官网的描述:
SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。
Filter机制便是Dubbo中SPI的一个应用场景,Dubbo对java的原生SPI做了扩展,使Dubbo具备了更丰富的扩展性。
Dubbo官方对Dubbo的定位是微内核架构,不想依赖三方框架如Spring、Guice等,这使得Dubbo的实现更加的优雅。
通常微核心都会采用 Factory、IoC、OSGi 等方式管理插件生命周期。考虑 Dubbo 的适用面,不想强依赖 Spring 等 IoC 容器。自已造一个小的 IoC 容器,也觉得有点过度设计,所以打算采用最简单的 Factory 方式管理插件。–引用自《扩展点重构》
具体的细节可以参考文章最后的参考链接。
接下来就进入到我们的正式的设计编码环节。
编码实现
首先建立一个maven工程,我将其命名为shield-dubbo-tracer。
依赖引入
在项目的pom.xml中引入必要的依赖,如下
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba/dubbo -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.5</version>
<scope>provided </scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.22</version>
</dependency>
</dependencies>
由于我们的工具包是基于dubbo运行的,因此要添加dubbo依赖,这里使用provided的作用域的原因是:我们不确定工具包的调用方具体使用哪个版本的dubbo,因此在打包的时候不需要将dubbo依赖都打到最终的jar中,保证我们工程的纯洁性和轻量性,同时也能很好的避免调用方出现依赖冲突的可能。
编写TraceId生成类–TraceIdGenerator
首先定义一个名为TraceIdGenerator的类,该类主要用于生成TraceId,且类的作用域是public,可供外部直接调用,核心代码如下
public class TraceIdGenerator {
/**
* 消费端创建TraceId,并设置到线程上下文中
* 该方法只调用一次
* @return
*/
public static String createTraceId() {
// 创建的同时就设置到上下文中
String traceId = getTraceid();
TraceIdUtil.setTraceId(traceId);
return traceId;
}
/**
* 生成32位traceId
* @return
*/
private static String getTraceid() {
String result = "";
String ip = "";
// 获取本地ipv4地址
try {
InetAddress address = InetAddress.getLocalHost();
ip = address.getHostAddress();
} catch (Exception var5) {
return result;
}
// 根据.截取为String数组
String[] ipAddressInArray = ip.split("\\.");
// 拼装为字符串,将每一个元素转换为16进制
for(int i = 3; i >= 0; --i) {
Integer id = Integer.parseInt(ipAddressInArray[3 - i]);
result = result + String.format("%02x", id);
}
// 拼装时间戳及随机数
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");
result = result + simpleDateFormat.format(new Date()) + UuidUtil.getUuid().substring(0, 7);
return result;
}
}
注释写的应当是比较详细的,这里总结一下,createTraceId()方法是提供给外部完成TraceId生成功能的。它内部调用私有静态方法getTraceid()生成32的一串TraceId的同时将该TraceId设置到本地线程的上下文TraceIdUtil中,并将该TraceId返回供后续业务使用。
编写线程上下文TraceIdUtil
上面提到,生成TraceId的同时设置到了TraceIdUtil中,TraceIdUtil可以认为是当前线程/当前线程子线程的TraceId的容器,具体代码如下
......
/**使用InheritableThreadLocal便于在主子线程间传递参数*/
private static final ThreadLocal<String> TRACE_ID = new InheritableThreadLocal<>();
public TraceIdUtil() {
}
/**
* 从当前线程局部变量获取TraceId
* 首次调用该方法会生成traceId,后续每次都从线程上下文获取
* @return
*/
public static String getTraceId() {
return TRACE_ID.get();
}
public static void setTraceId(String traceId) {
TRACE_ID.set(traceId);
}
public static void removeTraceId() {
TRACE_ID.remove();
}
......
有心的你可能发现了,我在这里使用了InheritableThreadLocal类作为TraceId保存的容器,之所以使用InheritableThreadLocal是要达到在当前线程的子线程中也能获取到TraceId的值,如果使用它的父类ThreadLocal则只能在当前线程获取到TraceId。关于InheritableThreadLocal的详细的介绍请自行参看文章末尾的参考链接中的《InheritableThreadLocal详解》一文,这里不再赘述。
通过TraceIdUtil,我们能够实现
- 将TraceId设置到当前线程的线程上下文中
- 从当前线程上下文中获取TraceId
- 从当前线程上下文中移除TraceId等的功能。
编写自定义的TraceIdFilter实现com.alibaba.dubbo.rpc.Filter接口
完成前面的准备工作,我们即将进行工具包开发的重头戏,实现自定义的TraceId传递Filter。
编写类TraceIdFilter,实现接口com.alibaba.dubbo.rpc.Filter,重写其invoke方法。
核心代码如下:
@Activate(group = {Constants.PROVIDER, Constants.CONSUMER})
public class TraceIdFilter implements Filter {
......
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
RpcContext rpcContext = RpcContext.getContext();
String traceId = "";
if (rpcContext.isConsumerSide()) {
if (StringUtils.isBlank(TraceIdUtil.getTraceId())) {
// 根调用,生成TraceId
traceId = TraceIdGenerator.createTraceId();
} else {
// 后续调用,从Rpc上下文取出并设置到线程上下文
traceId = TraceIdUtil.getTraceId();
}
TraceIdUtil.setTraceId(traceId);
RpcContext.getContext().setAttachment(TraceIdConst.TRACE_ID, TraceIdUtil.getTraceId());
}
if (rpcContext.isProviderSide()) {
// 服务提供方,从Rpc上下文获取traceId
traceId = RpcContext.getContext().getAttachment(TraceIdConst.TRACE_ID);
TraceIdUtil.setTraceId(traceId);
}
Result result = invoker.invoke(invocation);
return result;
}
......
}
这里简单先接单介绍下 @Activate 注解,该注解表示一个扩展是否被激活(即是否能够使用), 它可以放在类定义、方法之上。dubbo用它在spi扩展类定义上,表示这个扩展实现激活条件和时机。
这里我将TraceIdFilter标注为
@Activate(group = {Constants.PROVIDER, Constants.CONSUMER})
标识该拓展会在服务提供方和服务消费方同时生效。
接下来,我们详细的梳理一下重写的invoke方法的逻辑。
ConsumerSide消费者侧核心逻辑
首先获取到RpcContext上下文,判断当前调用者属于服务提供方还是服务消费方。
如果是服务消费方,则从线程上下文TraceIdUtil中获取TraceId判断其是否为空。即如下的判断逻辑
if (StringUtils.isBlank(TraceIdUtil.getTraceId())) {
如果判断返回true,表明当前是消费侧的根调用,且消费侧没有显式的生成TraceId,则我们的组件生成一个TraceId设置到当前线程的上下文,并将其通过RpcContext隐式传参的方式传递给后续的服务。代码为
TraceIdUtil.setTraceId(traceId);
RpcContext.getContext().setAttachment(TraceIdConst.TRACE_ID, TraceIdUtil.getTraceId());
如果线程上下文TraceIdUtil存在TraceId,表明当前消费端已经存在TraceId。
因为存在TraceId,则我们直接从线程上下文中取出该TraceId,并通过RpcContext隐式传参方式传递给后续的服务,核心代码为
......
traceId = TraceIdUtil.getTraceId();
......
TraceIdUtil.setTraceId(traceId);
RpcContext.getContext().setAttachment(TraceIdConst.TRACE_ID, TraceIdUtil.getTraceId());
ProviderSide提供者侧核心逻辑
提供者侧的逻辑就比较容易了,提供者的角色主要就是对TraceId的读取。
核心代码如下:
if (rpcContext.isProviderSide()) {
// 服务提供方,从Rpc上下文获取traceId
traceId = RpcContext.getContext().getAttachment(TraceIdConst.TRACE_ID);
TraceIdUtil.setTraceId(traceId);
}
可能这里你有疑问了,如果一个服务既是服务的提供者,又是消费者呢?该如何处理,其实在上文已经讲到了,如果它是提供方,则只需要从RpcContext上下文中读取TraceId进行使用即可。消费方的角色会在上述的消费者侧的逻辑处理,即读取TraceId,设置到当前线程,通过RpcContext隐式传递。
到这里,我们就完成了TraceFilter的主要逻辑,通过
Result result = invoker.invoke(invocation);
return result;
使业务逻辑继续往下走即可。
编写配置META-INF/dubbo/com.alibaba.dubbo.rpc.Filter
最后一步是dubbo的SPI规范要求的,不能更改。
在资源文件夹下创建 META-INF/dubbo 文件夹,在其中创建名为com.alibaba.dubbo.rpc.Filter 的文件,并编辑文件内容,在其中配置我们自定义的TraceIdFilter
traceIdFilter=com.snowalker.shield.dubbo.tracer.TraceIdFilter
具体的文件包路径如下
src
|-main
|-java
|-com
|-xxx
|-XxxFilter.java (实现Filter接口)
|-resources
|-META-INF
|-dubbo
|-org.apache.dubbo.rpc.Filter
(纯文本文件,内容为:xxx=com.xxx.XxxFilter)
要严格按照该路径进行开发和配置。
打包上传私服
到这里我们便可以打包该工具包并上传至私服,在业务中使用了。后续我会将完整的代码整理并上传至我的github仓库,以便读者自行参考打包使用。
mvn clean deploy -Dmaven.test.skip=true
小结
本文主要从开发及原理的角度,带领大家基于dubbo2.6.5实现了TraceId 设置/获取/传递 的需求,具有较高的实战价值。在下一篇中,我会以实际的案例展示如何在项目中使用该工具包,欢迎阅读。
参考链接
InheritableThreadLocal——父线程传递本地变量到子线程的解决方式及分析
dubbo系列七、dubbo @Activate 注解使用和实现解析
springboot整合dubbo报错No such extension traceIdFilter for filter/com.alibaba.dubbo.rpc.Filter
补充知识点:Maven中-DskipTests和-Dmaven.test.skip=true的区别
在使用mv编译、打包时为了跳过测试,会使用参数-DskipTests和-Dmaven.test.skip=true,这两个参数的主要区别是:
-DskipTests,不执行测试用例,但编译测试用例类生成相应的class文件至target/test-classes下。
-Dmaven.test.skip=true,不执行测试用例也不编译测试用例类。