cover_image

字节码增强在全链路压测场景的使用

基础架构团队 北京顺丰同城科技技术团队
2023年05月30日 12:02






字节码增强在全链路压测场景的使用


图片
01
背景

    压测是软件开发领域比不可少的一项活动,通过压测,我们可以评估软件系统的处理能力,并了解系统在极端负载下的稳定性。传统的压测方式一般是通过线下模拟的形式,对一个或几个核心应用进行测试。随着微服务的盛行,传统的压测方式迎来了一些挑战:服务端的调用链路变得愈发复杂,梳理出一个完整、没有遗漏的调用链路变得异常困难;应用节点数的增加所带来的镜像应用机器成本也不可忽视;只压测核心应用,较难找到整个调用链路的短板。

    因此,在线上环境进行全链路压测变得很有必要。全链路压测的建设,需要压测平台支持流量录制、回放,同时也需要各语言栈的适配工作,各语言的SDK要对压测标记进行传递,对数据库、MQ等中间件的压测流量进行拦截、转发。

    本文主要介绍全链路压测在Java技术栈的适配及Sftc-Agent的实现。


图片
02
技术选择

图片

技术方案

    Java语言的实现方案上有两种选择,一种是基于已有的开源SDK的某个版本,fork出新的分支进行源码级别的改造;一种是基于Java对ClassFile层面字节码修改的支持,使用Java Agent在运行时修改代码,添加压测相关的逻辑。

两种实现方式的对比

组件改造

字节码增强

  • 代码可读性好

  • 开发难度高

  • 侵入业务

  • 业务无侵入

  • 维护量大、升级困难

  • 业务无感知、升级简单

    基于以上考虑,我们选用字节码增强的方式实现压测标记的传递和压测流量的转发。

图片
字节码工具选择

    工欲善其事,必先利其器。在选择了字节码增强的方案后,需要选择一种高效、便捷的字节码修改工具。

字节码工具对比

字节码库

Asm

ByteBuddy

Javassist

特点

高性能、直接操作字节码

基于asm,API即DSL,社区活跃

采用java编码方式,简单快速

缺点

使用繁琐,工程开发必须进行二次封装

文档简单、特性复杂

代码通过字符串拼接不易维护

最终我们选择了Api更友好、同时性能也不错的ByteBuddy。


图片
03
Java Agent

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

图片


1.优先执行sftc-agent.jar的premain方法。

2.执行Instrumentation.addTransformer()方法注册回调。

3.执行应用的main方法。

4.加载应用代码的过程中触发回调,修改字节码,实现压测逻辑的注入。


图片


图片

04
Sftc-Agent实现

    Sftc-Agent是公司内部使用的Java Agent库,支撑了业务部门进行全链路压测,对业务上是透明、无感知的。

图片
模块介绍

Sftc-Agent包含多个模块,不同模块最终由不同类加载器加载,模块结构如下:


图片

图片


Agent:

Sftc-Agent的入口,负责其他模块的加载并注册修改字节码的回调。

Bootstrap-Inject:

需要通过appendToBootstrapClassLoaderSearch方法添加到BootstrapClassLoader的模块,因为我们增强了HttpURLConnection,有一些类需要放在一个能被访问到的位置。

Common:

通用的一些类和方法。

Plugin:

对Redis、Kafka等SDK的增强逻辑的具体实现。

Plugin-Api:

Plugin中共用的一些类和方法。

Agent-Core:

Sftc-Agent本身需要的一些功能,如配置热更新、Logger实现等。



图片

Plugin、Plugin-Api、Agent-Core模块都是通过自定义的类加载器是去加载的,这样的目的是为了类的隔离,避免污染应用程序内的类加载器。

图片
SDK适配

    为了避免压测流量对线上数据库、MQ、Redis等资源的污染,需要实现压测流量与线上流量的隔离。隔离性可以在物理级别保证,如新建集群(即Shadow数据库、MQ集群等),也可以共用同一套集群,在资源命名上进行区分,如Shadow Table、Shadow Topic、Shadow Key等方式。不同方案的侧重点不同,在使用时需要权衡。

    因此, SDK需要承担流量识别、流量拦截及转发的职责,这些逻辑是原有的SDK不具备的,需要通过字节码增强实现压测逻辑代码的注入。对于Java技术栈,需要适配的组件有:

图片


Servlet容器包含Tomcat、Jetty

Redis Client 包含Jedis、Lettuce、Redisson

Kafka Client 包含原生KafkaClient、SpringKafka

Http Client 包含Apache HttpClient、OkHttp、HttpURLConnection

MySQL Client DruidDataSource


图片

以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显式指定增强类的路径,让子类加载器统一加载所有的增强类,这样就消除了父子类加载器中的类互相访问的链路。


图片
05
现状及未来

    目前全链路压测Java Agent库已经支持了多个业务线的压测使用,支持处理Jetty、Kafka、Redis、MySQL相关的压测流量。同时,接入了配置中心,实现压测配置的动态更新,也对压测流量的监控指标进行了适配。

    技术实现往往会有副作用,Java Agent在无侵入改变代码逻辑的同时,也增加了应用的启动时间。尤其是应用使用了多个Java Agent库以后,启动时间可能会成倍的增加。当前我们使用了一些优化手段去提升应用的启动效率(使用ByteBuddy缓存等),也在探索新的方式去降低Java Agent对应用启动时间的影响。

    Sftc-Agent现阶段的主要功能是支持全链路压测,后续会在链路诊断上提供支持。也会逐渐承担部分治理的工作,如统一管理其他Java Agent(RASP产品、OpenTelemetry等)、支持Service Mesh等。



END


继续滑动看下一个
北京顺丰同城科技技术团队
向上滑动看下一个