文章篇幅有点长,先一句话总结一下:
在Spring启动过程中,自动注入链路配置;并且利用SpringBoot的EnableAutoConfiguration机制,实现了开发无感知、无代码侵入的zipkin链路跟踪框架
背景
基于zipkin打造的链路跟踪系统上线之后收到了开发同学的好评,尤其是定位跨系统的调用问题时非常方便,效率提升许多。但也有美中不足的地方:第一个版本接入方式是基本上是按照zipkin官方demo进行接入的。这就导致很多地方都需要开发同学进行自行配置、感知到框架底层的东西:如链路上报、采集相关配置以及各种组件需要手动开启,无法做到代码配置无侵入。
具有侵入性且繁杂的配置项会造成很多问题:
- 降低开发同学接入的积极性;
- 不同环境的server配置不一样,每个环境都需要修改,提高了使用成本。
于是下一步的工作重点放到了怎么做才能让开发同学降低接入和使用成本,将链路跟踪打造成基础设施,让上层应用系统接入无感知?
因为zipkin底层技术架构的原因,很难实现skywalking
的模式:将链路跟踪能力下沉到应用运行时环境。 某次阅读师兄代码时,发现师兄使用了BeanPostProcessor这个接口去对spring 容器中的bean进行二次加工处理。突然想到:能否利用BeanPostProcessor在Spring启动的过程中对链路配置进行处理从而实现对中间件的链路跟踪?
银弹:BeanPostProcessor
先看看BeanPostPorcessor
作用的时间点:
再来看看BeanPostProcessor
接口的方法:只有两个方法,分别会在bean初始化之前和之后进行调用
public interface BeanPostProcessor {
/**
* Apply this BeanPostProcessor to the given new bean instance <i>before</i> any bean
* initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
* or a custom init-method). The bean will already be populated with property values.
*/
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
/**
* Apply this BeanPostProcessor to the given new bean instance <i>after</i> any bean
* initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
* or a custom init-method). The bean will already be populated with property values.
*/
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
想要在Spring启动过程中启动对中间件的链路跟踪,那么就需要将对应的中间件的配置对象交由Spring进行管理,换句话说就是需要注入到Spring容器中成为一个bean。现在来梳理一下需要追踪的中间件:
- mysql
- dubbo
- incoming http:即对外提供的http接口
- outcoming http:OkHttp、HttpClient、RestTemplate
- 线程池:Executor、ExecutorService、@Scheduled注解
其中mysql、dubbo、incoming http、restTemplate这几个中间件的正常使用方式默认会注入到Spring容器中,处理起来还算方便。
主要的适配工作在HttpClient,有机会单独写一篇文章聊一聊我的适配实现思路。
具体实现
具体的实现逻辑
在Spring启动过程中注入链路跟踪相关配置
继承BeanPostProcessor
接口实现处理链路跟踪配置的ZipkinBeanPostProcessor
:在ZipkinBeanPostProcessor中的postProcessBeforeInitialization或者postProcessBeforeInitialization利用instanceof
关键字对需要进行链路跟踪的中间件配置实例对象进行过滤并进行加工处理;
利用SpringBoot的EnableAutoConfiguration机制,实现无感接入
设置ZipkinConfiguration对properties文件监听以及扫描并注入ZipkinBeanPostProcessor。如何制作spring-boot-starter请参见附录:SpringBoot 创建自己的 Starter
打好zipkin-spring-boot-starter
jar包以后,SpringBoot应用只需要引入这个starter jar包就可以绝大部分链路进行跟踪了。实现了代码配置无侵入、接入零成本的目标。
demo1:跟踪dubbo链路
/**
* dubbo链路配置
*
* @author liumian
*/
@Component
public class DubboConfigProcessor extends AbstractZipkinPostProcessor {
@Resource
private ZipkinTracingProperties properties;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (!properties.enable.getDubbo()) {
return bean;
}
if (bean instanceof ProviderConfig) {
ProviderConfig config = (ProviderConfig)bean;
return addProviderTracingFilter(config);
} else if (bean instanceof ConsumerConfig) {
ConsumerConfig consumerConfig = (ConsumerConfig)bean;
return addConsumerTracingFilter(consumerConfig);
} else {
return bean;
}
}
private final String tracingFilterName = "tracing";
private Object addProviderTracingFilter(ProviderConfig config) {
String filter = config.getFilter();
if (StringUtils.isNotBlank(filter)) {
if (!StringUtils.contains(filter, tracingFilterName)) {
String tracedFilter = StringUtils.join(tracingFilterName, ",", filter);
config.setFilter(tracedFilter);
}
} else {
config.setFilter(tracingFilterName);
}
return config;
}
private Object addConsumerTracingFilter(ConsumerConfig config) {
String filter = config.getFilter();
if (StringUtils.isNotBlank(filter)) {
if (!StringUtils.contains(filter, tracingFilterName)) {
String tracedFilter = StringUtils.join(tracingFilterName, ",", filter);
config.setFilter(tracedFilter);
}
} else {
config.setFilter(tracingFilterName);
}
return config;
}
}
对于跟踪dubbo链路主要提3点:
- 跟踪dubbo链路的核心逻辑是在filter配置中加上
tracingFilter
(zipkin官方实现的dubbo链路跟踪filter),所以只要拿到ProviderConfig和ConsumerConfig实例后并为其加上tracingFilter即可; - 为什么没有使用ProviderConfig和ConsumerConfig的父类
AbstractInterfaceConfig
,将添加tracing filter配置的两个方法合为一个? 答案是为了兼容2.7.x版本的dubbo,在2.7.x版本中AbstractInterfaceConfig
的包路径已经变成org.apache.dubbo
; - 为什么要使用
postProcessBeforeInitialization
而不是postProcessAfterInitialization
? 因为必须在dubbo客户端注册到注册中心之前将tracingFilter配置到filter链路中。
demo2:跟踪线程池链路
/**
* spring 线程池链路配置
*
* @author liumian
*/
@Component
public class ExecutorConfigProcessor extends AbstractZipkinBeanPostProcessor {
@Autowired
private Tracing tracing;
@Autowired
private GodeyeTracingProperties properties;
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (properties.enable.geteExecutor() && bean instanceof Executor) {
return decorateExecutor((Executor)bean);
} else {
return bean;
}
}
private Executor decorateExecutor(Executor delegate) {
class TracedExecutor implements Executor {
@Override
public void execute(Runnable command) {
String name = Thread.currentThread().getName();
TraceUtil.newChildTrace(tracing, name, span -> {
delegate.execute(tracing.currentTraceContext().wrap(command));
return null;
});
}
}
return new TracedExecutor();
}
}
线程池的链路跟踪本质上是将线程池内运行的每个task与线程池外部的链路上下文衔接起来,比如异步处理http请求等。这里除了支持Executor
线程池外,还支持ExecutorService
、@Scheduled
等常用的线程池和注解。为了降低代码的篇幅,这里只列举Executor
,另外两种线程池链路跟踪方式也是类似的。
为什么要使用postProcessAfterInitialization
而不是postProcessBeforeInitialization
? 因为接口被抹去的
原因,所以等bean初始化之后再进行包装;下一个章节会讨论这个问题。
遗留的问题
接口被“抹去”
从demo2:跟踪线程池链路
这个例子中可以了解到对线程池进行链路跟踪的步骤:
- 需要将实例对象A强制转换成
Executor
接口的实例B; - 对上一步强制转换的
Executor
B实例进行包装。
这会造成一个问题,如果A不仅仅实现了Executor
接口还实现了其他接口,那么其他的接口信息就会丢失。如果某些框架依赖这些接口类型去获取bean实例,那么就会造成奇怪的问题。
解法:使用cglib
等动态代理库进行方法增强而不是进行强制转换的方式进行包装
- SpringBoot 创建自己的 Starter:https://www.jianshu.com/p/18d57a99d359