压测是软件开发领域比不可少的一项活动,通过压测,我们可以评估软件系统的处理能力,并了解系统在极端负载下的稳定性。传统的压测方式一般是通过线下模拟的形式,对一个或几个核心应用进行测试。随着微服务的盛行,传统的压测方式迎来了一些挑战:服务端的调用链路变得愈发复杂,梳理出一个完整、没有遗漏的调用链路变得异常困难;应用节点数的增加所带来的镜像应用机器成本也不可忽视;只压测核心应用,较难找到整个调用链路的短板。
因此,在线上环境进行全链路压测变得很有必要。全链路压测的建设,需要压测平台支持流量录制、回放,同时也需要各语言栈的适配工作,各语言的SDK要对压测标记进行传递,对数据库、MQ等中间件的压测流量进行拦截、转发。
本文主要介绍全链路压测在Java技术栈的适配及Sftc-Agent的实现。
Java语言的实现方案上有两种选择,一种是基于已有的开源SDK的某个版本,fork出新的分支进行源码级别的改造;一种是基于Java对ClassFile层面字节码修改的支持,使用Java Agent在运行时修改代码,添加压测相关的逻辑。
两种实现方式的对比
组件改造 | 字节码增强 |
|
|
|
|
|
|
基于以上考虑,我们选用字节码增强的方式实现压测标记的传递和压测流量的转发。
工欲善其事,必先利其器。在选择了字节码增强的方案后,需要选择一种高效、便捷的字节码修改工具。
字节码工具对比
字节码库 | Asm | ByteBuddy | Javassist |
特点 | 高性能、直接操作字节码 | 基于asm,API即DSL,社区活跃 | 采用java编码方式,简单快速 |
缺点 | 使用繁琐,工程开发必须进行二次封装 | 文档简单、特性复杂 | 代码通过字符串拼接不易维护 |
最终我们选择了Api更友好、同时性能也不错的ByteBuddy。
Java Agent有两种使用方式:
一种是静态的,这种方式需要在jar启动命令中,添加-javaagent:sftc-agent.jar参数,Java Agent最终和Java应用在一个进程内,启动后JVM会调用agent.jar中定义的premain方法,注册修改字节码的回调函数
另外一种是动态attach,需要提供目标Java进程的pid,attach成功后会执sftc-agent.jar的agentmain方法,一些APM工具就是通过这种方式实现的。
以静态启动为例,Java Agent的执行流程:
执行java -jar -javaagent: sftc-agent.jar -jar application.jar
Sftc-Agent是公司内部使用的Java Agent库,支撑了业务部门进行全链路压测,对业务上是透明、无感知的。
Sftc-Agent包含多个模块,不同模块最终由不同类加载器加载,模块结构如下:
Plugin、Plugin-Api、Agent-Core模块都是通过自定义的类加载器是去加载的,这样的目的是为了类的隔离,避免污染应用程序内的类加载器。
为了避免压测流量对线上数据库、MQ、Redis等资源的污染,需要实现压测流量与线上流量的隔离。隔离性可以在物理级别保证,如新建集群(即Shadow数据库、MQ集群等),也可以共用同一套集群,在资源命名上进行区分,如Shadow Table、Shadow Topic、Shadow Key等方式。不同方案的侧重点不同,在使用时需要权衡。
因此, SDK需要承担流量识别、流量拦截及转发的职责,这些逻辑是原有的SDK不具备的,需要通过字节码增强实现压测逻辑代码的注入。对于Java技术栈,需要适配的组件有:
以HttpURLConnection相关类的增强为例,压测相关逻辑的代码如下:
解决类加载机制带来的问题
JVM对类的加载使用的是Parent Delegate机制,该机制会让子类加载器可以访问父类加载器加载的类,但父类加载器的类无法访问子类加载器加载的类。听起来很绕,但是是一个实际存在的问题。
在SpringBoot项目中,Jar包执行的方法入口是JarLauncher的main方法,JarLauncher中使用自己定义的LaunchedURLClassLoader去加载类。LaunchedURLClassLoader是App ClassLoader的子类,Application Class (业务代码、Redis、Kafka等SDK的代码)都会被它加载,而AgentModuleClass(增强逻辑代码)会被AppClassLoader加载,而增强逻辑最终会被注入到LaunchedURLClassLoader加载的SDK代码中,这样Application Class、AgentModuleClass就形成了双向的依赖,产生了上述问题。
那么,如何解决这个问题呢?可以在启动时,在App ClassLoader和AgentPlugin之间增加AgentClassLoader,AgentClassLoader负责加载增强类,实现字节码修改回调的注册。然后在LaunchedURLClassLoader显式指定增强类的路径,让子类加载器统一加载所有的增强类,这样就消除了父子类加载器中的类互相访问的链路。
目前全链路压测Java Agent库已经支持了多个业务线的压测使用,支持处理Jetty、Kafka、Redis、MySQL相关的压测流量。同时,接入了配置中心,实现压测配置的动态更新,也对压测流量的监控指标进行了适配。
技术实现往往会有副作用,Java Agent在无侵入改变代码逻辑的同时,也增加了应用的启动时间。尤其是应用使用了多个Java Agent库以后,启动时间可能会成倍的增加。当前我们使用了一些优化手段去提升应用的启动效率(使用ByteBuddy缓存等),也在探索新的方式去降低Java Agent对应用启动时间的影响。
Sftc-Agent现阶段的主要功能是支持全链路压测,后续会在链路诊断上提供支持。也会逐渐承担部分治理的工作,如统一管理其他Java Agent(RASP产品、OpenTelemetry等)、支持Service Mesh等。