agent启动、加载、拦截织入
所有的代码分析基于pinpoint 1.6.2
pinpoint agent使用方式为-javaagent:$PINPOINT_PATH/pinpoint-bootstrap-{version}.jar
,故从javaagent开始。
一、jvm会调用PinpointBootStrap.premain
方法,此方法做了如下操作:
- 将状态标志为启动(即同一jvm只能启动一次)
- 校验核心jar包
- 从classpath查找pinpoint-bootstrap-{version}.jar
- 解析pinpoint-bootstrap-{version}.jar所在文件夹$PINPOINT_PATH
- 从$PINPOINT_PATH/boot下查找核心jar,包含如下jar:
- pinpoint-commons.jar
- pinpoint-bootstrap-core.jar
- pinpoint-bootstrap-core-optional.jar
- pinpoint-annotations.jar
- 将核心jar包加入到jvm的BootstrapClassLoader中(因为classpath中只指定了pinpoint-bootstrap-{version}.jar,但是没有指定其他的jar,所以需要load进来)
- 调用PinpointStarter的start方法
二、PinpointStarter start主要做了如下操作:
-
获取pinpoint.agentId,格式:[a-zA-Z0-9\._\-]+ 最大24位
-
获取pinpoint.applicationName与pinpoint.agentId约束一样
-
获取$PINPOINT_PATH/plugin下面所有的jar
-
采用java通用的ServiceLoader.load(TraceMetadataProvider, PluginLoaderClassLoader)方式,从plugin下加载TraceMetadataProvider的实现类。
- 这里,使用的classloader是pinpoint自己的PluginLoaderClassLoader,其父classloader为AppClassLoader, 为了避免污染业务应用类。
- 而service类型是TraceMetadataProvider,关于java通用ServiceLoader的介绍参见。
- 调用所有的plugin的TraceMetadataProvider的setup实现
代码如下:
TraceMetadataLoaderService typeLoaderService = new DefaultTraceMetadataLoaderService(pluginJars, loggerFactory);
-
将pinpoint系统默认的ServiceType和各个plugin的ServiceType注册到pinpoint中(实际持有类为ServiceTypeRegistryService)
代码如下:
ServiceTypeRegistryService serviceTypeRegistryService = new DefaultServiceTypeRegistryService(typeLoaderService, loggerFactory);
-
将plugin的AnnotationKey注册到pinpoint中
1.TraceMetadataProvider 是pinpoint的插件开发时必须实现的接口,它提供的trace的基本信息。
2.ServiceType代表的是哪个lib需要被trace。
3.AnnotationKey用于定义参数,返回结果啥的
可以参考dubbo的具体实现:
public class DubboTraceMetadataProvider implements TraceMetadataProvider { @Override public void setup(TraceMetadataSetupContext context) { context.addServiceType(DubboConstants.DUBBO_PROVIDER_SERVICE_TYPE); context.addServiceType(DubboConstants.DUBBO_CONSUMER_SERVICE_TYPE); context.addAnnotationKey(DubboConstants.DUBBO_ARGS_ANNOTATION_KEY); context.addAnnotationKey(DubboConstants.DUBBO_RESULT_ANNOTATION_KEY); } }
pinpoint官方定义参考:https://naver.github.io/pinpoint/plugindevguide.html
-
加载pinpoint.config
-
使用自己实现的ParallelCapablePinpointURLClassLoader来并行加载$PINPOINT_PATH/lib下的jar,同样不会污染业务应用类。
-
将上面所有的信息封装为AgentOption,传递给DefaultAgent用于初始化。
-
调用DefaultAgent的start方法
三、关于classloader
pinpoint启动过程中涉及到好几处的classloader,而jvm是典型的双亲委派模型:
System Classloader -> Extensions Classloader -> Bootstrap Classloader
这里来看一下pinpont的classloader的整个结构图:
- pinpoint-bootstrap.jar为启动类,由System Classloader加载,它负责加载其他的jar包。
- $PINPOINT_PATH/boot有Bootstrap Classloader加载,以便在整个jvm中使用
- plugin和lib只是pinpoint自己追踪使用,不会对应用类可见。
- 这里有两点值得注意一下(上图中标红的虚线),pinpoint加载外部jar的两种方式。
四、DefaultAgent
继续梳理主流程,DefaultAgent在lib包中,其初始化的时候主要初始化了一个DefaultApplicationContext对象,
并且会调用start方法,也是调用DefaultApplicationContext的start,那么下面主要来说DefaultApplicationContext。
其初始化流程中主要步骤如下:
-
创建ApplicationContextModule,此类中初始化了很多的对象,这里不再赘述。
-
调用jdk的Instrumentation.addTransformer将DefaultClassFileTransformerDispatcher(这个对象后面再说)设置进去
该方法addTransformer就是注册了一个转换器。其作用就是加载class的时候,会经过该转换器转换。
-
初始化agentinfo发送器
-
初始化agentStat监视器
start方法如下:
-
agentinfo发送器启动
其发送频率为启动时及之后每5分钟一次,主要发送的内容有ip,hostname,agentid,applicationName,pid,启动时间,serverType,jvm版本,agentVersion等静态信息
采用TCP发送
-
agentStat监视器启动
收集频率频率为5秒一次,收集6次发送一次,故30秒发送一次。
这里的agentStat是指应用的状态:包括gc,cpuload,事务量(trace量),活跃Trace量,数据源统计。
采用UDP发送
详细的类图参考如下:
五、DefaultClassFileTransformerDispatcher
从上面的流程可以知道只有一个可以改变字节码的地方,就是DefaultClassFileTransformerDispatcher。
那么,它就是代码织入的关键了。下面来分析一下它的核心transform方法的具体实现:
-
过滤
- classloader为pinpoint自己定义的,不转换
- class在com.navercorp.pinpoint包下的不转换
-
动态转换:暂不清楚(貌似没用)
-
过滤:java或javax包下的不转换
-
根据classname获取ClassFileTransformer,并进行转换(jdk的ClassFileTransformer.transform)
这里的
根据classname获取ClassFileTransformer
从哪来的?这就需要看该类的初始化方法了。
该类的初始化方法有如下代码:
this.transformerRegistry = createTransformerRegistry(pluginContextLoadResult);
而this.transformerRegistry中持有了Map<String, ClassFileTransformer>
属性,故这些ClassFileTransformer都来自于PluginContextLoadResult,这里通过一个时序图来看一下ClassFileTransformer是如何构造到PluginContextLoadResult中的:
上面一个大概的流程就是会load plugin下的所有ProfilerPlugin的实现,并为其注入TransformTemplate,然后调用其setup方法,这样所有的ProfilerPlugin的实现类即可将自己的TransformCallback设置到ClassFileTransformerLoader中,供真正transform的时候进行回调,进而改变字节码。
接着上面的核心transform方法,继续分析接下来的流程,调用jdk的ClassFileTransformer.transform后:
这里已经知道,所谓的ClassFileTransformer实际的实现为MatchableClassFileTransformerGuardDelegate(即时序图的最后一个类)。
-
其transform方法实现为
final GuardInstrumentor guard = new GuardInstrumentor(this.profilerConfig, this.instrumentContext); try { // WARN external plugin api return transformCallback.doInTransform(guard, loader, className, classBeingRedefined, protectionDomain, classfileBuffer); } catch (InstrumentException e) { throw new PinpointException(e); } finally { guard.close(); }
即回调各个ProfilerPlugin的TransformCallback进行类转换。
-
接着看具体的回调方法,各个ProfilerPlugin的回调方法都需要拿到当前的类
InstrumentClass target = instrumentor.getInstrumentClass(loader, className, classfileBuffer)
此时的instrumentor其实是PluginInstrumentContext,那么来看他的getInstrumentClass实现
-
PluginInstrumentContext.getInstrumentClass
InstrumentEngine instrumentEngine = getInstrumentEngine(); return instrumentEngine.getClass(this, classLoader, className, classFileBuffer);
它将实现委托给了InstrumentEngine,默认的Engine为ASMEngine。
-
ASMEngine底层使用ASM来对二进制字节码进行操作,实现了方法级别的前置和后置代码织入。
到这里,整个agent的启动及代码加载和拦截织入分析完毕