Javaagent技术探秘

1 前言

从这篇文章开始,将不定期分享一些Java相关的知识;有基础的,也有专业的。本篇主要讨论Javaagent技术和字节码增强在分布式调用链和APM(Application Performance Monitoring, 应用性能监控)中的应用。

2 调用链

随着微服务架构的兴起,一个庞大复杂的服务往往被拆分成多个功能独立的模块,每个模块又会在多台服务器上部署以形成集群。集群化可以提高性能、可用性、伸缩性和扩展性。然而,微服务泛滥也会造成很多问题。比如,通常一次请求往往会涉及多个微服务的互相调用,如果请求失败或者接口太慢,在调用链路很长时就很难定位到故障(或性能瓶颈)究竟在哪个微服务的哪台服务器,而在生产环境发生故障时花很长时间去定位是不可接受的。

3 Javaagent技术

Javaagent是从JDK1.5开始引入的,agent的意思是代理,它实际上是个包含premain方法的jar包。要被监控的程序执行时需要在java启动参数中增加-javaagent参数,指定要加载的javaagent包。premain方法先于main方法执行,可以在类加载之前修改类定义,从而实现很多功能。

当Javaagent正确被加载,premain方法会自动被调用,并且会获取到Instrumentation对象的一个引用。通过Instrumentation这个对象,可以实现很多功能,比如修改类路径、获取已经加载的类、获取对象大小等等。其中最常用的是通过addTransformer方法添加类转化器,动态修改类定义,从而实现无侵入的应用监控。

从JDK1.6开始,提供了agentmain方法,提供了动态修改运行中的已经被加载的类的途径。一般通过VirtualMachine的attach(pid)方法获得VirtualMachine实例,随后调用loadagent方法将Javaagent的jar包加载到目标JVM中。

4 字节码增强

在Java语言设计之初,为了实现“一次编写、到处运行”的设计目标,语言设计者们制定了JVM规范和class文件标准。只要机器上安装了JRE(Java Runtime Environment),就可以执行由任何语言(Java、Scala、Kotlin等等)编译而来的符合规范的class文件。class文件中包含了一个类的所有完整信息,包括属性、方法等。类加载完成后,class文件将被解析到虚拟机的方法区。虚拟机的执行过程就是一个个方法在Java方法栈或本地方法栈中入栈出栈、方法中的字节码被逐条执行的过程。

通过修改字节码就可以修改类定义,常用的字节码增强库有Javassist和ASM等。两者相比较,前者易于使用,无需掌握字节码,但是效率相对较低,适用于注重开发效率的场景;后者更偏底层,需要熟悉字节码,适合用于对性能有较高要求的场景。

5 应用性能监控

通过Javaagent和字节码增强技术,改变特定类的特定方法,在这些方法之前、中、后加上一些代码,就可以实现简单但却强大的功能。比如,在Web容器(Tomcat、Jetty等)的关键类中监控url方法调用,在数据库(MySQL、Oracle等)的客户端类中监控sql执行,在HTTPClient中监控对其他服务的http调用;可以监控虚拟机执行过程中内存、CPU、线程等情况,也可以监控Redis、Kafka等中间件:只有你想不到,没有别人做不到。

常用的监控指标包括调用次数、错误次数、平均耗时以及耗时分布区间等。指标统计时,考虑到并发问题,通常采用原子类实现。值得注意的是,为了隔离agent实现,大部分agent类都通过自定义类加载器加载;而由于这些采集器类在agent中和增强后的应用程序中都会出现,因此必须通过Bootstrap类加载器加载。另外,由于url和sql数量太多,通常需要汇聚后展示;如果超出一定阈值还需要停止采集,否则占用过多内存会对被监控服务本身造成影响。

6 调用链的实现

分布式调用链技术源于谷歌的一篇论文——“Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”。在具体实现中大致可以分为以下几点:
1)每次调用会生成一个唯一的traceId,通过traceId可以把各个服务节点的调用链信息串起来;
2)在一个节点中的调用称为span,一个span包含spanId、耗时、url、method、返回码等信息;每个span下有若干方法调用,每次方法调用称为spanEvent,包含spanEventId、耗时、类名、方法名、异常等信息;
3)通过字节码增强,traceId和spanId被添加到请求头中,通过微服务之间的HTTP调用或者RPC调用实现在上下节点之间的传递,从而实现整个调用链路的串连;
4)在服务节点内部,spanEvent可以存储在ThreadLocal中。利用ThreadLocal线程隔离的特性,正好可以保存各个线程的方法调用信息。通过方法进入前判断TreadLocal中是否为null,可以确定上一个spanEvent和当前spanEvent是层级关系还是平级关系(仅适用于同步方法,不适用于异步方法);方法异常时捕获异常信息并再次抛出;方法结束时统计耗时信息;
5)所有的span和spanEvent数据被发往消息队列,最终存入数据库或搜索引擎;查询时通过traceId一次性获取整个调用链信息,并根据层级关系组装成树形结构展示,调用错误或性能瓶颈一目了然。

7 总结

本文介绍了Javaagent和字节码增强技术及其在调用链和应用性能监控中的应用,并给出了APM监控和调用链的实现方式。后续我也会把自己开发的一个Javaagent分享到GitHub上,目前还在完善中,诸位有兴趣的可以持续关注。

欢迎关注个人公众号!
这里写图片描述

微信交流群
在这里插入图片描述

你可能感兴趣的:(JVM)