Enhancer-轻量化的字节码增强组件包

博客针对大型To C互联网企业微服务架构下,单个系统故障影响整体系统的问题,采用5why提问法,决定通过字节码增强对目标方法做AOP拦截。介绍了方案选型、技术方案、实现、测试、性能测试、使用方式和扩展能力等内容,还规划了未来获取系统性能分析数据。

一、问题描述

当我们的业务发展到一定阶段的时候,系统的复杂度往往会非常高,不再是一个简单的单体应用所能够承载的,随之而来的是系统架构的不断升级与演变。一般对于大型的To C的互联网企业来说,整个系统都是构建于微服务的架构之上,原因是To C的业务有着天生的微服务化的诉求:需求迭代快、业务系统多、领域划分多、链路调用关系复杂、容忍延迟低、故障传播快。微服务化之后带来的问题也很明显:服务的管理复杂、链路的梳理复杂、系统故障会在整个链路中迅速传播。

这里我们不讨论链路的依赖或服务的管理等问题,本次要解决的问题是怎么防止单个系统故障影响整个系统。这是一个复杂的问题,因为服务的传播特性,一个服务出现故障,其他依赖或被依赖的服务都会受到影响。为了找到解决问题的办法,我们试着通过5why提问法来找答案。

PS:这里说的系统故障,是特指由于慢调用、慢查询等影响系统性能而导致的系统故障。

Q1
怎么防止单个系统故障影响整个系统?
A:避免耽搁系统的故障的传播。

Q2
怎么避免故障的传播?
A:找到系统故障的原因,解决故障。

Q3
怎么找到故障的原因?
A:找到并优化系统中耗时长的方法。

Q4
怎么找到系统中耗时长的方法?
A:通过对特定方法进行AOP拦截。

Q5
怎么对特定方法做AOP拦截?
A:通过字节码增强的方式对目标方法做拦截并植入内联代码。

通过5why提问法,我们得到了解决问题的方法,我们需要对目标方法做AOP拦截,统计业务方法及各个子方法的耗时,得到所有方法的耗时分布,快速定位到比较慢的方法,最后找出业务系统的性能瓶颈在哪里。

二、方案选型

我们知道AOP是一种编码思想,跟OOP不同,AOP是将特定的方法逻辑,以切面的形式编织到目标方法中,这里不再赘述AOP的思想。

如果在网上搜一下“AOP的实现方式”,你会得到大致相同的结果:AOP的实现方式是通过动态代理或Cglib代理。其实这不太准确,准确的来说,AOP可以通过代理或Advice两种方式来实现。请注意这里说的Advice并不是Spring所依赖的aspectj中的Advice,而是一种代码织入的技术,它与代理的区别在于,代码织入技术不需要创建代理类。

如果用图形表示的话,可以更简单更直观的感受到两者的区别。代码织入的方式,不会创建代理类,而是直接在目标方法的方法体的前后织入一段内联的代码,以达到增强的效果,如下图所示:
 


我选择代码织入技术而不是AOP,原因是可以避免创建大量的代理类增加元空间的内存占用,另外代码织入技术更底层一些,能实现的能力更强,此外内联代码会随着原方法一起执行,性能也更好。

有了具体的技术选型的方案之后,我们还需要确定该方案的建设目标,以下整理了一些基本的目标:

三、技术方案

代码织入的时机也有多种方式,比如Lombok是通过在编译器对代码进行织入,主要依赖的是在 Javac 编译阶段利用“Annotation Processor”,对自定义的注解进行预处理后生成代码然后织入;其他的像CGLIB、ByteBuddy等框架是在运行时对代码进行织入的,主要依赖的是Java Agent技术,通过JVMTI的接口实现在运行时对字节码进行增强。

本次的技术方案,用一句话可以概括为:通过字节码增强,对指定的目标方法进行拦截,并在方法前后织入一段内联代码,在内联代码中计算目标方法的耗时,最后将统计到的方法信息进行分析。

1 项目结构

整个方案的代码实现非常简单,用一个图描述如下:
 


项目的代码结构如下所示,核心代码非常少:

2 核心组件

其中Enhancer是增强器的入口类,在增强器启动时会扫描所有的插件:EnhancedPlugin。

EnhancedPlugin表示的是一个执行代码增强的插件,其中定义了几个抽象方法,需要由用户自己实现:

/**
 * 执行代码增强的插件
 *
 * @auther houyi.wh
 * @date 2023-08-15 20:12:01
 * @since 0.0.1
 */
public abstract class EnhancedPlugin {

    /**
     * 匹配特定的类型
     *
     * @return 类型匹配器
     * @since 0.0.1
     */
    public abstract ElementMatcher.Junction<TypeDescription> typeMatcher();

    /**
     * 匹配特定的方法
     *
     * @return 方法匹配器
     * @since 0.0.1
     */
    public abstract ElementMatcher.Junction<MethodDescription> methodMatcher();

    /**
     * 负责执行增强逻辑的拦截器
     *
     * @return 拦截器
     * @since 0.0.1
     */
    public abstract Class<? extends Interceptor> interceptorClass();

}

此外EnhancedPlugin中还需要指定一个Interceptor,一个Interceptor是对目标方法执行代码增强的拦截器,主要的拦截逻辑定义在Interceptor中。

3 增强原理

扫描到EnhancedPlugin之后,会构建ByteBuddy的AgentBuilder,主要的构建过程为:

(1)找到所有匹配的类型

(2)找到所有匹配的方法

(3)传入执行代码增强的Transformer

最后通过AgentBuilder.install方法将增强的代码Transformer,传递给Instrumentation实例,实现运行时的字节码retransformation。

这里的Transformer是由Advice负责实现的,而在Advice中实现了增强逻辑的dispatch,即根据不同的EnhancedPlugin可以将增强逻辑交给指定的Interceptor拦截器去实现,主要在拦截器中抽象了两个方法。一个是beforeMethod,负责在目标方法调用之前进行拦截:

/**
 * 在方法执行前进行切面
 *
 * @param pluginName 绑定在该目标方法上的插件名称
 * @param target     目标方法所属的对象,需要注意的是@Advice.This不能标识构造方法
 * @param method     目标方法
 * @param arguments  方法参数
 * @return 方法执行返回的临时数据
 * @since 0.0.1
 */
@Advice.OnMethodEnter
public static <T> T beforeMethod(
        // 接收动态传递过来的参数
        @PluginName String pluginName,
        // optional=true,表示this注解可以接收:构造方法或静态方法(会将this赋值为null),而不报错
        @Advice.This(optional = true) Object target,
        // 目标方法
        @Advice.Origin Method method,
        // nullIfEmpty=true,表示可以接收空参数
        @Advice.AllArguments(nullIfEmpty = true) Object[] arguments
) {
    String[] parameterNames = new String[]{};
    T transmitResult = null;
    try {
        InstanceMethodInterceptor<T> interceptor = getInterceptor(pluginName);
        // 执行beforeMethod的拦截逻辑
        transmitResult = interceptor.beforeMethod(target, method, parameterNames, arguments);
    } catch (Throwable e) {
        InternalLogger.AutoDetect.INSTANCE.error("InstanceMethodAdvice beforeMethod occurred error", e);
    }
    return transmitResult;
}

一个是afterMethod,负责在目标方法被调用之后进行拦截:

/**
 * 在方法执行后进行切面
 *
 * @param pluginName     绑定在该目标方法上的插件名称
 * @param transmitResult beforeMethod所传递过来的临时数据
 * @param originResult   目标方法原始返回结果,如果目标方法是void型,则originResult为null
 * @param throwable      目标方法抛出的异常
 */
@Advice.OnMethodExit(onThrowable = Throwable.class)
public static <T> void afterMethod(
        // 接收动态传递过来的参数
        @PluginName String pluginName,
        // beforeMethod传递过来的临时数据
        @Advice.Enter T transmitResult,
        // typing=DYNAMIC,表示可以接收void类型的方法
        @Advice.Return(typing = Assigner.Typing.DYNAMIC) Object originResult,
        // 目标方法自己抛出的运行时异常,可以在方法中进行捕获,看具体的需求
        @Advi
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值