之前的几篇文章已经详细介绍了Opentracing产生的背景以及相关概念等,今天我们来使用Opentracing实战一把。
Opentracing是个啥
如果看过之前的文章的同学并且已经理解了Opentracing的同学可以忽略本小节。这一小节是给其他入门的人做一个简单的介绍,做到快速入门。
我们首先来看看官方的定义
Vendor-neutral APIs and instrumentation for distributed tracing
中文翻译过来就是:为分布式追踪提供的厂商无关的api和工具
厂商无关怎么理解?其实就是你可以任意添加(或更换)追踪系统的实现。说白了就是保证你有后悔药可以吃,用了一种追踪系统之后感觉不爽以后可以很爽快地换掉。
如果你对于Opentracing还不了解的话,那我建议你可以阅读我之前的分布式链路追踪系列文章。如果实在是不想看的话,那我简单类比一下你肯定就懂了。
Opentracing在分布式链路追踪领域中的作用就类似于JDBC在数据库访问中的作用一样,都是让使用者在面向接口编程,而不用管下层的实现,并且可以在几乎不修改代码的提前下非常简单地切换底层的实现。
如下是两种标准在各自领域的作用和所处的层次。
Opentracing
JDBC
啥?你还是不懂,那好吧,直接上代码吧。
我们使用opentacing来埋个点
// tracer可以替换为任意的追踪系统的实现,如Jaeger Tracer,Zipkin Tracer等,
// tracer 提供的api就是厂商无关的api,tracer可任意替换
Tracer tracer = new MockTracer();
MockSpan span = tracer.buildSpan("foo").start();
span.setTag(Tags.COMPONENT, "my-own-application");
try{
doSomething();
}finally{
span.finish();
}
埋点是个啥
通俗点来讲,所谓埋点就是在一段代码的前后加上另外一段代码。那为啥要在一段代码上加上另外一段代码呢?无非就是想陌陌地干点"坏事"。
那到底可能会干些啥坏事呢,比如监控某种业务的执行时间,执行次数,或者打印一些日志啥的。用Java里面的术语来讲就是面向切面编程。
如下就是一段非常简单的埋点代码,用来记录方法的执行时间的。
long start = System.currentTimeMillis();
try{
doSomething();
}finally{
log.info("time cost :"+(System.currentTimeMillis()-start));
}
啥叫用Opentracing埋点呢
就是使用Opentracing的api在应用中植入分布式追踪功能
埋点环境
埋点说白了基本上跟上面的例子一样,就是前后加一段代码就OK了,非常简单。要真这么简单,那老夫今天还讲个毛啊。
首先,埋点不是你想埋就能埋的,这受限于你埋点的环境。比如,被埋点代码是可以直接修改的,那当然可以直接修改源代码进行埋点。但是如果你拿不到源代码。比如一些开源的框架,你又该如何埋点呢?
下面我们就来说说这两种场景下的埋点
可直接修改源代码情况下的埋点
一般来讲,可以拿到源代码的情况一般是自己的业务代码,或者是公司内部自己的框架。这种情况下的埋点相对来讲比较容易,因为代码都掌握在你自己手上了,还不是你想怎么埋就怎么埋。但是这个里面也有一个问题,就是如果你要埋点的地方比较多的话,那么最好在一个统一的收口的地方来埋点,不然不仅工作量大,而且容易漏掉一些地方。后面会详细讲这种情况的埋点策略
不可修改源代码情况下的埋点
如果你们使用的是开源的框架的话,那么几乎就是不可修改源代码的情况。那这种情况下又该如何埋点呢?这个问题其实不用担心,现在的开源框架基本都预留了扩展点来让用户实现自己特有的业务,所以对于这种情况就是寻找框架的扩展点,然后定制自己的埋点业务。比如SpringMvc的埋点就可以使用HandlerInterceptor来埋点,其他框架的埋点我们后面再详细地讲
埋点方式
在分布式追踪领域埋点基本可以分为两种方式:
- 自动探针模式
- 手动埋点
自动探针模式在Java里面都是使用Java Intrumentation、Java agent技术来实现的,如国内的skywalking,韩国棒子的pinpoint,以及国内的商品 APM听云等都是自动探针模式来埋点的。不过这种埋点方式对于前期的投入比较大,研发难度相对而言比较高。我们今天暂时不讨论这种埋点方式。
我们今天只讨论使用Opentracing手动埋点方式。
手动
那是不是说手动埋点方式一定比较low呢?也不一定。因为自动探针好就好在开发人员完全不需要关心分布式追踪,就自动有了分布式追踪功能,真正做到了零侵入地埋点接入到分布式追踪系统
那手动埋点是不是就完全就做不到零侵入地接入呢?对,是的。但是我们可以做到几乎零侵入的埋点,尤其是现在有了SpringBoot这种自动装配的框架,更是更非常容易地实现几乎零侵入*地埋点功能。
下面我们就来讲一下在SpringBoot环境下的埋点
SpringBoot环境下的埋点
发车首先SpringBoot为我们提供了很多的扩展点,使得我们可以任意地定制我们自己想要的功能,这为几乎零侵入的埋点提供了可能。
我们来简单看一下SpringBoot环境下需要为哪些组件提供埋点功能
- SpringMvc或者纯Java Web
- Http访问(RestTemplate,Feign)
- 数据库访问
- Redis访问(RedisTemplate)
- 消息中间件的埋点(rabbitmq,kafka等)
对于SpringMvc来讲,我们可以通过定制HandlerInterceptor来拦截Http请求,如果没有使用SpringMvc,那么可以定制Filter来实现对http请求的埋点。对Http访问组件RestTemplate可以定制ClientHttpRequestInterceptor来实现埋点。具体可以参考java-spring-web。对于这种埋点方式就是要注意埋点组件在框架中的顺序。就是说如果是对于Springmvc来讲HandlerInterceptor的埋点组件一定是第一个执行的,不然如果有其他的用户自定义的HandlerInterceptor,那么就会丢失这一部分的执行时间。ClientHttpRequestInterceptor埋点也要注意这个问题。
然后是对于数据库访问的埋点。如果是针对每种不同类型的数据库访问进行埋点,那么需要研究每种数据库驱动的扩展点,比如mysql5.x的驱动可以扩展com.mysql.jdbc.StatementInterceptorV2来实现对mysql数据库访问的埋点。那对于其他类型的数据库访问就需要研究其他驱动的扩展点,相对来讲比较繁琐特别是对于公司内使用了多种数据库的情况。所以我们可以在Jdbc层做埋点。这就是标准的好处。
但是Jdbc层又没有合适的扩展点来做Jdbc的埋点,那怎么办呢?
这种情况下就可以使用Spring AOP + 静态代理的方法。具体可以参考opentracing-spring-cloud-jdbc-starter。那这种方法总结起来就是:Spring Aop在运行时获取Connnection,然后再将原生的Connnection替换为Opentracing的Connnection。
那其实还有另外一种比较吃力的做法,使用BeanFactoryPostProcessor来适配各种数据源连接池的方式,比如一开始sofa-tracer就是使用的这种方式,具体代码参考DataSourceBeanFactoryPostProcessor 。不过这种方式是有缺陷(DataSource埋点接入配置问题 )的,所以后来官方又提供了BeanPostProcessor的支持。
所以个人不建议使用BeanFactoryPostProcessor来做埋点。
对于RedisTemplate来讲也没有类似于RestTemplate的Interceptor扩展点,所以我们就可以采用静态代理的方式来把原生的RedisConnection替换为具有Opentracing功能的RedisConnection。关键是什么时机替换?可以使用Spring Aop在运行时替换,也可以在使用Spring容器提供的扩展点BeanPostProcessor来偷偷替换掉默认的RedisConnectionFactory。如果对BeanPostProcessor 不熟悉的同学,可以参考我的另外一篇文章Spring扩展点总结。不过对于BeanPostProcessor 的使用需要注意二次代理的问题### spring的二次代理原因及如何排查
那对于消息中间件的埋点也基本类似。这里就不一一说明了,对于rabbitmq的埋点可以参考java-spring-rabbitmq
非SpringBoot环境下的埋点
如果不是使用Spring的组件,比如是自己公司的框架,或者一些工具类(比如httpclient)之类的,那么埋点也基本跟SpringBoot下的埋点类似,只不过SpringBoot下的埋点几乎可以做到零侵入,而非SpringBoot环境下的埋点大多数情况下需要用户修改一点代码。如
java-apache-httpclient在使用的时候就需要讲HttpClientBuilder对象替换为TracingHttpClientBuilder对象。java-redis-client在使用原生jedis的时候就需要讲Jedis对象替换为TracingJedis对象。
其基本思想都一样,都是静态代理
那有没有其他的方式来做埋点呢?可以使用动态代理。比如你的框架定义了非常多的方法,而且有可能不停的增加方法,那么久会写非常多的代理方法,而且还存在反复修改的情况。所以如果有一个统一的访问入口来收口的话,那么只需要埋一次点就万事大吉了。我们公司的自研的redis访问组件就是采用的这种思想,因为redis的命令太多了,光写静态代理就得写个几百个方法,要累死人,所以这种动态代理(无论是jdk原生代理还是cglib代理都可以)的方式比较合适。
但是静态代理的优点是参数含义比较确定。比如在对redis埋点的时候,静态代理是可以明确redis的key是什么值的。但是动态代理就不太能确定key到底是什么,所以动态代理无法明确每一个参数的含义,有可能会影响埋点的质量。需要做权衡。
埋点方式总结
总结埋点方式 | 适用场景 | 注意点 | 优先级 |
---|---|---|---|
定制框架内置的inteceptor,filter等扩展点 | 只要框架支持就优先考虑 | 组件顺序 | 最先考虑 |
Spring aop + 静态代理 | 框架没有预留扩展点 | 重复埋点 | 第二选择 |
Spring aop + BeanPostProcessor | 框架没有预留扩展点 | 防止二次代理的问题 | 第二选择 |
动态代理 | 静态代理需要代理的放非常多而且比较容易变动方法定义的时候 | 处理好参数含义 | 静态代理太麻烦的时候考虑 |
埋点之坑
坑使用Spring Aop + 静态代理的方法来埋点的时候要注意,被替换掉的对象可能存在多种代理,在切面中要处理重复代理的情况。如Bug Report jdbc generate duplicated span
在使用Opentracing埋点的时候不要忘记对异常情况的处理。如when error occurs,it do not report error span
如果对底层的api不熟悉的话,那么很容易在埋点的时候忽略了一些性能上的问题。如
JdbcAspect.getConnection
will executeSELECT USER()
every time
如果埋点做得比较完善的话,那么即使是各种极端情况,也不会对应用产生影响。如Redis keys length should be limited ,以及解析jdbc url出错的情况下的异常处理,如
add jdbc url parser for adding for database information to the trace
网友评论