前言
经常在应用的启动或者运行过程中需要动态的查看数据,或者实时的验证我们写的代码的结构与执行过程,此时需要一种工具能够动态的检测程序运行的状态,内存数据,线程情况,最好能够动态的替换代码实时生效,方便我们从日志或者其他埋点断言我们的猜测。
1. arthas 阿尔萨斯的工程结构
其实有很多工具可以达到这种效果,arthas就是其中一种。
从工程结构,其实arthas的核心功能是core,里面有arthas的attach与诊断指令的代码。 通过实际启动分析进一步看原理。
2. arthas 启动
2.1 打包
对源码去除
git-commit-id-plugin
插件,毕竟现在github已经很难连接了
执行mvn clean package,在packing module下
src下面其实有
assembly.xml
文件定义了打包的详情,每个module定义了打包的插件,毕竟诊断工具需要把所有第三方的jar class字节码打进jar,即fatjar,所以对依赖需要尽量少,观源码arthas重度依赖Telnet netty,感觉依赖有点重。
2.2 执行boot启动
boot的启动是执行java -jar,其实就是一个普通的jar应用
2.2.1 选择进程pid
pid = ProcessUtils.select(bootstrap.isVerbose(), telnetPortPid, bootstrap.getSelect());
其实很简单,就是去找java home,找到jps命令,然后jps -l
可以看到findJps
查找本机jvm进程
2.2.2 启动进程attach pid
ProcessUtils.startArthasCore(pid, attachArgs);
不明白为啥要独立启动一个进程去attach,这个进程在attach完成后自动运行结束。参数就是前面的pid core agent等,其实核心是pid agent jar,其他都是额外功能的。
public static void startArthasCore(long targetPid, List<String> attachArgs) {
// find java/java.exe, then try to find tools.jar
String javaHome = findJavaHome();
// find java/java.exe
File javaPath = findJava(javaHome);
if (javaPath == null) {
throw new IllegalArgumentException(
"Can not find java/java.exe executable file under java home: " + javaHome);
}
File toolsJar = findToolsJar(javaHome);
if (JavaVersionUtils.isLessThanJava9()) {
if (toolsJar == null || !toolsJar.exists()) {
throw new IllegalArgumentException("Can not find tools.jar under java home: " + javaHome);
}
}
List<String> command = new ArrayList<String>();
command.add(javaPath.getAbsolutePath());
if (toolsJar != null && toolsJar.exists()) {
command.add("-Xbootclasspath/a:" + toolsJar.getAbsolutePath());
}
command.addAll(attachArgs);
// "${JAVA_HOME}"/bin/java \
// ${opts} \
// -jar "${arthas_lib_dir}/arthas-core.jar" \
// -pid ${TARGET_PID} \
// -target-ip ${TARGET_IP} \
// -telnet-port ${TELNET_PORT} \
// -http-port ${HTTP_PORT} \
// -core "${arthas_lib_dir}/arthas-core.jar" \
// -agent "${arthas_lib_dir}/arthas-agent.jar"
ProcessBuilder pb = new ProcessBuilder(command);
try {
final Process proc = pb.start();
这里严重依赖tools.jar,因为使用了里面虚拟机的attach方法
启动里面的arthas-core.jar
那么执行com.taobao.arthas.core.Arthas
源码分析,VirtualMachine即tools的能力,所以前面需要查找tools.jar
private void attachAgent(Configure configure) throws Exception {
VirtualMachineDescriptor virtualMachineDescriptor = null;
//VirtualMachine.list() 相当于jps -lv的能力
for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
String pid = descriptor.id();
if (pid.equals(Long.toString(configure.getJavaPid()))) {
virtualMachineDescriptor = descriptor;
break;
}
}
VirtualMachine virtualMachine = null;
try {
if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 这种方式
virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
} else {
virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
}
Properties targetSystemProperties = virtualMachine.getSystemProperties();
String targetJavaVersion = JavaVersionUtils.javaVersionStr(targetSystemProperties);
String currentJavaVersion = JavaVersionUtils.javaVersionStr();
if (targetJavaVersion != null && currentJavaVersion != null) {
if (!targetJavaVersion.equals(currentJavaVersion)) {
AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail.",
currentJavaVersion, targetJavaVersion);
AnsiLog.warn("Target VM JAVA_HOME is {}, arthas-boot JAVA_HOME is {}, try to set the same JAVA_HOME.",
targetSystemProperties.getProperty("java.home"), System.getProperty("java.home"));
}
}
String arthasAgentPath = configure.getArthasAgent();
//convert jar path to unicode string
configure.setArthasAgent(encodeArg(arthasAgentPath));
configure.setArthasCore(encodeArg(configure.getArthasCore()));
//载入jar
virtualMachine.loadAgent(arthasAgentPath,
configure.getArthasCore() + ";" + configure.toString());
} finally {
if (null != virtualMachine) {
//attach 完成后需要通知结束
virtualMachine.detach();
}
}
}
2.2.3 attach后处理
attach pid后,会loadAgent,加载agent的jar
定义了
Premain-Class、Agent-Class、Can-Redefine-Classes、Can-Retransform-Classes
Premain-Class、Agent-Class定义执行的main方法: Agent-Class是attach的方式;Premain-Class是agent随启动的执行方式
Can-Redefine-Classes、Can-Retransform-Classes 定义字节码增强的开关
获取classloader,然后bind,先看classloader
private static ClassLoader getClassLoader(Instrumentation inst, File arthasCoreJarFile) throws Throwable {
// 构造自定义的类加载器,尽量减少Arthas对现有工程的侵蚀
return loadOrDefineClassLoader(arthasCoreJarFile);
}
private static ClassLoader loadOrDefineClassLoader(File arthasCoreJarFile) throws Throwable {
if (arthasClassLoader == null) {
arthasClassLoader = new ArthasClassloader(new URL[]{arthasCoreJarFile.toURI().toURL()});
}
return arthasClassLoader;
}
其实就是自定义classloader,载入jar包
反射创建
ArthasBootstrap实例
private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) throws Throwable {
/**
* <pre>
* ArthasBootstrap bootstrap = ArthasBootstrap.getInstance(inst);
* </pre>
*/
Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP);
Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, Instrumentation.class, String.class).invoke(null, inst, args);
boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap);
if (!isBind) {
String errorMsg = "Arthas server port binding failed! Please check $HOME/logs/arthas/arthas.log for more details.";
ps.println(errorMsg);
throw new RuntimeException(errorMsg);
}
ps.println("Arthas server already bind.");
}
其实就是new 对象的时候,干些初始化的事情
/**
* 单例
*
* @param instrumentation JVM增强
* @return ArthasServer单例
* @throws Throwable
*/
public synchronized static ArthasBootstrap getInstance(Instrumentation instrumentation, Map<String, String> args) throws Throwable {
if (arthasBootstrap == null) {
arthasBootstrap = new ArthasBootstrap(instrumentation, args);
}
return arthasBootstrap;
}
进一步跟踪
private ArthasBootstrap(Instrumentation instrumentation, Map<String, String> args) throws Throwable {
this.instrumentation = instrumentation;
//新版才加入的,不明白为啥加入fastjson
initFastjson();
// 1. initSpy() 其实就是加载java.arthas.SpyAPI的class对象
initSpy();
// 2. ArthasEnvironment,扣的Spring环境的源码
initArthasEnvironment(args);
//输出路径
String outputPathStr = configure.getOutputPath();
if (outputPathStr == null) {
outputPathStr = ArthasConstants.ARTHAS_OUTPUT;
}
outputPath = new File(outputPathStr);
outputPath.mkdirs();
// 3. init logger
loggerContext = LogUtil.initLooger(arthasEnvironment);
// 4. 增强ClassLoader,初始化
//instrumentation.addTransformer(classLoaderInstrumentTransformer, true);
enhanceClassLoader();
// 5. init beans ResultViewResolver HistoryManagerImpl 结果解析器与历史记录
initBeans();
// 6. start agent server
// 顾名思义,绑定shellServer 创建http Telnet的链接
bind(configure);
executorService = Executors.newScheduledThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
final Thread t = new Thread(r, "arthas-command-execute");
t.setDaemon(true);
return t;
}
});
shutdown = new Thread("as-shutdown-hooker") {
@Override
public void run() {
ArthasBootstrap.this.destroy();
}
};
//非常关键,字节码增强使用
transformerManager = new TransformerManager(instrumentation);
Runtime.getRuntime().addShutdownHook(shutdown);
}
环境信息的源码,其实是Spring的源码
关键的字节码增强初始化
public TransformerManager(Instrumentation instrumentation) {
this.instrumentation = instrumentation;
classFileTransformer = new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
for (ClassFileTransformer classFileTransformer : reTransformers) {
byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
protectionDomain, classfileBuffer);
if (transformResult != null) {
classfileBuffer = transformResult;
}
}
for (ClassFileTransformer classFileTransformer : watchTransformers) {
byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
protectionDomain, classfileBuffer);
if (transformResult != null) {
classfileBuffer = transformResult;
}
}
for (ClassFileTransformer classFileTransformer : traceTransformers) {
byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined,
protectionDomain, classfileBuffer);
if (transformResult != null) {
classfileBuffer = transformResult;
}
}
return classfileBuffer;
}
};
instrumentation.addTransformer(classFileTransformer, true);
}
instrumentation.addTransformer(classFileTransformer, true);
至此结束,其实原理很简单:attach 然后初始化Telnet与http服务,通过addTransformer来动态字节码增强,client端连接上去,然后发指令。
3. arthas的原理
基于Instrumentation的产品,除了arthas,常用的还有
pinpoint、skywalking
这些非常有名气 的产品。Instrumentation非常关键的API
arthas的关键原理是Javaagent,有2种方式
1.在 JVM 启动的时加载 JDK5开始支持
使用javaagent VM参数 java -javaagent:xxxagent.jar xxx,这种方式在 main 方法之前执行 agent 中的 premain 方法
public static void premain(String agentArgument, Instrumentation instrumentation) throws Exception
2.在 JVM 启动后 Attach JDK6开始支持
通过 Attach API 进行加载,在进程存在的时候,动态attach,这种方式会在 agent 加载以后执行 agentmain 方法
public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception
且必须加上maven插件参数,其他方式代码管理同理
<manifestEntries>
<Premain-Class>com.taobao.arthas.agent334.AgentBootstrap</Premain-Class>
<Agent-Class>com.taobao.arthas.agent334.AgentBootstrap</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
总结
arthas其实原理很简单,使用Javaagent技术,其实debug模式也是使用这种技术。arthas增强了字节码,写了一些native方法获取jvm的堆等信息。从源码看,不知道为啥使用telnet协议,重度依赖netty termd,为啥不使用简单的HTTP协议,无状态降低依赖,而且方便前端图形化,admin端目前是telnet透传。估计设计之初就认为是敲命令吧,但是有没有联想能力,敲命令还是很费时,需要学习。