作者:未完成交响曲,资深Java工程师!目前在某一线互联网公司任职,架构师社区合伙人!
本文源码基于Pinpoint 2.0.3-SNAPSHOT版本
官方开源地址:/naver/pinpoint
Pinpoint Agent
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文件中:
执行premain()方法
通过上述配置可知,当我们启动接入了pinpoint的Java应用时,会先执行PinpointBootStrap.premain()
方法:
去掉了日志等非核心逻辑代码
publicstaticvoidpremain(StringagentArgs,Instrumentationinstrumentation){//1.设置启动状态,避免重复初始化finalbooleansuccess=STATE.start();if(!success){return;}//2.初始化和解析启动参数finalJavaAgentPathResolverjavaAgentPathResolver=JavaAgentPathResolver.newJavaAgentPathResolver();finalStringagentPath=javaAgentPathResolver.resolveJavaAgentPath();finalMap<String,String>agentArgsMap=argsToMap(agentArgs);finalClassPathResolverclassPathResolver=newAgentDirBaseClassPathResolver(agentPath);//3.查找核心jar包finalAgentDirectoryagentDirectory=resolveAgentDir(classPathResolver);BootDirbootDir=agentDirectory.getBootDir();appendToBootstrapClassLoader(instrumentation,bootDir);//4.获取类加载器,加载核心jar中的类ClassLoaderparentClassLoader=getParentClassLoader();finalModuleBootLoadermoduleBootLoader=loadModuleBootLoader(instrumentation,parentClassLoader);PinpointStarterbootStrap=newPinpointStarter(parentClassLoader,agentArgsMap,agentDirectory,instrumentation,moduleBootLoader);//5.启动bootStrapif(!bootStrap.start()){logPinpointAgentLoadFail();}}
可以看到premain()方法有两个参数,最重要的是这个instrumentation
对象
Instrumentation
是Java提供的一个来自JVM的接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向classLoader的classpath下加入jar文件等,
在PinpointBootStrap.premain()
方法中,主要完成了相关jar包的查找和加载,然后将一系列配置以及instrumentation对象构造成PinpointStarter
对象,并执行start()
方法完成后续的启动:
booleanstart(){//1.读取agentId和applicationNamefinalAgentIdsagentIds=resolveAgentIds();finalStringagentId=agentIds.getAgentId();finalStringapplicationName=agentIds.getApplicationName();finalbooleanisContainer=newContainerResolver().isContainer();try{//2.解析并加载配置finalPropertiesproperties=loadProperties();ProfilerConfigprofilerConfig=newDefaultProfilerConfig(properties);//3.设置日志路径和版本信息到systemPropertysaveLogFilePath(agentDirectory);savePinpointVersion();//4.创建AgentClassLoaderURL[]urls=resolveLib(agentDirectory);finalClassLoaderagentClassLoader=createClassLoader("pinpoint.agent",urls,parentClassLoader);if(moduleBootLoader!=null){moduleBootLoader.defineAgentModule(agentClassLoader,urls);}finalStringbootClass=getBootClass();AgentBootLoaderagentBootLoader=newAgentBootLoader(bootClass,agentClassLoader);finalList<String>pluginJars=agentDirectory.getPlugins();//5.构建AgentOption,并作为参数通过反射机制构建Agent(DefaultAgent)AgentOptionoption=createAgentOption(agentId,applicationName,isContainer,profilerConfig,instrumentation,pluginJars,agentDirectory);AgentpinpointAgent=agentBootLoader.boot(option);//6.启动死锁监控线程、agent数据上报线程、注册ShutdownHookpinpointAgent.start();pinpointAgent.registerStopHandler();}catch(Exceptione){returnfalse;}returntrue;}
初始化上下文
上面过程其实还是加载配置并构建一些对象,这里面最核心的逻辑是构建Agent
对象,执行了DefaultAgent
类的构造器,初始化了上下文:
new DefaultAgent()
┆┈DefaultAgent.newApplicationContext()
┆┈┈┈new DefaultApplicationContext()
这里我们直接看DefaultApplicationContext
类的构造器中的关键逻辑:
publicDefaultApplicationContext(AgentOptionagentOption,ModuleFactorymoduleFactory){//1.获取Instrumentation对象finalInstrumentationinstrumentation=agentOption.getInstrumentation();//2.构建Guiceioc容器,用于依赖注入finalModuleapplicationContextModule=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);...}
绑定TransformCallback
Guice是谷歌开源的一个轻量级的依赖注入框架,pinpoint依靠Guice管理各种对象。
在初始化ioc容器的过程中,会遍历plugin目录下的所有插件对其进行初始化,调用过程如下:
ApplicationServerTypeProvider.get()
|—PluginContextLoadResultProvider.get()
|——new DefaultPluginContextLoadResult()
|———DefaultProfilerPluginContextLoader.load()
|————DefaultProfilerPluginContextLoader.setupPlugin()
|—————DefaultPluginSetup.setupPlugin()
|——————XxxPlugin.setup()
(具体Plugin实现)
以DubboPlugin
为例,在setup()
方法中主要对dubbo中的核心类进行转换器绑定:
@Overridepublicvoidsetup(ProfilerPluginSetupContextcontext){DubboConfigurationconfig=newDubboConfiguration(context.getConfig());...this.addTransformers();}privatevoidaddTransformers(){//为dubbo核心rpc调用类绑定Transform关系transformTemplate.transform("com.alibaba.dubbo.rpc.protocol.AbstractInvoker",AbstractInvokerTransform.class);transformTemplate.transform("com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker",AbstractProxyInvokerTransform.class);}
再来看看其中一个Transform类都做了些什么:
publicstaticclassAbstractInvokerTransformimplementsTransformCallback{@Overridepublicbyte[]doInTransform(Instrumentorinstrumentor,ClassLoaderloader,StringclassName,Class<?>classBeingRedefined,ProtectionDomainprotectionDomain,byte[]classfileBuffer)throwsInstrumentException{//指定目标类(上一步绑定的类)finalInstrumentClasstarget=instrumentor.getInstrumentClass(loader,className,classfileBuffer);//指定目标方法(方法名、参数)InstrumentMethodinvokeMethod=target.getDeclaredMethod("invoke","com.alibaba.dubbo.rpc.Invocation");if(invokeMethod!=null){//为此方法添加拦截器invokeMethod.addInterceptor(DubboConsumerInterceptor.class);}returntarget.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应用启动后,加载的就是我们增强后的类,从而实现链路监控或其他的功能。
特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢