前言
分布式链路追踪中,记录数据库的调用是必不可少的,但是数据库的分布式链路追踪,与调用下游服务或者发送Kafka有着显著的不同,那就是链路信息不需要传递到数据库服务端,所以就不需要将Span通过某种方式进行传递,而需要做的,就是把请求数据库服务端时的一些信息记录下来并作为链路日志输出。
这里选择基于MyBatis实现数据库链路追踪,实现的机制是基于MyBatis的拦截器,因此实际上MyBatis-Plus也是适用的。
github地址:honey-tracing
正文
一. 链路日志改造说明
之前的链路日志格式如下所示。
1 | json复制代码{ |
其中requestStacks字段用于记录下游的Span信息,而由于数据库链路追踪中并不需要将链路信息传递给数据库服务端,所以requestStacks字段不再适用,我们新增加一个dbStacks字段来记录数据库操作的信息,示例如下。
1 | json复制代码{ |
新增字段说明如下。
- dbServer。表示数据库服务端地址,从url连接串中解析出;
- dbName。表示操作的数据库名,从url连接串中解析出;
- sqlText。表示执行的SQL语句信息,从MyBatis的BoundSql中获取;
- sqlParams。表示执行的SQL参数,同样从MyBatis的BoundSql中获取;
- sqlDuration。表示操作数据库的耗时,单位ms,由于MyBatis拦截器的拦截时机是先于从数据源中拿出连接的,所以这里的耗时包括等待获取数据库连接的时间;
- sqlTimestamp。表示开始操作数据库的时间点的毫秒时间戳。
二. MyBatis拦截器回顾
MyBatis的拦截器也就是常说的插件,可以作用于Executor,ParameterHandler,ResultSetHandler和StatementHandler这四个组件,所有拦截器都需要实现org.apache.ibatis.plugin.Interceptor接口,一个简单的拦截器示例如下所示。
1 | java复制代码@Intercepts({ |
上面出现的@Signature注解的type,method和args三个字段共同决定拦截器会作用于哪个组件的哪个方法上,例如上面示例中,就会作用于Executor组件的update() 方法和两个重载的query() 方法,而我们又知道,MyBatis执行SQL时,无论是增删改查,其实都是会调用到Executor的update() 方法或者query() 方法,所以上面示例的拦截器,其实就可以拦截所有SQL的执行。
三. 数据库链路追踪MyBatis拦截器设计与实现
在开始前,需要在pom文件中先添加如下依赖。
1 | xml复制代码<dependency> |
我们要实现数据库链路追踪,其实就是记录操作数据库的行为,所以需要拦截每一条SQL的执行,因此拦截器作用的目标组件就是Executor,作用的目标方法就是update() 和两个重载的query() 方法,拦截器实现如下所示。
1 | java复制代码/** |
上述实现的拦截器,主要干了下面几件事情。
- 针对当前操作数据库的行为创建Span。这里创建Span并不是要把Span传递给数据库服务端,而是通过Span来记录开始时间,执行耗时以及数据库操作的一些信息;
- 在SQL执行前,执行成功和执行失败时分别应用装饰器的逻辑。拦截器并没有写很重的逻辑,解析url,解析SQL等逻辑全部放到装饰器中,让拦截器和记录信息的行为解耦,方便后续扩充dbStack的内容;
- 在拦截器的最后生成dbStack并记录在当前节点的Span中。注意,生成dbStack是基于我们在拦截器中创建出来的Span,在dbStack生成出来后,在拦截器中创建出来的Span的使命就完成了,后续就需要把生成出来的dbStack记录在当前节点的Span中。
基于Span生成dbStack的工具类DbStackUtil实现如下。
1 | java复制代码/** |
1 | java复制代码public class CommonConstants { |
其中很关键的一点是在于将logEventKind设置为了dbStack,这样在打印链路日志时,可以根据logEventKind来知道当前要按照dbStack的格式来组装日志。
最后再看一下装饰器接口的定义,如下所示。
1 | java复制代码/** |
四. 数据库链路追踪装饰器设计与实现
提供一个HoneyDbExecutorTracingDecorator接口的实现类,在SQL执行前,完成记录数据库服务端地址,数据库名,SQL语句和SQL参数,这些记录的信息,全部存储在Span的Tags字段中。
1. 数据库服务端地址和数据库名获取
装饰器实现如下。
1 | java复制代码/** |
上述装饰器首先会从拦截方法的参数中拿到MappedStatement,从而最终可以拿到当前使用的数据源DataSource,再然后判断数据源的类型,如果能够明确数据源的类型,那么就可以直接拿到数据库连接串url,如果无法判断出数据源类型,则可以选择先从数据源中获取一个数据库连接,然后再从数据库连接的元数据信息中拿到url。
获取到url后,就按照如下两种url格式来解析出数据库服务端地址和数据库名。
1 | txt复制代码jdbc:mysql://数据库服务端地址/数据库名 |
1 | txt复制代码jdbc:mysql://数据库服务端地址/数据库名?配置项1=配置值1 |
2. SQL语句和SQL参数获取
继续在上一小节的装饰器中添加代码,完成SQL语句和SQL参数的获取,如下所示。
1 | java复制代码/** |
先从拦截的方法的参数中拿到MappedStatement,然后再从MappedStatement中拿到BoundSql,我们需要的SQL语句和SQL参数,都在BoundSql中,其中SQL语句的获取比较简单,直接通过BoundSql就可以拿到SQL语句,我们需要做的就是把多余的空格和换行符给去掉,让SQL看起来好看一些。而参数要稍微麻烦一点,因为参数是不确定的,在BoundSql中是这样来表示参数的。
1 | java复制代码public class BoundSql { |
实际就是要使用parameterMappings和parameterObject共同来解析出SQL参数,这里的解析逻辑,直接参考的DefaultParameterHandler中的代码。
至此数据库链路追踪装饰器就实现完毕了,使用到的一些常量如下所示。
1 | java复制代码public class CommonConstants { |
五. 注册MyBatis拦截器
在MyBatis中注册拦截器,其实就是拿到MyBatis的Configuration后,调用其addInterceptor() 方法即可,所以有两种实现思路。
- 提供ConfigurationCustomizer并在其customize() 方法中添加拦截器。ConfigurationCustomizer是mybatis-spring-boot-starter中提供出来专门用于定制化Configuration的,所以如果有使用mybatis-spring-boot-starter,那么可以基于ConfigurationCustomizer来添加拦截器到Configuration中;
- 自定义BeanPostProcessor并处理所有SqlSessionFactory。因为MyBatis整合到Spring中后,很核心的一点就是SqlSessionFactory会作为bean被注册到Spring容器中,所以可以提供一个BeanPostProcessor来处理所有的SqlSessionFactory,通过SqlSessionFactory拿到其持有的Configuration,然后调用addInterceptor() 方法添加拦截器。
注意,在使用了mybatis-spring-boot-starter后,其实我们只需要将拦截器注册到Spring容器中即可,mybatis-spring-boot-starter提供的MybatisAutoConfiguration会获取到所有Spring容器中的拦截器,然后在构建SqlSessionFactory时会把拦截器都添加到Configuration中,所以此时我们再提供一个ConfigurationCustomizer,实际是会重复添加拦截器的,因此下面通过自定义BeanPostProcessor的方式来注册拦截器。
首先自定义一个BeanPostProcessor,如下所示。
1 | java复制代码public class SqlSessionFactoryBeanPostProcessor implements BeanPostProcessor { |
然后提供一个自动装配类HoneyDbTracingConfig,如下所示。
1 | java复制代码@Configuration |
最后在spring.factories文件中添加上述自动装配类,如下所示。
1 | yaml复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
六. 链路日志打印
现在还需要在原有链路日志打印的基础上,把dbStack添加进去。
定义HoneyDbStack表示链路日志中的dbStacks字段,如下所示。
1 | java复制代码public class HoneyDbStack { |
然后在HoneySpanReportEntity中做如下修改。
1 | java复制代码public class HoneySpanReportEntity { |
如此,在打印链路日志时,就会带上dbStack了。
七. 演示案例
改造example-service-1,进行数据库链路追踪的测试。
首先pom文件添加如下依赖。
1 | xml复制代码<dependency> |
由于需要把映射文件打进jar包,pom文件还需要添加如下构建步骤。
1 | xml复制代码<build> |
然后使用如下DDL语句在MySQL数据库中创建一张表。
1 | sql复制代码CREATE TABLE `people` ( |
这张表的映射接口,映射文件和对应实体对象如下所示。
1 | java复制代码public interface PeopleMapper { |
1 | xml复制代码<?xml version="1.0" encoding="UTF-8" ?> |
1 | java复制代码public class People { |
然后在配置文件中加入数据库相关配置。
1 | yaml复制代码spring: |
由于并没有通过mybatis.mapper-locations来指定映射文件位置,所以我们需要在启动类上添加@MapperScan注解来扫描得到映射接口和映射文件,如下所示。
1 | java复制代码@MapperScan |
最后提供一个MyBatisController来查询数据库,如下所示。
1 | java复制代码@RestController |
启动example-service-1,调用如下接口。
1 | txt复制代码http://localhost:8080/mybatis/select?peopleName=Lee&peopleAge=20 |
链路日志打印如下。
1 | json复制代码{ |
总结
本文基于MyBatis拦截器,实现了数据库链路追踪,核心思路就是通过拦截器,拿到本次SQL执行的相关信息,并输出到链路信息中。
本文转载自: 掘金