本文源码基于Pinpoint 2.0.3-SNAPSHOT版本
官方开源地址:https://github.com/naver/pinpoint
Pinpoint通过字节码增强技术来实现无侵入式的调用链采集。其核心实现是基于JVM的Java Agent机制。
我们使用Pinpoint时,需要在Java应用启动参数上加上-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar
参数,这样,当我们的Java应用启动时,会同时启动Agent。
Pinpoint Agent在启动的时候,会加载plugin
文件夹下所有的插件,这些插件会对特定class类修改字节码,在一些指定的方法调用前后加上链路采集逻辑(比如Dubbo中AbstractProxyInvoker
的invoke()
方法),这样就实现了调用链监控功能。
Pinpoint官方文档中的原理描述:
在pintpoin-bootstrap模块中,我们可以在pom文件中看到maven插件里有MANIFEST相关的配置,指定了Premain-Class
,这个配置在打包时也会生成到MANIFEST.MF文件中:
通过上述配置可知,当我们启动接入了pinpoint的Java应用时,会先执行PinpointBootStrap.premain()
方法:
去掉了日志等非核心逻辑代码
public static void premain(String agentArgs, Instrumentation instrumentation) {
// 1.设置启动状态,避免重复初始化
final boolean success = STATE.start();
if (!success) {
return;
}
// 2.初始化和解析启动参数
final JavaAgentPathResolver javaAgentPathResolver = JavaAgentPathResolver.newJavaAgentPathResolver();
final String agentPath = javaAgentPathResolver.resolveJavaAgentPath();
final Map<String, String> agentArgsMap = argsToMap(agentArgs);
final ClassPathResolver classPathResolver = new AgentDirBaseClassPathResolver(agentPath);
// 3.查找核心jar包
final AgentDirectory agentDirectory = resolveAgentDir(classPathResolver);
BootDir bootDir = agentDirectory.getBootDir();
appendToBootstrapClassLoader(instrumentation, bootDir);
// 4.获取类加载器,加载核心jar中的类
ClassLoader parentClassLoader = getParentClassLoader();
final ModuleBootLoader moduleBootLoader = loadModuleBootLoader(instrumentation, parentClassLoader);
PinpointStarter bootStrap = new PinpointStarter(parentClassLoader, agentArgsMap, agentDirectory, instrumentation, moduleBootLoader);
// 5.启动bootStrap
if (!bootStrap.start()) {
logPinpointAgentLoadFail();
}
}
可以看到premain()方法有两个参数,最重要的是这个instrumentation
对象
Instrumentation
是Java提供的一个来自JVM的接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向classLoader的classpath下加入jar文件等,
在PinpointBootStrap.premain()
方法中,主要完成了相关jar包的查找和加载,然后将一系列配置以及instrumentation对象构造成PinpointStarter
对象,并执行start()
方法完成后续的启动:
boolean start() {
// 1.读取agentId和applicationName
final AgentIds agentIds = resolveAgentIds();
final String agentId = agentIds.getAgentId();
final String applicationName = agentIds.getApplicationName();
final boolean isContainer = new ContainerResolver().isContainer();
try {
// 2.解析并加载配置
final Properties properties = loadProperties();
ProfilerConfig profilerConfig = new DefaultProfilerConfig(properties);
// 3.设置日志路径和版本信息到systemProperty
saveLogFilePath(agentDirectory);
savePinpointVersion();
// 4.创建AgentClassLoader
URL[] urls = resolveLib(agentDirectory);
final ClassLoader agentClassLoader = createClassLoader("pinpoint.agent", urls, parentClassLoader);
if (moduleBootLoader != null) {
moduleBootLoader.defineAgentModule(agentClassLoader, urls);
}
final String bootClass = getBootClass();
AgentBootLoader agentBootLoader = new AgentBootLoader(bootClass, agentClassLoader);
final List<String> pluginJars = agentDirectory.getPlugins();
// 5.构建AgentOption,并作为参数通过反射机制构建Agent(DefaultAgent)
AgentOption option = createAgentOption(agentId, applicationName, isContainer, profilerConfig, instrumentation, pluginJars, agentDirectory);
Agent pinpointAgent = agentBootLoader.boot(option);
// 6.启动死锁监控线程、agent数据上报线程、注册ShutdownHook
pinpointAgent.start();
pinpointAgent.registerStopHandler();
} catch (Exception e) {
return false;
}
return true;
}
上面过程其实还是加载配置并构建一些对象,这里面最核心的逻辑是构建Agent
对象,执行了DefaultAgent
类的构造器,初始化了上下文:
new DefaultAgent()
┆┈ DefaultAgent.newApplicationContext()
┆┈┈┈ new DefaultApplicationContext()
这里我们直接看DefaultApplicationContext
类的构造器中的关键逻辑:
public DefaultApplicationContext(AgentOption agentOption, ModuleFactory moduleFactory) {
// 1.获取Instrumentation对象
final Instrumentation instrumentation = agentOption.getInstrumentation();
// 2.构建Guice ioc容器,用于依赖注入
final Module applicationContextModule = moduleFactory.newModule(agentOption);
this.injector = Guice.createInjector(Stage.PRODUCTION, applicationContextModule);
// 3.通过Guice注入一系列对象
this.profilerConfig = injector.getInstance(ProfilerConfig.class);
this.interceptorRegistryBinder = injector.getInstance(InterceptorRegistryBinder.class);
this.instrumentEngine = injector.getInstance(InstrumentEngine.class);
this.classFileTransformer = injector.getInstance(ClassFileTransformer.class);
this.dynamicTransformTrigger = injector.getInstance(DynamicTransformTrigger.class);
// 4.通过instrumentation对象注册类转换器
instrumentation.addTransformer(classFileTransformer, true);
...
}
Guice是谷歌开源的一个轻量级的依赖注入框架,pinpoint依靠Guice管理各种对象。
在初始化ioc容器的过程中,会遍历plugin目录下的所有插件对其进行初始化,调用过程如下:
ApplicationServerTypeProvider.get()
|— PluginContextLoadResultProvider.get()
|—— new DefaultPluginContextLoadResult()
|——— DefaultProfilerPluginContextLoader.load()
|———— DefaultProfilerPluginContextLoader.setupPlugin()
|————— DefaultPluginSetup.setupPlugin()
|—————— XxxPlugin.setup()
(具体Plugin实现)
以DubboPlugin
为例,在setup()
方法中主要对dubbo中的核心类进行转换器绑定:
@Override
public void setup(ProfilerPluginSetupContext context) {
DubboConfiguration config = new DubboConfiguration(context.getConfig());
...
this.addTransformers();
}
private void addTransformers() {
// 为dubbo核心rpc调用类绑定Transform关系
transformTemplate.transform("com.alibaba.dubbo.rpc.protocol.AbstractInvoker", AbstractInvokerTransform.class);
transformTemplate.transform("com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker", AbstractProxyInvokerTransform.class);
}
再来看看其中一个Transform类都做了些什么:
public static class AbstractInvokerTransform implements TransformCallback {
@Override
public byte[] doInTransform(Instrumentor instrumentor, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
// 指定目标类(上一步绑定的类)
final InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer);
// 指定目标方法(方法名、参数)
InstrumentMethod invokeMethod = target.getDeclaredMethod("invoke", "com.alibaba.dubbo.rpc.Invocation");
if (invokeMethod != null) {
// 为此方法添加拦截器
invokeMethod.addInterceptor(DubboConsumerInterceptor.class);
}
return target.toBytecode();
}
}
可以看到,这个类实现了TransformCallback
接口,这个接口从名字上可以看出是一个回调接口,而在其doInTransform()
方法中,是通过字节码增强的方式,为com.alibaba.dubbo.rpc.protocol.AbstractInvoker
类的invoke()
方法添加了一个拦截器DubboConsumerInterceptor
。
DubboConsumerInterceptor
实现了AroundInterceptor
接口的before()
和after()
方法,这里可以看出和Spring AOP很相似了,而在拦截器中,主要是对dubbo的RPC调用进行trace、span等链路追踪信息的记录。
在上下文初始化时,Pinpoint向instrumentation
注册了一个Transformer
类转换器,该接口只定义了一个方法transform()
,该方法会在加载新class类或者重新加载class类时调用,其调用路径如下:
DefaultClassFileTransformerDispatcher.transform()
|— BaseClassFileTransformer.transform()
|—— MatchableClassFileTransformerDelegate.transform()
|——— TransformCallback.doInTransform()
可以看到,最后执行的就是我们在上面执行XxxPlugin.setup()
方法时配置的回调接口,即对指定的方法进行字节码增强。而Java应用启动后,加载的就是我们增强后的类,从而实现链路监控或其他的功能。