一、Arthas是什么
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率
二、Arthas的使用
1、如何安装
可以在使用相关命令,下载对应的jar包
curl -O https://arthas.aliyun.com/arthas-boot.jar
然后通过java 启动命令进行启动
java -jar arthas-boot.jar
启动后,arthas会执行jps命令,展示当前所有运行的jvm进程,我们手动输入pid即可。
注意:
由于k8s中的jdk是不完善的,使用jps命令的时候会卡住,所以可以在启动命令中添加对应进程的pid,k8s中的java进程pid一般都为1
java -jar arthas-boot.jar 1
2、追踪耗时链路 trace命令(trace)
当我们想要知道一个接口的调用耗时的使用,可能会采用skywaking等链路追踪方式进行查看,或者是采用手动在代码中打耗时日志的方式进行处理。但是以上两种方式都是有缺陷的。采用日志的方式,需要去手动修改代码,以及部署环境,步骤繁琐。而链路追踪方式查看到的数据是有限的,只能针对于外部服务调用以及中间件命令执行等。
针对于以上的情况,我们可以使用trace命令,来监控某一段方法执行的耗时,例如
trace com.xxx.xxx.impl.StockMallServiceNewImpl preemptionStock -n 5 --skipJDKMethod false
其中每个方法前面的百分号是这个方法执行耗时占了整个链路的百分比,后面是相关具体耗时,可以看到上图中setPreAndAvailNum耗时是比较久的,占用了6ms多,由于arthas无法动态监听下一级方法(新版本已经提供,但是感觉效果一般,具体可以查看相关文档),所以需要我们手动添加该方法的监听命令
trace -E com.xxx.xxx.impl.StockMallServiceNewImpl|com.xxx.xxx.agg.StockGoodsAggNewService setPreAndAvailNum|preemptionStock -n 5 --skipJDKMethod false '1==1'
这个时候就可以看到这个方法里面耗时了,可以按照这个方式不断的往下追踪。其它的使用可以参考文档中的相关参数
支持ognl表达式过滤,有些时候我们只想看某个调用参数的链路耗时,可以通过ognl表达式来进行过滤
trace com.xxx.xxx.impl.StockMallServiceNewImpl preemptionStock "params[0][0].spuId == @java.lang.Long@parseLong('1549087264473120')" -n 5 --skipJDKMethod false
注意,该命令的追踪耗时并不是完全正确的,但是可以作为大体参考的依据。
3、观察方法执行参数、结果与异常 watch命令(watch)
watch com.xxx.xxx.impl.StockMallServiceNewImpl preemptionStock '{params,target,returnObj,throwExp}' -n 5 -x 3
其中观察表达式,默认值:目前有四个参数
params:方法入参
target:执行方法对象
returnObj:方法返回
throwExp:异常结果
该命令也与trace命令一致,支持ognl表达式,可以进行使用表达式来指定我们想要看到的参数
4、时空隧道tt (tt )
方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
- tt 命令的实现是:把函数的入参/返回值等,保存到一个Map<Integer, TimeFragment>里,默认的大小是 100。
- tt 相关功能在使用完之后,需要手动释放内存,否则长时间可能导致OOM。退出 arthas 不会自动清除 tt 的缓存 map。
tt -t com.xxx.xxx.impl.StockMallServiceNewImpl preemptionStock -n 5
这个时候在tt的缓存中进行储存了相关的信息,我们可以使用 tt -l 的命令获取相关的记录
当前这些记录的获取也是可以支持ognl表达式的
查看某一次调用的记录
最重要的一点,tt可以通过缓存记录来重新发起请求,但是对于ThreadLocal等相关信息,arthas是无法复现的
以及不要忘了,不使用这个命令后请手动清理一下缓存
tt --delete-all
5、反编译JAD
比如现在线上出现了问题,但是会怀疑线上可能不是最新的代码,我们要如何获取线上现在运行的代码是什么呢?arthas提供了jad命令,可以对线上类文件进行反编译,查看当前运行的代码
jad --source-only com.xxx.xxx.impl.StockMallServiceNewImpl
6、线程命令thread(thread)
thread命令可以查看当前JVM中相关的线程信息,比如查看某个线程的堆栈,查看占用cpu大的线程以及当前正在堵塞的线程等
thread -n 3
- cpuUsage为采样间隔时间内线程的 CPU 使用率,与dashboard命令的数据一致。
- deltaTime为采样间隔时间内线程的增量CPU 时间,小于 1ms 时被取整显示为 0ms。
- time 线程运行总 CPU 时间。
thread -b
一些问题:
1、arthas指令构建复杂
arthas的指令众多,有些指令构建比较复杂,所以可以使用已有IDEA plugin来帮助我们简化指令构建。arthas-idea-plugin · 语雀 (yuque.com)
在对应方法使用右键 -> 选择Arthas Command -> 点击对应指令即可快速生成
arthas中很多指令支持ognl表达式 ,附上ognl表达式文档 Apache Commons OGNL - 语言指南
Arthas的一些特殊用法文档说明 · Issue #71 · alibaba/arthas · GitHub
2、arthas对原本进程的性能影响
arthas对原本进程的影响,本质上arthas对进程的影响是很小的,但是对于一些命令的执行我们要额外关注下,比如trace的时候尽量不要一次性监听多个类。以及对于调用频率高的方法,由于arthas会重写字节码,可能会导致性能产生波动等(比如字节码已经由JIT编译为机器码了,但是由于重新修改了字节码导致JIT编译的失效了)
trace存在ms级的性能损耗吗? · Issue #2025 · alibaba/arthas · GitHub
3、arthas如果命令执行失败了,原本方法执行会失败么
不会,源码中有try catch保证原本方法不会执行失败
4、trace命令在开启另外一个线程下是否会呈现树结构
不会,这些耗时信息储存在ThreadLocal中的,不同线程的数据不同,导致找不到所属节点,无法展示成树结构
三、arthas的原理
首先对于arthas的原理,我们要先知道两个东西
通过javaagent,我们可以灵活的修改虚拟机中字节码,从而扩展功能
arthas启动分析:
1、启动分析
arthas通过jdk提供的类VirtualMachine获取当前所有正在运行的JVM实例,通过pid获取对应的实例,然后通过attach方法对该实例进行连接。之后会通过loadAgent的方式来对jvm已加载的类进行增强
com.taobao.arthas.core.Arthas#attachAgent
之后根据java agent的规范会调用AgentBootstrap方法中的agentmain方法,该方法会启动一个arthas server,绑定到对应端口上,监听我们客户端上传的命令
private void attachAgent(Configure configure) throws Exception {
VirtualMachineDescriptor virtualMachineDescriptor = null;
//遍历当前所有的java虚拟机,获取对应pid的虚拟机,
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);
}
```
//在虚拟机中加载arthas agent
try {
virtualMachine.loadAgent(arthasAgentPath,
configure.getArthasCore() + ";" + configure.toString());
}
}
2、命令执行分析
来看一下trace命令arthas是如何处理的,trace命令实际上是对监听的方法的执行都织入了一个advice(增强)
在arthas的源码中有一个TraceCommand的处理类,这个类会返回一个TraceAdviceListener对我们要监听的类进行增强,
TraceCommand继承于EnhancerCommand,EnhancerCommand中有一个方法可以对类进行增强处理,使用ClassFileTransformer的方式
protected void enhance(CommandProcess process) {
//省略一部分代码。。。。。
try {
Instrumentation inst = session.getInstrumentation();
AdviceListener listener = getAdviceListenerWithId(process);
if (listener == null) {
logger.error("advice listener is null");
String msg = "advice listener is null, check arthas log";
process.appendResult(new EnhancerModel(effect, false, msg));
process.end(-1, msg);
return;
}
boolean skipJDKTrace = false;
if(listener instanceof AbstractTraceAdviceListener) {
skipJDKTrace = ((AbstractTraceAdviceListener) listener).getCommand().isSkipJDKTrace();
}
//创建一个增强器,本质上是ClassFileTransformer实现
Enhancer enhancer = new Enhancer(listener, listener instanceof InvokeTraceable, skipJDKTrace, getClassNameMatcher(), getClassNameExcludeMatcher(), getMethodNameMatcher());
// 注册通知监听器
process.register(listener, enhancer);
// 从jvm加载的所有类中获取需要增强的类,并进行增强
effect = enhancer.enhance(inst, this.maxNumOfMatchedClass);
if (effect.getThrowable() != null) {
String msg = "error happens when enhancing class: "+effect.getThrowable().getMessage();
process.appendResult(new EnhancerModel(effect, false, msg));
process.end(1, msg + ", check arthas log: " + LogUtil.loggingFile());
return;
}
//省略一部分代码。。。。。
}
Enhancer 这个类实现了 ClassFileTransformer, 会对对应的类将AdviceListener 进行织入,调用Instrumentation retransformClasses 进行字节码的替换
public synchronized EnhancerAffect enhance(final Instrumentation inst, int maxNumOfMatchedClass) throws UnmodifiableClassException {
// 获取需要增强的类集合
this.matchingClasses = GlobalOptions.isDisableSubClass
? SearchUtils.searchClass(inst, classNameMatcher)
: SearchUtils.searchSubClass(inst, SearchUtils.searchClass(inst, classNameMatcher));
if (matchingClasses.size() > maxNumOfMatchedClass) {
affect.setOverLimitMsg("The number of matched classes is " +matchingClasses.size()+ ", greater than the limit value " + maxNumOfMatchedClass + ". Try to change the limit with option '-m <arg>'.");
return affect;
}
// 过滤掉无法被增强的类
List<Pair<Class<?>, String>> filtedList = filter(matchingClasses);
if (!filtedList.isEmpty()) {
for (Pair<Class<?>, String> filted : filtedList) {
logger.info("ignore class: {}, reason: {}", filted.getFirst().getName(), filted.getSecond());
}
}
logger.info("enhance matched classes: {}", matchingClasses);
affect.setTransformer(this);
try {
ArthasBootstrap.getInstance().getTransformerManager().addTransformer(this, isTracing);
// 批量增强
if (GlobalOptions.isBatchReTransform) {
final int size = matchingClasses.size();
final Class<?>[] classArray = new Class<?>[size];
arraycopy(matchingClasses.toArray(), 0, classArray, 0, size);
if (classArray.length > 0) {
inst.retransformClasses(classArray);
logger.info("Success to batch transform classes: " + Arrays.toString(classArray));
}
} else {
// for each 增强
for (Class<?> clazz : matchingClasses) {
try {
inst.retransformClasses(clazz);
logger.info("Success to transform class: " + clazz);
} catch (Throwable t) {
logger.warn("retransform {} failed.", clazz, t);
if (t instanceof UnmodifiableClassException) {
throw (UnmodifiableClassException) t;
} else if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new RuntimeException(t);
}
}
}
}
} catch (Throwable e) {
logger.error("Enhancer error, matchingClasses: {}", matchingClasses, e);
affect.setThrowable(e);
}
TraceCommand的增强类
public class TraceAdviceListener extends AbstractTraceAdviceListener implements InvokeTraceable {
/**
* Constructor
*/
public TraceAdviceListener(TraceCommand command, CommandProcess process, boolean verbose) {
super(command, process);
super.setVerbose(verbose);
}
/**
* trace 会在被观测的方法体中,在每个方法调用前后插入字节码,所以方法调用开始,结束,抛异常的时候,都会回调下面的接口
*/
@Override
public void invokeBeforeTracing(ClassLoader classLoader, String tracingClassName, String tracingMethodName, String tracingMethodDesc, int tracingLineNumber)
throws Throwable {
// normalize className later
threadLocalTraceEntity(classLoader).tree.begin(tracingClassName, tracingMethodName, tracingLineNumber, true);
}
@Override
public void invokeAfterTracing(ClassLoader classLoader, String tracingClassName, String tracingMethodName, String tracingMethodDesc, int tracingLineNumber)
throws Throwable {
threadLocalTraceEntity(classLoader).tree.end();
}
@Override
public void invokeThrowTracing(ClassLoader classLoader, String tracingClassName, String tracingMethodName, String tracingMethodDesc, int tracingLineNumber)
throws Throwable {
threadLocalTraceEntity(classLoader).tree.end(true);
}
}
在这个tree中保存着每个方法执行时候的耗时。
public class TraceTree {
private TraceNode root;
private TraceNode current;
private int nodeCount = 0;
public TraceTree(ThreadNode root) {
this.root = root;
this.current = root;
}
/**
* Begin a new method call
* @param className className of method
* @param methodName method name of the call
* @param lineNumber line number of invoke point
* @param isInvoking Whether to invoke this method in other classes
*/
public void begin(String className, String methodName, int lineNumber, boolean isInvoking) {
TraceNode child = findChild(current, className, methodName, lineNumber);
if (child == null) {
child = new MethodNode(className, methodName, lineNumber, isInvoking);
current.addChild(child);
}
child.begin();
current = child;
nodeCount += 1;
}
以上就是大体的流程,更加具体的流程大家可以看看源码