如何使用Java实现一个分布式调用链追踪系统?

collie

使用Java实现一个分布式调用链追踪系统

现在项目已经开源,欢迎提pr和star,项目地址:分布式调用链追踪系统Collie

项目系列博客地址:
柠檬好酸啊:用Java实现一个分布式调用链追踪系统 (一)聊聊自己的想法
柠檬好酸啊:用java实现一个分布式调用链追踪系统(二)项目搭建过程中的一些注意事项
柠檬好酸啊:用java实现一个分布式调用链追踪系统(三)最核心的实现之Javassist
柠檬好酸啊:用java实现一个分布式调用链追踪系统(四)项目具体实现
柠檬好酸啊:用java实现一个分布式调用链追踪系统(五)总结

什么是分布式调用链追踪?

在微服务和分布式应用十分常见的系统中,如果系统的规模非常的庞大,那么会带来非常多的麻烦,首先是系统复杂度升高了,各个系统之间互相调用,使得查找问题等变得非常复杂。对于新接手项目的人来说也是非常不友好的。所以急需一个工具来使得复杂的系统变得更清晰。分布式调用链追踪就是这样的一个工具。

初次听到这样的系统,感觉非常的牛逼。秉持着想深入了解一个东西的话那就来实现它的理念,我决定自己动手实现一个简单的分布式调用链追踪系统。

分布式调用链追踪调研

各大公司都有自己的分布式调用链追踪系统,Google的叫做Dapper,淘宝叫鹰眼,Twitter的叫ZipKin,京东商城叫Hydra,eBay叫Centralized Activity Logging (CAL),大众点评网叫CAT。

如何使用Java实现一个分布式调用链追踪系统?_第1张图片

如何使用Java实现一个分布式调用链追踪系统?_第2张图片

如何使用Java实现一个分布式调用链追踪系统?_第3张图片

如何实现一个分布式调用链追踪系统?

调用链追踪无非就是把一个请求的历程中每一个步骤的信息记录并输出到一个地方,最终展示出来。按照这个方式的话我们其实可以简单的构想一下如何实现这种功能:

  1. 直接在每个需要追踪的地方加上代码,这种最简单,但是也是最low的方式了,问题不言自明。
  2. 使用厉害一点的技术,可以用aop比如使用spring的aop还是比较方便的,但是aop还是需要在代码里做一定的修改,对代码的侵入程度比较大,而且加入这个分布式调用链追踪并不会对大多数业务产生正向影响,所以很难让所有开发人员去加上这个功能。
  3. 有没有更厉害的技术呢?当然有,那就是Java agent技术,也叫Java探针。具体的概念先略过了,之前也写过一些。通俗的理解就是可以做到jvm层面的aop,不需要在业务代码里做任何操作,仅使用一个jar包就可以完成代码的改造,不改动业务代码也能实现最终的效果。

那就开始用Java探针技术来做一个分布式调用链追踪。仅使用Java探针技术还不足以完成这个项目,大致想了想。大概会用到一下技术:

  1. Java agent技术

  2. javasist字节码修改技术,可以对class进行更改,当然更牛逼的是ASM但是有学习的成本,自己也学过ASM,但是感觉ASM还是实现起来较为复杂,所以我们只是验证这个分布式追踪的功能实现,并不特别追求性能,还是什么方便用什么吧。

  3. ThreadLocal,在一个线程中串联起各个函数的调用的话,最好的方法就是ThreadLocal了,而且目前确实有些调用链就是这使用这个来实现的。

  4. 数据存储,需要一个接收数据的地方,kafka或者es

  5. 前端数据展示,这个不知道要不要做,当然一个好的系统肯定是要有展示的东西,特别是调用链涉及到时间,有界面展示可能更直观。

  6. 如何使用Java实现一个分布式调用链追踪系统?_第4张图片

调研过程中遇到的困惑和一些技术细节

  1. 分布式的场景下如何实现一个请求的标记和顺序?这个其实比较好解决,在请求到来时设置一个ID就可以,然后再请求过程中一直带着这个ID。暂且命名为TraceId吧。

  2. 如果涉及到不同的系统,这个ID就比较关键,需要一直传递下去。也就是全链路追踪能力。其中vivo的一篇文章提到的非常具有参考性。

    全链路数据传递能力是 vivo 调用链系统功能完整性的基石,也是Agent最重要的基础设施,前面提到过的spanId、traceId及链路标志等很多数据传递都依赖于全链路数据传递能力,系统开发中途由于调用链系统定位更加具体,当前无实际功能依赖于链路标志,本文将不做介绍。项目之初全链路数据传递能力,仅用于Agent内部数据跨线程及跨进程传递,当前已开放给业务方来使用了。

    一般 Java 研发同学都知道 JDK 中的ThreadLocal工具类用于多线程场景下的数据安全隔离,并且使用较为频繁,但是鲜有人使用过JDK 1.2即存在的InheritableThreadLocal,我也是从未使用过。

    InheritableThreadLocal用于在通过new Thread()创建线程时将ThreadLocalMap中的数据拷贝到子线程中,但是我们一般较少直接使用new Thread()方法创建线程,取而代之的是JDK1.5提供的线程池ThreadPoolExecutor,而InheritableThreadLocal在线程池场景下就无能为力了。你可以想象下,一旦跨线程或者跨线程池了,traceId及spanId等等重要的数据就丢失不能往后传递,导致一次请求调用的链路断开,不能通过traceId连起来,对调用链系统来说是多么沉重的打击。因此这个问题必须解决。

    其实跨进程的数据传递是容易的,比如http请求我们可以将数据放到http请求的header中,Dubbo 调用可以放到RpcContext中往后传递,MQ场景可以放到消息头中。而跨线程池的数据传递是无法做到对业务代码无侵入的,vivo调用链Agent是通过拦截ThreadPoolExecutor的加载,通过字节码工具修改线程池ThreadPoolExecutor的字节码来实现的,这个也是一般开源的调用链系统不具备的能力。

  3. 在一个系统中的请求一定是有一个入口和出口,这个入口和出口是接收ID或返回数据的地方。针对不同的系统需要做一下适配。

  4. 不能对所有的类都做切面处理,而是要针对业务类,所以需要针对特定规则来进行过滤。

  5. agent开发过程还是比较麻烦的,主要每次调试之前都得打一个jar包。当然jar包在idea里也可以调试,主要还是每次改动都得打包。

  6. 数据的存储?数据结构的存储,一个是放在JVM里,一个是放在数据库里,或许ELK?这个还是得在过程中来确定,肯定也不止一个。

  7. 不能每一次调用都做处理,需要采样。

  8. 自定义classloader,打破双亲委派模型。为什么要自定义classloader呢,实现Agent与应用环境隔离。还有一个原因是父类加载器加载的类不能引用子类加载器加载的类

  9. 也可以采集jvm相关信息

感谢以下文章:

[1] https://segmentfault.com/a/1190000038254246 vivo写的,非常非常好

[2] https://zhuanlan.zhihu.com/p/136855172 一个简单的字节码插桩

[3] https://my.oschina.net/u/4598048/blog/4549854 一个初步实现的调用链工具,非常有建设性,提供了几个问题的解答

[4] https://my.oschina.net/xiaominmin/blog/3153685 javasist获取系统不到类加载器中的类的问题

想了几个名字,最终决定使用Collie,意为牧羊犬,因为我觉得各个分布式的应用就像是羊群,需要一个牧羊犬来协助开发人员管理。

接下来就开始实现吧,如果你对这个有什么意见也欢迎提出来,也可以一起来实现这个项目。

如果你喜欢,欢迎点赞关注加收藏,后续我还会继续做一些类似的有意思的java玩具。

你可能感兴趣的:(java,java分布式调用链追踪,java,分布式,开发语言)