APM Agent 之 动态注入 agent

郦何平
2023-12-01

APM agent在sprint 17加入了“动态注入”的新特性。使得agent可以在监控的目标应用不需要重启和额外配置的情况下,就能够注入到目标应用。

agent动态注入特性支持JDK6及以上版本。

一、动态注入agent的命令:

 假设APM agent的路径是$AGENT_HOME;目标应用已经启动,其进程号(pid)是 22814;jdk路径是$JAVA_HOME。

java -Xbootclasspath/a:$JAVA_HOME/lib/tools.jar -DAPM.agentId=<agent id> -DAPM.applicationName=<app name> -DcollectorIp=<collector ip> -DlogstashIp=<logstash ip> -jar $AGENT_HOME/APMbootstrap.jar 22814

下面解释一下其原理。

动态注入agent的特性使用了从JDK6开始加入的JDK的特性“虚拟机启动后的动态 instrument”。它允许在jvm启动后注入agent。与jvm启动前在命令行中设置agent类似,jvm启动后执行的agent有一些特殊的代码和打包的要求:


   1. 必须存在agentmain入口:public static void agentmain (String agentArgs, Instrumentation inst); 或者public static void agentmain (String agentArgs); 两者都存在时,jvm选择前者执行;

  2. 必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类,例如:Agent-Class: com.test.AgentMain ;

 动态注入agent的特性使用了JDK的非标准api:Attach API。该api需要使用tools.jar,存在与$JAVA_HOME/lib/tools.jar。Attach API使得agentmain进程能够根据目标应用的pid获取到目标应用的jvm实例,并通知目标应用jvm加载agent,完成agent的注入;
动态注入agent完成后,由于APMagent是基于字节码动态修改的原理开发的,所以需要再次获取目标应用jvm已经加载的class,并再次触发修改其字节码;

二、在APM agent中实际的应用及代码摘录和解释如下:

新增入库main函数,其接受1个参数:目标应用的pid,并调用DynamicAgentLoader.load(pid)为目标进程注入agent:
public static void main(String[] args) {
    if (args == null || args.length != 1) {
        System.err.println("must be one pid");
        System.exit(1);
    }
    String pid = args[0];
    DynamicAgentLoader.load(pid);
}
 添加main方法入口时需要在META-INF/MANIFEST.MF中添加:
 Main-Class: com.navercorp.apm.bootstrap.ApmBootStrap

2.DynamicAgentLoader.load(pid)的实现如下,这个方法使用JDK的非公开Attach API获取目标应用的jvm实例,并通知目标应用jvm加载agent。由于java agent(premain和agentmain)都只接受1个外部传入的参数,并且需要自行解析参数,所以把命令行里的参数并拼接后传给agent。

public static void load(String pid) {
    ProtectionDomain protectionDomain = APMBootStrap.class.getProtectionDomain();
    CodeSource codeSource = protectionDomain.getCodeSource();
    URL location = codeSource.getLocation();
    String jarPath = location.getPath();

    String agentId = System.getProperty("APM.agentId");
    String appName = System.getProperty("APM.applicationName");
    String collectorIp = System.getProperty("collectorIp");
    String logstashIp = System.getProperty("logstashIp");
    String agentArg = agentId + "," + appName + "," + collectorIp + "," + logstashIp;
    try {
        VirtualMachine vm = VirtualMachine.attach(pid);
        vm.loadAgent(jarPath, agentArg);
        vm.detach();
        System.out.println("APMagent injected.");
    } catch (Exception e) {
        e.printStackTrace();
        System.exit(1);
    }
}
  1. 添加agentmain函数,以响应上一步的“vm.loadAgent(jarPath, agentArg);”方法。这里把上一步拼装的参数重新再解析开,并且反射调用RetransformLoadedClassesAgent完成进一步的字节码修改:
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
    DynamicAgentLoader.loadAgentArgs(agentArgs);
    printArgs(agentArgs);

    if (checkDuplicateLoadState()) {
        logAPMAgentLoadFail();
        return;
    }

    loadBootstrapCoreLib(instrumentation);
    APMStarter bootStrap = new APMStarter(agentArgs, instrumentation);
    bootStrap.start("com.navercorp.APM.profiler.RetransformLoadedClassesAgent");
}

添加agentmain入库,需要在META-INF/MANIFEST.MF中添加:

Agent-Class: com.navercorp.APM.bootstrap.APMBootStrap

4.已加载的类的字节码的修改。通过instrumentation.getAllLoadedClasses()获取已加载的类,并使用instrumentation.retransformClasses(loadedClass);来重新修改字节码:


private void retransformLoadedClasses(DefaultAgent defaultAgent) {
    Instrumentation instrumentation = defaultAgent.getInstrumentation();
    TransformerRegistry transformerRegistry = defaultAgent.getClassFileTransformerDispatcher().getTransformerRegistry();
    for (Class loadedClass : instrumentation.getAllLoadedClasses()) {
        String jvmClassName = loadedClass.getName().replaceAll("\\.", "/");
        ClassFileTransformer transformer = transformerRegistry.findTransformer(jvmClassName);
        if (transformer == null) {
            continue;
        }
        try {
            instrumentation.retransformClasses(loadedClass);
            logger.info("retransformed: " + loadedClass);
        } catch (Exception e) {
            logger.error("retransform class {} failed. Caused by {}", loadedClass, e.toString());
        }
    }
}

调用retransformClasses方法需要在META-INF/MANIFEST.MF中添加::

Can-Retransform-Classes: true

5.后续加载的类的字节码的修改。复用了原先代码中的DefaultAgent中设置ClassTransformer:


// public DefaultAgent(AgentOption agentOption, final InterceptorRegistryBinder interceptorRegistryBinder)
instrumentation.addTransformer(this.classFileTransformer, true);

三、关于premain和agentmain以及Attach API的更多说明请参见:

http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html

 类似资料: