pfinder实现原理揭秘
01 引言
在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!
02
pfinder概述
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将
2.1 pfinder简介
2.2 pfinder功能
多维监控: 支持按多个维度统计监控指标,按机房、按分组、按JSF别名、按调用方,各种维度随心组合查看
自动埋点: 自动对 SpringMVC,JSF,MySQL,JMQ 等常用中间件进行性能埋点,无需改动代码,接入即可观测
应用拓扑: 自动梳理服务的上下游和中间件的依赖拓扑
调用链追踪: 基于请求的跨服务调用追踪,助你快速分析性能瓶颈
自动故障分析: 通过AI算法自动分析调用拓扑上所有服务的监控数据,自动判断故障根因
流量录制回放: 通过录制线上流量,回放至待特定环境(测试、预发),对比回放与录制时产生的差异,帮助用户补全业务场景、完善测试用例
跨单元逃逸流量监控: 支持 JSF 跨单元流量、逃逸流量监控,单元化应用运行状态一目了然
2.3 APM类组件对比
Zipkin | Pinpoint | SkyWalking | CAT | pfinder | |
贡献者 | 韩国公司 | 华为 | 美团 | 京东 | |
实现方式 | 拦截请求,发送 http/mq 数据到 zipkin 服务 | 字节码注入 | 字节码注入 | 代理埋点(拦截器、注解、过滤器) | 字节码注入 |
接入方式 | 基于 linkerd/sleuth,引入配置即可 | javaagent 字节码 | javaagent 字节码 | 代码侵入 | javaagent 字节码 |
agent 到 collector 传输协议 | http、MQ | thrift | gRPC | http/tcp | JMTP |
OpenTracing | 支持 | 支持 | 支持 | ||
粒度 | 接口级 | 方法级 | 方法级 | 代码级 | 方法级 |
全局调用统计 | 支持 | 支持 | 支持 | 支持 | |
traceid 查询 | 支持 | 支持 | 支持 | ||
告警 | 支持 | 支持 | 支持 | 支持 | |
JVM 监控 | 支持 | 支持 | 支持 | 支持 |
更重要的一点是:pfinder对京东内部自研组件提供了支持,比如:jsf、jmq、jimdb
03
pfinder背后的秘密
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将
既然pfinder是基于字节码增强实现的,那么讲到pfinder,字节码增强技术自然也是无法避开的话题。这里我将字节码增强技术分两点来说,也是我认为实现字节码增强需要解决的两个关键点:
1.字节码是为了机器设计的,而非人类,字节码可读性极差、修改门槛极高,那么我们如何修改字节码呢?
3.1 字节码修改
字节码修改成熟的框架已经很多了,诸如:ASM、javassist、bytebuddy、bytekit,下面我们用这几个字节码修改框架实现一个相同的功能,来对比下这几个框架使用上的区别。现在我们通过字节码修改来实现下面的功能:
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
//方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("com.ggc.javassist.HelloWord");
CtMethod m = cc.getDeclaredMethod("printHelloWord");
m.insertBefore("{ System.out.println(\"start\"); }");
m.insertAfter("{ System.out.println(\"end\"); }");
Class c = cc.toClass();
cc.writeFile("/Users/gonghanglin/workspace/workspace_me/bytecode_enhance/bytecode_enhance_javassist/target/classes/com/ggc/javassist");
HelloWord h = (HelloWord)c.newInstance();
h.printHelloWord();
// 使用ByteBuddy动态生成一个新的HelloWord类
Class<?> dynamicType = new ByteBuddy()
.subclass(HelloWord.class) // 指定要修改的类
.method(ElementMatchers.named("printHelloWord")) // 指定要拦截的方法名
.intercept(MethodDelegation.to(LoggingInterceptor.class)) // 指定拦截器
.make()
.load(HelloWord.class.getClassLoader()) // 加载生成的类
.getLoaded();
// 创建动态生成类的实例,并调用方法
HelloWord dynamicService = (HelloWord) dynamicType.newInstance();
dynamicService.printHelloWord();
public class LoggingInterceptor {
@RuntimeType
public static Object intercept(@AllArguments Object[] allArguments, @Origin Method method, @SuperCall Callable<?> callable) throws Exception {
// 打印start
System.out.println("start");
try {
// 调用原方法
Object result = callable.call();
// 打印end
System.out.println("end");
return result;
} catch (Exception e) {
System.out.println("exception end");
throw e;
}
}
}
// Parse the defined Interceptor class and related annotations
DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();
List<InterceptorProcessor> processors = interceptorClassParser.parse(HelloWorldInterceptor.class);
// load bytecode
ClassNode classNode = AsmUtils.loadClass(HelloWord.class);
// Enhanced process of loaded bytecodes
for (MethodNode methodNode : classNode.methods) {
MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);
for (InterceptorProcessor interceptor : processors) {
interceptor.process(methodProcessor);
}
}
public class HelloWorldInterceptor {
@AtEnter(inline = true)
public static void atEnter() {
System.out.println("start");
}
@AtExit(inline = true)
public static void atEit() {
System.out.println("end");
}
}
ASM | Javassist | ByteBuddy | ByteKit | |
性能 | ASM的性能最高,因为它直接操作字节码,没有中间环节 | 劣于ASM | 介于javassist和ASM之间 | 介于javassist和ASM之间 |
易用性 | 需精通字节码,学习成本高,不支持debug | Java语法进行开发,但是采用的硬编码形式开发,不支持debug | 比Javassist更高级,更符文Java开发习惯,可以对增强代码进行断点调试 | 比Javassist更高级,更符文Java开发习惯,可以对增强代码进行断点调试 |
功能 | 直接操作字节码,功能最为强大。 | 功能相对完备 | 功能相对完备 | 功能相对完备,对比ByteBuddy,ByteKit能防止重复增强 |
3.2 字节码注入
JVMTI接口 | |
接口 | 功能 |
Agent_OnLoad(JavaVM *vm, char *options, void *reserved); | agent在启动时加载的情况下,也就是在vm参数里通过-agentlib来指定,那在启动过程中就会去执行这个agent里的Agent_OnLoad函数。 |
Agent_OnAttach(JavaVM* vm, char* options, void* reserved); | agent是attach到目标进程上,然后给对应的目标进程发送load命令来加载agent,在加载过程中就会调用Agent_OnAttach函数。 |
Agent_OnUnload(JavaVM *vm); | 在agent卸载的时候调用 |
上面说到JVMTIAgent基于C语言开发,以动态链接的形式加载并运行,这对java开发者不太友好。在JDK5之后,JDK开始提供java.lang.instrument.Instrumentation接口,让开发者可以使用Java语言编写Agent。其实,instrument也是基于JVMTI实现的,在MACOS下instrument动态库名为libinstrument.dylib。
instrument主要方法 | |
方法 | 功能 |
void addTransformer(ClassFileTransformer transformer) | 添加一个字节码转换器,用来修改加载类的字节码 |
Class[] getAllLoadedClasses() | 返回当前JVM中加载的所有的类的数组 |
Class[] getInitiatedClasses(ClassLoader loader) | 返回指定的类加载器中的所有的类的数据 |
void redefineClasses(ClassDefinition... definitions) | 用给定的类的字节码数组替换指定的类的字节码文件,也就是重新定义指定的类 |
void retransformClasses(Class<?>... classes) | 指定一系列的Class对象,被指定的类都会重新变回去(去掉附加的字节码) |
3.2.3 instrument和ByteBuddy实现javaagent打印方法耗时
3.2.3.1agent包MANIFEST.MF配置(maven插件)
<archive>
<manifestEntries>
// 指定premain()的所在方法
<Agent-CLass>com.ggc.agent.GhlAgent</Agent-CLass>
<Premain-Class>com.ggc.agent.GhlAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
public class GhlAgent {
public static Logger log = LoggerFactory.getLogger(GhlAgent.class);
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
log.info("agentmain方法");
boot(instrumentation);
}
public static void premain(String agentArgs, Instrumentation instrumentation) {
log.info("premain方法");
boot(instrumentation);
}
private static void boot(Instrumentation instrumentation) {
//创建一个代理增强对象
new AgentBuilder.Default().type(ElementMatchers.nameStartsWith("com.jd.aviation.performance.service.impl"))//拦截指定的类
.transform((builder, typeDescription, classLoader, javaModule) ->
builder.method(ElementMatchers.isMethod().and(ElementMatchers.isPublic())
).intercept(MethodDelegation.to(TimingInterceptor.class))
).installOn(instrumentation);
}
}
3.2.3.3 拦截器
public class TimingInterceptor {
public static Logger log = LoggerFactory.getLogger(TimingInterceptor.class);
@RuntimeType
public static Object intercept(@SuperCall Callable<?> callable) throws Exception {
long start = System.currentTimeMillis();
try {
// 原方法调用
return callable.call();
} finally {
long end = System.currentTimeMillis();
log.info("Method call took {} ms",(end - start));
}
}
}
3.2.3.4 效果
04
pfinder实现原理
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将
4.1 pfinder应用架构
4.2 pfinder插件增强代码解析
4.2.1 service加载
bootService中完成创建service实例、注册service、初始化service,service的加载至此就完成了。
在介绍插件加载前,我们先了解下插件的包含了哪些信息。
plugin的加载和字节码增强发生在初始化service过程中,具体地说发生在com.jd.pfinder.profiler.service.impl.PluginRegistrar这个service初始化的过程中了。
protected boolean doInitialize(ProfilerContext profilerContext) {
AgentEnvService agentEnvService = (AgentEnvService)profilerContext.getService(AgentEnvService.class);
Instrumentation instrumentation = agentEnvService.instrumentation();
if (instrumentation == null) {
LOGGER.info("Instrumentation missing, PFinder PluginRegistrar enhance ignored!");
return false;
}
this.pluginLoaders = profilerContext.getAllService(PluginLoader.class);
this.enhanceHandler = new EnhancePluginHandler(profilerContext);
ElementMatcher.Junction<TypeDescription> typeMatcherChain = null;
for (PluginLoader pluginLoader : this.pluginLoaders) {
pluginLoader.loadPlugins(profilerContext);
for (ElementMatcher.Junction<TypeDescription> typeMatcher : (Iterable<ElementMatcher.Junction<TypeDescription>>)pluginLoader.typeMatchers()) {
if (typeMatcherChain == null) {
typeMatcherChain = typeMatcher; continue;
}
typeMatcherChain = typeMatcherChain.or((ElementMatcher)typeMatcher);
}
}
if (typeMatcherChain == null) {
LOGGER.warn("no any enhance-point. pfinder enhance will be ignore.");
return false;
}
ConfigurationService configurationService = (ConfigurationService)profilerContext.getService(ConfigurationService.class);
String enhanceExcludePolicy = (String)configurationService.get(ConfigKey.PLUGIN_ENHANCE_EXCLUDE);
LoadedClassSummaryHandler loadedClassSummaryHandler = null;
if (((Boolean)configurationService.get(ConfigKey.LOADED_CLASSES_SUMMARY_ENABLED, Boolean.valueOf(false))).booleanValue()) {
loadedClassSummaryHandler = new LoadedClassSummaryHandler.DefaultImpl(configurationService, ((ScheduledService)profilerContext.getService(ScheduledService.class)).getDefault());
}
(new AgentBuilder.Default())
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
.with(new AgentBuilder.RedefinitionStrategy.Listener()
{
public void onBatch(int index, List<Class<?>> batch, List<Class<?>> types) {}
public Iterable<? extends List<Class<?>>> onError(int index, List<Class<?>> batch, Throwable throwable, List<Class<?>> types) {
return Collections.emptyList();
}
public void onComplete(int amount, List<Class<?>> types, Map<List<Class<?>>, Throwable> failures) {
for (Map.Entry<List<Class<?>>, Throwable> entry : failures.entrySet()) {
for (Class<?> aClass : entry.getKey()) {
PluginRegistrar.LOGGER.warn("Redefine class: {} failure! ignored!", new Object[] { aClass.getName(), entry.getValue() });
}
}
}
}).ignore((ElementMatcher)ElementMatchers.nameStartsWith("org.groovy.")
.or((ElementMatcher)ElementMatchers.nameStartsWith("jdk.nashorn."))
.or((ElementMatcher)ElementMatchers.nameStartsWith("javax.script."))
.or((ElementMatcher)ElementMatchers.nameContains("javassist"))
.or((ElementMatcher)ElementMatchers.nameContains(".asm."))
.or((ElementMatcher)ElementMatchers.nameContains("$EnhancerBySpringCGLIB$"))
.or((ElementMatcher)ElementMatchers.nameStartsWith("sun.reflect"))
.or((ElementMatcher)ElementMatchers.nameStartsWith("org.apache.jasper"))
.or((ElementMatcher)pfinderIgnoreMather())
.or((ElementMatcher)Matchers.forPatternLine(enhanceExcludePolicy))
.or((ElementMatcher)ElementMatchers.isSynthetic()))
.type((ElementMatcher)typeMatcherChain)
.transform(this)
.with(new Listener(loadedClassSummaryHandler))
.installOn(instrumentation);
return true;
}
第8行,先从上下文中取出注册的PluginLoader(插件加载器),第12行遍历插件加载器加载插件,插件加载逻辑其实和service一样,使用的都是AddonLoader中的load方法。插件加载完成之后被插件加载器持有,第14-19行则收集插件中增强类的匹配器,用于AgentBuilder的创建。AgentBuilder的创建标志着字节码增强的开始,具体的逻辑在transform的实例方法中。
transform方法中遍历插件,enhance方法中对各个插件做增强。
enhance方法中遍历各个插件的增强点数组走enhanceInterceptPoint方法做增强。
enhanceInterceptPoint方法中根据增强点类型做增强。
上图是以Advice方式增强实例方法,传递了interceptorFieldAppender和methodCacheFieldAppender两个参数,并使用AdviceMethodEnhanceInvoker访问并修改待增强的类和方法。AdviceMethodEnhanceInvoker中有onMethodEnter、onMethodExit两个方法,分别表示进入方法后和退出方法前。
05
一些思考
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将
5.1 多线程traceId丢失问题
pfinder目前已经将traceId放到了MDC中,我们通过在日志配置文件中添加[%X{PFTID}]便能在日志中打印traceId。但是我们知道MDC使用的是ThreadLocal去保存的traceId,在跨线程时会出现线程丢失的情况。pfinder在这方面做了字节码增强,无论使用线程池还是@Async,都不会存在traceId丢失的问题。
public class TracingRunnable
implements PfinderWrappedRunnable
{
private final Runnable origin;
private final TracingSnapshot<?> snapshot;
private final Component component;
private final String operationName;
private final String interceptorName;
private final InterceptorClassLoader interceptorClassLoader;
public TracingRunnable(Runnable origin, TracingSnapshot<?> snapshot, Component component, String operationName, String interceptorName, InterceptorClassLoader interceptorClassLoader) {
this.origin = origin;
this.snapshot = snapshot;
this.component = component;
this.operationName = operationName;
this.interceptorClassLoader = interceptorClassLoader;
this.interceptorName = interceptorName;
}
public void run() {
TracingContext tracingContext = ContextManager.tracingContext();
if (tracingContext.isTracing() && tracingContext.traceId().equals(this.snapshot.getTraceId())) {
this.origin.run();
return;
}
LowLevelAroundTracingContext context = SpringAsyncTracingContext.create(this.operationName, this.interceptorName, this.snapshot, this.interceptorClassLoader, this.component);
context.onMethodEnter();
try {
this.origin.run();
} catch (RuntimeException ex) {
context.onException(ex);
throw ex;
} finally {
context.onMethodExit();
}
}
public Runnable getOrigin() {
return this.origin;
}
public String toString() {
return "TracingRunnable{origin=" + this.origin + ", snapshot=" + this.snapshot + ", component=" + this.component + ", operationName='" + this.operationName + '\'' + '}';
}
}
拿线程池执行Runnable任务来说,pfinder通过TracingRunnable包装我们的Runnable的实现,利用构造函数将主线程的traceId通过snapshot参数传给TracingRunnable,在run方法中将参数snapshot放到上下文中,最后从上下文中取出放到子线程的MDC中,从而实现traceId跨线程传递。
5.2 热部署
类搜索:
上述只是笔者做的一个简单的实现,还有很多不足的地方:
1.对于Spring XML、MyBatis XML的支持;
欢迎有兴趣的同学一起学习交流。
京东Apple Vision Pro版 Is Coming!
记一次疑似JVM内存泄漏的排查过程探索大语言模型:理解Self Attention