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);
}
}
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