pinpoint通过字节码增加技术(有的叫动态探针技术)来实现无侵入式的调用链采集。其核心实现原来还是基于JVM的javaagent机制来实现。pinpoint在启动时通过设置

-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar

来指定pinpoint agent加载路径,在启动的时候agent将在加载应用class文件之前做拦截并修改字节码,在class方法调用的前后加上链路采集逻辑,从而实现链路采集功能。

javaAgent的底层机制主要依赖JVMTI ,JVMTI全称JVM Tool Interface,是JVM暴露出来的一些供用户扩展的接口集合。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。但JVMTI都是一些接口合集,需要有接口的实现,这就用到了java的instrument,可以理解instrument是JVMTI的一种实现,为JVM提供外挂支持。

instrument支持启动时加载和运行时加载两种方式,分别实现JVMTI的Agent_OnLoad和Agent_OnAttach方法;pinpoint目前采用的是启动时加载方式,下面我来看pinpoint是如何实现启动时修改应用字节码的:

1. Pinpoint Agent 类必须打成jar包,然后里面的 META-INF/MAINIFEST.MF 必须包含 Premain-Class这个属性

  1.  
    Manifest-Version: 1.0
  2.  
    Premain-Class: com.navercorp.pinpoint.bootstrap.PinpointBootStrap
  3.  
    Archiver-Version: Plexus Archiver
  4.  
    Built-By: user
  5.  
    Can-Redefine-Classes: true
  6.  
    Pinpoint-Version: 1.6.0-SNAPSHOT
  7.  
    Can-Retransform-Classes: true
  8.  
    Created-By: Apache Maven 3.5.2
  9.  
    Build-Jdk: 1.8.0_152

2. 启动类中实现instrument规定的premain方法(PinpointBootStrap.java),应用在启动前会优先调用这个方法。

  1.  
    public static void premain(String agentArgs, Instrumentation instrumentation) {
  2.  
    if (agentArgs == null) {
  3.  
    agentArgs = "";
  4.  
    }
  5.  
    logger.info(ProductInfo.NAME + " agentArgs:" + agentArgs);
  6.   
  7.  
    final boolean success = STATE.start();
  8.  
    if (!success) {
  9.  
    logger.warn("pinpoint-bootstrap already started. skipping agent loading.");
  10.  
    return;
  11.  
    }
  12.  
    Map<String, String> agentArgsMap = argsToMap(agentArgs);
  13.   
  14.  
    final ClassPathResolver classPathResolver = new AgentDirBaseClassPathResolver();
  15.  
    if (!classPathResolver.verify()) {
  16.  
    logger.warn("Agent Directory Verify fail. skipping agent loading.");
  17.  
    logPinpointAgentLoadFail();
  18.  
    return;
  19.  
    }
  20.   
  21.  
    BootstrapJarFile bootstrapJarFile = classPathResolver.getBootstrapJarFile();
  22.  
    appendToBootstrapClassLoader(instrumentation, bootstrapJarFile);
  23.   
  24.   
  25.  
    PinpointStarter bootStrap = new PinpointStarter(agentArgsMap, bootstrapJarFile, classPathResolver, instrumentation);
  26.  
    if (!bootStrap.start()) {
  27.  
    logPinpointAgentLoadFail();
  28.  
    }
  29.   
  30.  
    }

agentArgs 是 premain 函数得到的程序参数,随同 “-javaagent”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串;pinpoint中的agentArgs就是pinpoint的jar包。

instrumentation 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。

3. 跟踪premain代码主要调用PinpointStarter.start()方法

  1.  
    boolean start() {
  2.  
    /**省略代码*/
  3.   
  4.  
    try {
  5.  
    // Is it right to load the configuration in the bootstrap?
  6.  
    ProfilerConfig profilerConfig = DefaultProfilerConfig.load(configPath);
  7.   
  8.  
    // this is the library list that must be loaded
  9.  
    List<URL> libUrlList = resolveLib(classPathResolver);
  10.  
    AgentClassLoader agentClassLoader = new AgentClassLoader(libUrlList.toArray(new URL[libUrlList.size()]));
  11.  
    final String bootClass = getBootClass();
  12.  
    agentClassLoader.setBootClass(bootClass);
  13.  
    logger.info("pinpoint agent [" + bootClass + "] starting...");
  14.   
  15.  
    AgentOption option = createAgentOption(agentId, applicationName, profilerConfig, instrumentation, pluginJars, bootstrapJarFile, serviceTypeRegistryService, annotationKeyRegistryService);
  16.  
    Agent pinpointAgent = agentClassLoader.boot(option);
  17.  
    pinpointAgent.start();
  18.  
    registerShutdownHook(pinpointAgent);
  19.  
    logger.info("pinpoint agent started normally.");
  20.  
    } catch (Exception e) {
  21.  
    // unexpected exception that did not be checked above
  22.  
    logger.warn(ProductInfo.NAME + " start failed.", e);
  23.  
    return false;
  24.  
    }
  25.  
    return true;
  26. }

4. 继续跟踪代码主要调用pinpointAgent.start()方法,而pinpointAgent在此处的实现类是DefaultAgent.

  1.  
    public DefaultAgent(AgentOption agentOption, final InterceptorRegistryBinder interceptorRegistryBinder) {
  2.   
  3.  
    /**省略代码*/
  4.   
  5.  
    this.profilerConfig = agentOption.getProfilerConfig();
  6.  
    this.instrumentation = agentOption.getInstrumentation();
  7.  
    this.agentOption = agentOption;
  8.   
  9.  
    //默认使用ASM字节码引擎
  10.  
    this.classPool = createInstrumentEngine(agentOption, interceptorRegistryBinder);
  11.   
  12.  
    if (logger.isInfoEnabled()) {
  13.  
    logger.info("DefaultAgent classLoader:{}", this.getClass().getClassLoader());
  14.  
    }
  15.   
  16.  
    //加载转换class字节码逻辑的插件,主要逻辑是将各个插件需要转换的目标class拦截器(拦截器是pinpoint自身封装的一类方法)存入List<ClassFileTransformer>中
  17.  
    pluginContexts = loadPlugins(agentOption);
  18.  
    //将List<ClassFileTransformer>转成HashMap<className,ClassFileTransformer>
  19.  
    this.classFileTransformer = new ClassFileTransformerDispatcher(this, pluginContexts);
  20.  
    this.dynamicTransformService = new DynamicTransformService(instrumentation, classFileTransformer);
  21.   
  22.  
    ClassFileTransformer wrappedTransformer = wrapClassFileTransformer(classFileTransformer);
  23.  
    //调用jvm instrumentation工具方法
  24.  
    instrumentation.addTransformer(wrappedTransformer, true);
  25.   
  26.   
  27.  
    /**省略代码*/
  28.  
    }

可以看出,addTransformer 方法并没有指明要转换哪个类,此时并未发生实际的字节码转换。转换发生在 premain 函数执行之后,main 函数执行之前,这时每装载一个类,transform 方法就会执行一次,看看是否需要转换,此时wrappedTransformer最终会通过dynamicTransformService封装的HashMap<className,classFileTransformer>来判断当前加载的应用类是否需要转换。

最后我们通过一幅图来展示pinpoint的字节码转换过程:

1. JVM初始化并通过System ClassLoader加载Pinpoint Agent类;创建Instrumentation接口实例并调用Pinpoint Agent的Premain方法,并自动传入Instrumentation实例;

2. Pinpoint Agent加载plugins插件,将其中的transformer类注册到Instrumentation实例中;

3. System ClassLoader加载其他的应用Java类,此时将调用注册的transformer方法对要加载的java类进行字节码转换;

4. JVM将转换后的class放入方法区。

 

转自https://www.freesion.com/article/4853167805/

 posted on 2022-04-26 15:42  归鸿藏宝阁  阅读(139)  评论(0)    收藏  举报