一、问题描述
当我们的业务发展到一定阶段的时候,系统的复杂度往往会非常高,不再是一个简单的单体应用所能够承载的,随之而来的是系统架构的不断升级与演变。一般对于大型的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

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

被折叠的 条评论
为什么被折叠?



