Java在线诊断利器之Arthas
2021-03-20 09:27
标签:property 支持 第三方 code bat null tip 提交 lse Arthas是阿里在2019年9月份开源的一款java在线诊断工具,能够分析、诊断、定位java应用问题,例如:JVM信息、线程信息、搜索类中的方法、 跟踪代码执行、观测方法的入参和返回参数等等。 Arthas最大的特点是能在不修改代码和不需要重新发布的情况下,对业务问题进行诊断,包括查看方法调用的出参入参、异常、监测方法执行耗时、类加载信息等,大大提升线上问题排查效率。 目前的arthas版本都是基于命令行的交互方式,所以下面会按照上面的适用场景列出一些重要和常用的命令,全部命令请查看官方安装。 这里有一个坑,如果在widows环境安装,本地之前安装了多个版本的jdk,在Attach到目标进程时有可能会提示tools.jar包找不到的异常,如下图(没有这个问题可以忽略): 因为Arthas使用了非系统环境变量版本的jdk运行自身,而不是环境变量 全部命令请查看官方文档: Arthas用户文档 Arthas正是使用Java的 源码部分目前只列出主要实现, 一些细节来不及看, 感兴趣的可以自己去git上下载下来看 github.com/alibaba/art… 根据官网入门手册里的 这是java的一种机制, 告知jdk jar包执行入口通过.MF, 具体可参考 下面是引导程序Bootstrap的入口 通过上面的 下面的 这样就建立了连接,在运行前或者运行时,将自定义的 Agent加载并和 VM 进行通信 然后是arthas-agent.jar代理包的MANIFEST.MF文件, 该jar已经被第一步arthas-boot.jar里的 接下来就看core核心包里的 剩下的就可以看下常用的命令是怎么实现逻辑了, 比如 最后一个 总结: 通过上面的代码分析我们知道了JDK的这两项功能: Arthas的整体逻辑也是在jdk的 然后将对应的Advice织入进去,对于类的查找,方法的查找,都是通过 这些机制在以后的工作中如果遇到类似的问题也会给我们带来启发, 嗯, 文章来源:javakk.com/153.html git地址:github.com/alibaba/art… 官方文档:alibaba.github.io/arthas/inde… Java在线诊断利器之Arthas 标签:property 支持 第三方 code bat null tip 提交 lse 原文地址:https://www.cnblogs.com/maoyx/p/13929528.html一. 简介
二. 适用场景
三. 安装使用
JAVA_HOME
设置的jdk,可以先切换到JAVA_HOME
设置的目录,然后再运行 java -jar arthas-boot.jar
即可,这个算是arthas的一个bug,后续版本会优化掉。四. 常用指令
watch
命令(观察指定方法的调用情况,包括返回值、异常、入参、对象属性值)watch
命令还可以根据耗时和具体的入参条件筛选过滤,只要符合Ognl语法,可以满足很多监控维度,如:基于Ognl的一些特殊语法trace
命令(方法内部调用路径,并输出方法路径上的每个节点上耗时),该命令主要用于统计整个调用链路上的所有性能开销和追踪调用链路,使用下来感觉这个命令也是很有用的,包括本地环境,尤其是要排查接口响应时间慢这样的场景下,可以快速定位到具体哪个方法或哪些方法导致的,甚至包括第三方jar包的方法stack
命令(输出当前方法被调用的路径),同样也可以查看依赖的jar里的方法被谁调用tt
命令(time tunnel 时间轴,记录下指定方法每次调用的入参和返回信息),相当于watch
指令的多次记录)但watch
命令需要提前观察并拼写表达式,tt
则不需要,这里着重说下 -n 参数,当你执行一个调用量不高的方法时可能你还能有足够的时间用 CTRL+C
中断 tt 命令记录的过程,但如果遇到调用量非常大的方法,瞬间就能将你的 JVM 内存撑爆,当我们改了问题后,比如改了配置,需要在线上测试下是否修复的时候,可能会用到该功能,因为环境和数据的问题本地可能无法验证,但线上环境不可能让用户再调用一次,所以这个参数 -p 就可以再重新发起一次调用。但是是由阿尔萨斯内部发起的线程实现的,所以调用方不一样,而且如果之前的调用数据有从threaLocal
里获取的话,这次调用代码里也无法获取,使用时需要注意。其实最重要的还是要结合实际场景,因为线上真实环境去模拟用户再次发起调用如果牵涉到下单或支付流程的话还是要慎重的,否则可能引起一些非幂等的后果。jobs
后台异步任务命令,当线上出现偶发的问题时,比如需要watch
某个条件,而这个条件一天可能才会出现一次时,这种情况可以使用异步任务将命令在后台运行,而且可以保存到指定的文件,方便查看。这里需要注意:使用异步任务时,请勿同时开启过多的后台异步命令,以免对目标JVM性能造成影响redefine
命令(加载外部的.class文件),类似于热加载或热修复的功能,修改java文件后,将替换掉jvm已加载的class类,但是因为jdk本身的限制,修改的class文件里不允许新增加成员变量和方法。基于这个功能可以模拟一个简单的监控功能,比如在java文件的某个方法里加上调用耗时和请求参数的打印功能,然后使用redefine
即可看到该方法的耗时时间和参数值,并且不用重启服务。jad
命令(反编译指定已加载类的源码,可以查看部署在线上服务器的.class文件对应的java源码),该功能基于一个第三方的反编译工具CFR实现五. 实现原理
sun.instrument.InstrumentationImpl
通过instrument
机制]的实现可以构建一个独立于应用程序的代理程序Agent,再结合attach机制来绑定我们的应用程序的pid就可以实现监控和协助运行在JVM上的程序,还可以替换和修改类的定义(主要通过redefine
,addTransformer
函数),比如实现虚拟机级别支持的AOP实现方式。attach机制可以提供一种jvm进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行内部的一些操作,instrument
和AttachAPI
是btrace,greys,arthas等监控工具的原理基础。instrument
底层就是基于此实现的,JVMTI提供了可用于 debug 和 profiler 的接口,在 Java 5/6 中,虚拟机接口也增加了监听(Monitoring),线程分析(Thread analysis)以及覆盖率分析(Coverage Analysis)等功能。正是由于 JVMTI的强大功能,它是实现 Java 调试器,以及其它 Java 运行态测试与分析工具的基础,Instrumentation
底层也是基于JVMTI实现的。另外还有Eclipse,IntellJ Idea 等编译期的debug功能都是基于JPDA(Java Platform Debugger Architecture)实现的,如下图:Instrumentation
特性,结合ASM等第三方字节码操作框架的动态增强功能来实现的(核心功能实现在 com.taobao.arthas.core.advisor.Enhancer enhance()
方法中)六. 源码分析
java -jar arthas-boot.jar
可知程序入口在这个jar包下, 查看META-INF下的MANIFEST.MF文件可知(SPI机制)java.util.ServiceLoader
实现, 感兴趣的也可以了解下 SPI 机制main
方法, 只列出主要代码逻辑, 可对照源码查看, 下面的所有代码分析中加注释"//"说明的都是关键地方public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException, ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
...... 省略部分代码AnsiLog.info("Try to attach process " + pid);
AnsiLog.debug("Start arthas-core.jar args: " + attachArgs);
ProcessUtils.startArthasCore(pid, attachArgs); //加载arthas-agent.jar和arthas-core.jar, startArthasCore方法主要是利用了tool.jar这个包中的VirtualMachine.attach(pid)来实现
AnsiLog.info("Attach process {} success.", new Object[]{pid});
......
Class> telnetConsoleClas = classLoader.loadClass("com.taobao.arthas.client.TelnetConsole"); //通过反射机制调用控制台命令行交互
Method mainMethod = telnetConsoleClas.getMethod("main", String[].class); //TelnetConsole用到了JLine工具, JLine是一个用来处理控制台输入的Java类库,可以轻松实现Java命令行输入
}
startArthasCore()
方法内部ProcessBuilder
类调用 arthas-core.jar 的进程服务, 下面就是arthas-core.jar包和入口执行类, 同样也可以通过查看MANIFEST.MF获得,attachAgent
方法正是使用了tool.jar这个包中的VirtualMachine.attach(pid)
来实现,同时上面加载了自定义的agent代理,见下面 virtualMachine.loadAgent
,Main-Class: com.taobao.arthas.core.Arthas
--------------------------------------------------------------------------
private void attachAgent(Configure configure) throws Exception {
VirtualMachineDescriptor virtualMachineDescriptor = null;
Iterator var3 = VirtualMachine.list().iterator();
String targetJavaVersion;
while(var3.hasNext()) {
VirtualMachineDescriptor descriptor = (VirtualMachineDescriptor)var3.next();
targetJavaVersion = descriptor.id();
if (targetJavaVersion.equals(Integer.toString(configure.getJavaPid()))) {
virtualMachineDescriptor = descriptor;
}
}
VirtualMachine virtualMachine = null;
try {
if (null == virtualMachineDescriptor) {
virtualMachine = VirtualMachine.attach("" + configure.getJavaPid()); //核心功能正是调用了com.sun.tools.attach.VirtualMachine类, 底层又调用了WindowsAttachProvider类, 这个类又是调用jdk的native方法实现的
} else {
virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
}
Properties targetSystemProperties = virtualMachine.getSystemProperties();
targetJavaVersion = targetSystemProperties.getProperty("java.specification.version");
String currentJavaVersion = System.getProperty("java.specification.version");
if (targetJavaVersion != null && currentJavaVersion != null && !targetJavaVersion.equals(currentJavaVersion)) {
AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail.", new Object[]{currentJavaVersion, targetJavaVersion});
AnsiLog.warn("Target VM JAVA_HOME is {}, try to set the same JAVA_HOME.", new Object[]{targetSystemProperties.getProperty("java.home")});
}
virtualMachine.loadAgent(configure.getArthasAgent(), configure.getArthasCore() + ";" + configure.toString()); //这里通过loadAgent将我们自定义的Agent(arthas-core.jar)加载并和我们应用程序所在的JVM进行通信
} finally {
if (null != virtualMachine) {
virtualMachine.detach();
}
}
}
ProcessUtils.startArthasCore
方法加载Manifest-Version: 1.0
Premain-Class: com.taobao.arthas.agent.AgentBootstrap //jdk5的intrument机制,只能支持jvm启动前指定监控的类
Built-By: hengyunabc
Agent-Class: com.taobao.arthas.agent.AgentBootstrap //jdk6之后对intrument机制改进,可以在jvm启动后实时修改类,arthas的很多功能都是通过这个设置生效的
Can-Redefine-Classes: true //重新定义类, 正如上面介绍的redefine -p 指令一样, 通过这个属性设置告知jvm
Can-Retransform-Classes: true //转换类, watch, trace, monitor等命令都是动态修改类, 和Redefine-Classes的区别是直接在现有加载的class字节码基础上修改, 不需要一个新的class文件替换
Created-By: Apache Maven 3.5.3
Build-Jdk: 1.8.0_181
--------------------------------------------------------------------------
public static void premain(String args, Instrumentation inst) { //同上,main方法执行前,jdk5的intrument机制, 这里你已经拿到了Instrumentation对象实例
main(args, inst);
}
public static void agentmain(String args, Instrumentation inst) { //main执行后, jdk6的intrument机制, 这里你已经拿到了Instrumentation对象实例
main(args, inst);
}
private static synchronized void main(String args, final Instrumentation inst) {
try {
ps.println("Arthas server agent start...");
int index = args.indexOf(59);
String agentJar = args.substring(0, index);
final String agentArgs = args.substring(index, args.length());
File agentJarFile = new File(agentJar); //拿到arthas-agent.jar
if (!agentJarFile.exists()) {
ps.println("Agent jar file does not exist: " + agentJarFile);
} else {
File spyJarFile = new File(agentJarFile.getParentFile(), "arthas-spy.jar"); //拿到arthas-spy.jar, spy里面主要是些钩子类,基于aop有前置方法,后置方法,这样动态增强类,实现相应command功能
if (!spyJarFile.exists()) {
ps.println("Spy jar file does not exist: " + spyJarFile);
} else {
final ClassLoader agentLoader = getClassLoader(inst, spyJarFile, agentJarFile); //类加载器加载agent和spy, 具体见下面的getClassLoader方法解析
initSpy(agentLoader); //初始化钩子,这里面主要是通过反射的方式获取AdviceWeaver编织类, 比如前置方法,后置方法, 并配合asm实现类的动态增强
Thread bindingThread = new Thread() {
public void run() {
try {
AgentBootstrap.bind(inst, agentLoader, agentArgs); //bind方法又通过反射调用了arthas-core.jar的ArthasBootstrap.bind方法, bind方法这里就不列出了, 可以自己看下
} catch (Throwable var2) {
var2.printStackTrace(AgentBootstrap.ps);
}
}
};
bindingThread.setName("arthas-binding-thread");
bindingThread.start();
bindingThread.join();
}
}
} catch (Throwable var10) {
var10.printStackTrace(ps);
try {
if (ps != System.err) {
ps.close();
}
} catch (Throwable var9) {
;
}
throw new RuntimeException(var10);
}
}
private static ClassLoader getClassLoader(Instrumentation inst, File spyJarFile, File agentJarFile) throws Throwable {
inst.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile)); //这里把spy添加到jdk的启动类加载器里, 就是我们熟知的BootstrapClassLoader加载, 这样做的目的是为了下面的子加载器能共享spy, 我理解可能是很多命令都不是实时返回的,需要异步获取
return loadOrDefineClassLoader(agentJarFile); //而agent是交给arthas自定义的classLoader加载的, 这样做的目的应该是不对我们的业务代码侵入
}
AgentBootstrap.bind
方法做了什么public void bind(Configure configure) throws Throwable {
long start = System.currentTimeMillis();
if (!this.isBindRef.compareAndSet(false, true)) {
throw new IllegalStateException("already bind");
} else {
try {
ShellServerOptions options = (new ShellServerOptions()).setInstrumentation(this.instrumentation).setPid(this.pid).setSessionTimeout(configure.getSessionTimeout() * 1000L);
this.shellServer = new ShellServerImpl(options, this); //ShellServer服务初始化, 应该就是我们的命令行窗口服务
BuiltinCommandPack builtinCommands = new BuiltinCommandPack(); //这一步就是初始化上面讲到各种命令的类, 比如"watch,trace,redefine...", 每个命令对应一个Command类,具体怎么实现可以看下一个源码分析
List
redefine
, watch
, jad
等, 下面只列举了部分命令, 感兴趣的可以看源码, 大同小异。RedefineCommand
源码,对应"redefine"命令(每个命令都是继承AnnotatedCommand
类,重写他的process
方法实现)public void process(CommandProcess process) {
if (this.paths != null && !this.paths.isEmpty()) {
......省略部分代码
Instrumentation inst = process.session().getInstrumentation(); //还是通过Instrumentation实现
File file = new File(path); //path就是我们的redefine -p 后面指定的class文件路径, 然后下面还会校验文件是否存在
f = new RandomAccessFile(path, "r"); //读取我们修改的class为byte[]字节数组
......省略部分代码
Class[] var25 = inst.getAllLoadedClasses(); //通过Instrumentation获取jvm所有加载的类
......省略部分代码
try {
inst.redefineClasses((ClassDefinition[])definitions.toArray(new ClassDefinition[0])); //最终还是调用Instrumentation的redefineClasses方法实现的
process.write("redefine success, size: " + definitions.size() + "n");
} catch (Exception var18) {
process.write("redefine error! " + var18 + "n");
}
process.end();
}
}
}
WatchCommand
源码,对应"watch
"指令(WatchCommand
的实现是在EnhancerCommand
里, 因为这个指令和trace
,stack
, tt
等都有相同的功能,所以放在父类里实现了)public class Enhancer implements ClassFileTransformer {
public static synchronized EnhancerAffect enhance(Instrumentation inst, int adviceId, boolean isTracing, boolean skipJDKTrace, Matcher classNameMatcher, Matcher methodNameMatcher) throws UnmodifiableClassException {
......省略部分代码
inst.addTransformer(enhancer, true); //将enhancer实例添加到转换器里,enhancer是ClassFileTransformer的实现类, ClassFileTransformer正是instrument的另一个关键组件,所有的转换实现都是基于ClassFileTransformer实现的
if (GlobalOptions.isBatchReTransform) {
......省略部分代码
while(var17.hasNext()) {
Class clazz = (Class)var17.next();
try {
inst.retransformClasses(new Class[]{clazz}); //重新转换指定的类,即动态修改原来的class文件,他和redefineClass方法的区别就是不需要源class文件,而是直接在现有的class文件上做修改,见下面的transform()方法
logger.info("Success to transform class: " + clazz);
} catch (Throwable var15) {
......省略部分代码
throw new RuntimeException(var15);
}
}
}
} finally {
inst.removeTransformer(enhancer);
}
return affect;
}
public byte[] transform(final ClassLoader inClassLoader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 这个方法正是重载了ClassFileTransformer.transform方法, 通过asm字节码工具的ClassReader和ClassWriter实现修改我们的class文件的
// 代码这里就不展开了(其实我也看不懂... 内部都是些字节码语法,如果是用javassist还勉强能看)
}
}
JadCommand
命令实现比较简单, 主要是通过一个第三方的反编译框架CFR实现的,cfr支持java8的一些新特性,比如lambda
表达式的反编译, 对新的jdk支持比较好private void processExactMatch(CommandProcess process, RowAffect affect, Instrumentation inst, Set
VirtualMachine Instrumentation
Instrumentation
基础上实现的,所有加载的类会通过Agent加载,addTransformer
之后再进行增强,SearchUtil
来进行的,通过Instrument
的loadAllClass
方法将所有的JVM加载的class按名字进行匹配,再进行后续处理Instrumentation
是个好东西 : )七. 注意事项
tt
指令,建议加 -n 参数 限制输出次数,sc *
通配符的使用不当,范围过大,使用异步任务时,请勿同时开启过多的后台异步命令,以免对目标JVM性能造成影响,一把双刃剑(它甚至可以修改jdk里的原生类),所以在线上运行肯定是需要权限和流程控制的。八. 相关资料