INDEX
- §1 文档资料
- §2 推荐的使用原则
- §3 使用方式速通
- §4 指令([文档](https://arthas.aliyun.com/doc/commands.html))
- `help` 帮助
- `dashboard` 大屏
- `thread` 线程信息
- `logger` 日志控制
- `watch` 函数执行数据观测
- `options` arthas 开关切换
- `stack` 方法外调用路径【谁调了方法】
- `trace` 方法内调用路径【方法每一步耗时】
- `jvm` 信息
- `reset` 还原增强类
- `vmtool` 虚拟机工具
- `jad` 反编译 / `mc` 内存编译 / `redefine` 重定义 / `retransform` 外部加载【热修改、热部署】
- `sc` 类信息
- `classloader` 类加载器信息
- `sysprop` 查看系统参数 / `sysenv`查看系统环境变量
- §5 生产实践
- §6 部分关键源码位置(方便定制化)
§1 文档资料
官方资料
在线教程(自行 killercode 登录)
git(直接去找 releases 的 arthas-all 就行)
§2 推荐的使用原则
建议的使用方式
- 生产环境:应急手段,比如快速定位性能问题、部分死锁、gc情况等
- 测试环境:增效工具,验证类加载情况,调用方法等,常用于联调时链路阻塞导致的频繁修改试错
- 开发环境:可以随便用,但想不到什么使用场景
强烈不建议的使用方式
- 任何环境下作为常规工具使用
- 远程 debug,解决问题后不进行任何优化修改
- 生产环境下,使用带有通配符的命令,尤其是 类名通配、包名通配
日志收集、服务监控、线上手动 rpc,中间件平台,属于系统基建。
同时,问题的处理通常不是一次性的,当前的问题及时解决了,也不代表几次迭代后不会再同一个地方换一个方式出点别的问题。
因此,在必要情况下进行了应急处理之后(限生产环境),建议考虑对基建进行增补。
日志打印过多影响效率?
- 禁用栈收集
- 禁用打印行号
- 禁泛用 json 序列号,改用 toString(toString 是可以实现 toJson 的),附对应模板
- 整理日志的 appender,防止双份日志打印
- 去掉完全没用的日志(通常是不输出任何变量值的日志)
不能复现的请求?
- 只要有入参,理论上就都能复现
- 缺少的数据从对应环境上获取
- 环境问题调不通的接口,尝试通过mock解决
- 过于长的链路,尝试缩小复现范围
§3 使用方式速通
arthas 操作的整体思路
- 启动 arthas(默认已经启动了被监控应用)
- 完成挂载(官方称为 attach)
- 使用/退出 arthas 指令
- 退出 arthas
启动
启动项目 -> 启动 arthas 并自动挂载
nohup java -jar math-game.jar > /data/logs/arthas/log.log 2>&1 &
挂载(attach)
java -jar arthas-boot.jar
arthas 启动时,会自动探查活着的 java 进程并展示列表,直接输入 数字+ enter
即可完成 attach,如下图
需注意
- 启动 arthas 所用用户必须具有被探查进程的权限,否则可能 attach 失败
- arthas 的日志默认在
~/logs/arthas/
,如下图
使用/退出指令
这里以 dashboard
为例
q 退出 dashboard 命令
退出
退出指令分两类
- exit/quit:退出当前 attach
- stop:退出所有 attach
§4 指令(文档)
用法有索引,可以 ctrl + F
help
帮助
用法
查看所有指令和概述
help
查看某个之类的说明
help dashboard
,查看 dashboard 用法
dashboard
大屏
用法
打印 dashboard 信息
dashboard
, 不限次,间隔 5 秒
按需打印 dashboard 信息
dashboard -i 3000 -n 2
,打印 dashboard 信息,一共 2 次,间隔 3 秒
信息说明
绝大部分信息下图足够,jvm 内部线程 id 另行说明
jvm 内部线程 id
这是 jvm 的线程在内部的编号,与 linux 线程 id 对应关系如下图
arthas 里可以看到 jvm 线程 id 和线程名,jstack 可以看到前两个内容和系统线程 id (16进制的)
经过换算后,0x11B28=72488,可以用来使用各种 linux 指令做进一步排查
thread
线程信息
用法
查看 top n 信息【topn】
thread -n 5
,查看 TOP 5 线程
thread -n 1 -i 2000
,查看 2 秒内最忙线程(这里 -i 是统计窗口不是采样间隔)
查看阻塞线程【死锁】
thread -b
,可以找到阻塞与 synchronized 的线程信息,但 对 Lock 无力
【不建议使用】:支持场景不全
从实践上看,不如 jstack
好使,对比用例如下
public class DeathLockTestDemo {
Object l1 = new Object(); Object l2 = new Object();
Lock l3 = new ReentrantLock(); Lock l4 = new ReentrantLock();
public void deathLock(){
Thread t1 = new Thread(()->{
synchronized (l1){
try {
TimeUnit.SECONDS.sleep(3);
synchronized (l2){System.out.println("不可能,绝对不可能 1");}
} catch (InterruptedException e) { e.printStackTrace(); }
}
});
// 和 t1 一样,交换锁顺序
Thread t2 = new Thread(()->{});
Thread t3 = new Thread(()->{
try {
l3.lock();
TimeUnit.SECONDS.sleep(3);
l4.lock();
System.out.println("不可能,绝对不可能 3");
} catch (InterruptedException e) { e.printStackTrace(); }finally {
l3.unlock();
l4.unlock();
}
});
// 和 t4 一样,交换锁顺序
Thread t4 = new Thread(()->{ });
// 依次开线程
System.out.println("卍解...........................");
t1.start(); t2.start(); t3.start(); t4.start();
}
}
使用 arthas,只能看到一组死锁,但是显示相对友好
jstack 可以看到两组死锁
查看所有线程信息
thread --all
,与 dashboard 展示信息一致
查看指定线程信息
thread 1
,通过 jvm 线程 id 指定
查看指定状态的线程
thread --state RUNNABLE
,查看运行态线程
thread --state RUNNABLE
== thread -b
logger
日志控制
用法
查看 logger 信息=
logger
动态修改日志级别
logger -c classloader的hashcode --name logger名字 --level 日志级别
推荐用指定 logger 的方式处理(比 ognl 的方式简单易懂)
直接操作 root 就是修改全局日志级别,若某个类或包单独配了 logger,就可以单独控制这些类、包的日志级别
可用于紧急打开部分未开启的日志复现问题(比如一个消费失败的消息还在重试)
示例
//每 3 秒,按 "HH:mm:ss" 格式大约当前时间,日志级别 info
new Thread(()->{
for (;;) { logger.info("{}",df.format(new Date())); }
}).start();
修改全局日志级别为 error,过段时间再改回来
classloader -l
获取 AppClassLoader 的 hashcode(7daf6ecc)
logger -c 7daf6ecc --name ROOT --level error
,过几秒再改回来
watch
函数执行数据观测
用法*
监听某个接口输入输出(接口出入参)(优先考虑用接口日志、访问日志处理)
watch 类表达式 方法表达式 "观察表达式" -b -f
,监听接口出入参
但建议优先补全日志
示例
public class WatchTestDemo {
public void a (){
System.out.println("卍解...........................");
new Thread(()->{
for (;;) {
//睡5秒
String u = UUID.randomUUID().toString();
System.out.println(b(u,3));
}
}).start();
}
public int b(String u,int n){
return u.hashCode()%64 * n;
}
watch com.atd.arthastestdemo.WatchTestDemo b "{params[0],returnObj}" -b -f
查看 com.atd.arthastestdemo.WatchTestDemo#b
的出入参,入参只打印第一个元素
watch com.atd.arthastestdemo.* * "{params,returnObj}" -b -f
查看 com.atd.arthastestdemo
包下所有方法的出入参,但入参看出来有几个
options json-format true
watch com.atd.arthastestdemo.* * "{params}" -b -f '#cost>2000'
查看 com.atd.arthastestdemo.WatchTestDemo#b
耗时超过 2 秒的请求的入参
类表达式
- 支持全类名指定,如
com.atd.arthastestdemo.WatchTestDemo
- 支持通配,如
com.atd.arthastestdemo.*
方法表达式
- 支持全方法名,如
b
,b
是下面demo中的方法名 - 支持通配,如
*
观察表达式
默认的表达式:{params, target, returnObj}
- params:默认是个数组,直接用 params 只能打印个锤子,需要更进一步的写法,比如 params[0]
- target:其实就是监听的目标,因为类表达式和方法表达式支持通配,所以可能一个 watch 下去会监听多个接口
- returnObj:返回值
观察表达式是一个 ognl 表达式,可以按其规则灵活定义,比如下面表达式都是有效的
{params, target, returnObj}
,入参按数组打印个锤子
必要时,可以结合options json-format true
使用
{params[0],params[1],returnObj}
,打印前两个入参和返回值
{params[0].length,params[1],returnObj}
,打印第一个参数的长度,第二个参数和返回值
条件表达式
条件表达式可以用来过滤指令输出,只有满足的条件的才会应用打印
表达式可以基于以下内容进行判断:
- target : 对象
- clazz : 对象的类
- method : 方法
- params : 入参数组
- params[0…n] : 指定索引的入参,如 ‘params[0].field>100’
- returnObj : 返回值
- throwExp : 异常
- isReturn : 是否正常返回,如 ‘isReturn’
- isThrow : 是否抛异常、错误,如 ’ isThrow’
- #cost : 耗时,如 ‘#cost>1000’
options
arthas 开关切换
用法
查看所有开关
options
控制指定开关
options json-format true
,开关 json 化输出对象 开启
可以用来优化上面那个锤子
stack
方法外调用路径【谁调了方法】
用法
查看某方法谁在调用
stack com.atd.arthastestdemo.WatchTestDemo b
按条件筛选方法的调用
stack
的条件表达式同 watch
stack com.atd.arthastestdemo.WatchTestDemo b 'isReturn'
,筛选正常返回的调用
stack com.atd.arthastestdemo.WatchTestDemo b '#cost>1000'
,筛选耗时 > 1s 的调用
trace
方法内调用路径【方法每一步耗时】
用法
排查方法调用耗时点
trace com.atd.arthastestdemo.LoggerLevelRewriteTestDemo d --skipJDKMethod false
,也计算 jdk 方法的耗时,实际使用中通常不需要计算
trace -E com.atd.arthastestdemo.LoggerLevelRewriteTestDemo b|d
,同时排查多个方法,类也同理
trace com.atd.arthastestdemo.LoggerLevelRewriteTestDemo b '#cost>1000'
,只排查耗时 > 1s 的调用的耗时点
【可使用】
精确度说明
trace
可能因为如下原因导致时间不准,但实际使用上不会产生过大影响
- jdk 调用
- 非函数调用,比如 i++
- arthas 自身开销
- GC、同步块 等不可抗力
jvm
信息
用法
jvm 打印如下信息
主要信息包括 内存信息、线程信息、垃圾回收器信息,因上述信息 dashboard 都包含,因此不推荐使用
reset
还原增强类
用法
watch
/ trace
都是基于类增强的,使用后需要 reset
还原所有增强类
reset
还原指定增强类
reset 类名
vmtool
虚拟机工具
用法
获取对象并执行表达式【查看内存中变量,查看变量】
这个功能完全可以理解为 IDE 中的 add to watch
vmtool --action getInstances --className 类名表达式
vmtool --action getInstances --className 类名表达式 --express '表达式'
类名表达式支持通配,比如以下用法
--className com.atd.arthastestdemo.VmtoolExpTestDemo
--className *.VmtoolExpTestDemo
示例
@Component("vt")
public class VmtoolExpTestDemo {
private int aaa;
private int bbb;
private String text;
public void a (){
System.out.println("卍解...........................");
new Thread(()->{
for (;;) {
aaa = new Random().nextInt(10);
bbb = new Random().nextInt(10);
text = UUID.randomUUID().toString();
//打印,然后睡30秒
}
}).start();
}
}
PS:类名
com.atd.arthastestdemo.VmtoolExpTestDemo
略长,下文用 <类> 表示
vmtool --action getInstances --className <类> --limit 1
,获取 1 个指定类对象,并展示结果
vmtool --action getInstances --className <类> --express 'instances[0].getText()'
vmtool --action getInstances --className <类> --express 'instances[0].getText().length()'
vmtool --action getInstances --className <类> --express 'instances[0].getText().substring(0,10)'
强制 GC【强制 Full GC】
vmtool --action forceGc
【可使用】:常用于在测试环境上复现问题
示例(arthas-test-demo
是测试项目)
java -jar -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC arthas-test-demo-0.0.1-SNAPSHOT.jar
arthas attach 上项目后,使用 vmtool --action forceGc
,可见如下项目日志
jad
反编译 / mc
内存编译 / redefine
重定义 / retransform
外部加载【热修改、热部署】
用法
反编译类
jad --source-only demo.MathGame
,不带 classLoader 信息
【不建议使用】:类太大时还不如把jar 包down下来
反编译方法
jad demo.MathGame main --lineNumber false
,带 classLoader 不带行号
热修改、热部署
常见于联调环境卡流程时快速实验想法,但需注意 试完了别忘在代码上做实际修改
示例
//压缩了格式
@RestController
public class ArthasTestDemoController {
@Value("${props.b}") private String b;
@GetMapping("/bibi")
public String get(){ return b; }
}
jad --source-only com.atd.arthastestdemo.ArthasTestDemoController --lineNumber false > /tmp/ArthasTestDemoController.java
反编译,只要源码,不要行号,输出到指定目录- 修改
/tmp/ArthasTestDemoController.java
为如下,怎么修改都行
@GetMapping(value={"/bibi"})
public String get() {
return "233333333333333333333333";
}
classloader -l
获取可用的 classloader(内存编译需要指定),选 App 的:org.springframework.boot.loader.LaunchedURLClassLoader
-mc --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader /tmp/ArthasTestDemoController.java -d /tmp
内存编译,结果如下,输出目录为 /tmp/com/atd/arthastestdemo/ArthasTestDemoController.class
-redefine /tmp/com/atd/arthastestdemo/ArthasTestDemoController.class
重定义,完成热修
或使用retransform /tmp/com/atd/arthastestdemo/ArthasTestDemoController.class
,效果一样redefine
后无法还原retransform
可以通过下面指令还原
# 也可以用 retransform -l 拉取所有 entry,然后 retransform -d <id> 定点删除
retransform --deleteAll
retransform --classPattern com.atd.arthastestdemo.ArthasTestDemoController
- 验证
sc
类信息
用法
鉴定某类是否被加载【生效,初始化】
sc *ArthasTestDemo*
,已经加载的类中包含 ArthasTestDemo 的有哪些
可以用于
- 检查基于各种
@Condition
的类是否按期望加载 - 检查基于类的代码修改是否生效
更多信息查看官网
classloader
类加载器信息
用法
列出所有 classloader
classloader -l
sysprop
查看系统参数 / sysenv
查看系统环境变量
只用于看系统参数、系统环境变量,不支持通配,一般用不到
§5 生产实践
生产环境中,项目通常运行在容器中,不利于顺利 attach,可以采用 arthas tunnel server 的方式集中管理
§5.1 server 端
下载
server 端直接部署 arthas tunnel server,需要下载 fatjar
server 的版本要和项目端版本对应
配置
打开 fatjar,找到 application.properties
修改配置
按需修改配置,然后在放回 fatjar 中
- 打开
arthas.enable-detail-pages
- 修改 server 端口,尽量别占用 8080
# arthas tunnel server host
arthas.server.host=0.0.0.0
# arthas tunnel server port
arthas.server.port=7777
# for all endpoints
management.endpoints.web.exposure.include=*
# default user name
spring.security.user.name=arthas
# If set to true, be sure to do security protection to ensure that the server will not be illegally accessed
arthas.enable-detail-pages=true
spring.cache.type=caffeine
spring.cache.cache-names=inMemoryClusterCache
spring.cache.caffeine.spec=maximumSize=3000,expireAfterAccess=3600s
#arthas.embedded-redis.enabled=true
#arthas.embedded-redis.settings=maxmemory 128M
#spring.redis.host=127.0.0.1
server.port=9999
启动 server
nohup java -jar arthas-tunnel-server-3.7.2-fatjar.jar > /data/logs/arthas/tunnel-server.log 2>&1 &
§5.2 项目端
需按目前项目选型,对于大多数项目,优先选择 Arthas Spring Boot Starter
对接简单,但只支持 springboot 2
添加依赖
server 的版本要和项目端版本对应
<dependency>
<groupId>com.taobao.arthas</groupId>
<artifactId>arthas-spring-boot-starter</artifactId>
<version>3.7.2</version>
</dependency>
添加配置
arthas:
# agent-id: asdf
tunnel-server: 'ws://192.168.32.3:7777/ws'
# 如果配置了,agentId=app-name_agent-id
# 否则 spring-application-name_agent-id
# 如果再缺省可以通过agent id强行连接,但 apps 页面中不展示,源码中认为属于非法 agent-id
# app-name: a
- agent-id:需要精确控制时可以配置,否则自动生成
- app-name:
- 若配置,agentId = app-name_agent-id
- 否则,agentId = spring-application-name_agent-id
- spring.application.name 缺省属于非法 agent-id,可以在 index 强行连接,但 apps 页面不显示
- tunnel-server:必配,项目启动后自动启动 arthas,并用此地址长连接至 server 端
- disabled-commands:禁用的指令列表,默认禁用 stop(会导致整个 server 凉),应该按需求禁用危险的指令,比如 redefine
启动
略
验证
启动成功
查看已经 attach 的应用
查看已经 attach 的 agent
直接用 agentId 连接
- 可以通过 agentId 强制连接 apps 页面没有显示的应用的 agent
- 禁用的指令会找不到,如下图
注意:不能用 ↑/↓ 快速调用指令
$5.3 集群
官方文档里,集群的笔墨较少,提供的方案是结合 Nginx + 粘性 session 做负载均衡,只是个分布式方案
默认的 server 端存储是本地缓存 caffeine,存储的数据大多是各个 agent 的数据,量不多,集群的诉求不强烈
§6 部分关键源码位置(方便定制化)
项目端启动绑定
com.alibaba.arthas.spring.ArthasConfiguration#arthasAgent
->
com.taobao.arthas.agent.attach.ArthasAgent#init
->
Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, Instrumentation.class, Map.class).invoke(null,
instrumentation, configMap);
com.taobao.arthas.core.server.ArthasBootstrap#ArthasBootstrap
->
com.taobao.arthas.core.server.ArthasBootstrap#bind
agent-id 生成 & 绑定
client 端绑定
Tunnel Client 绑定 server 过程中,若配置了 agent-id,则在 com.taobao.arthas.core.server.ArthasBootstrap#bind
创建 TunnelClient 时用配置值赋值
若没有配置,会在连接 server 端后,由 TunnelServer 生成,由 TunnelClientSocketClientHandler
接受并保存
com.taobao.arthas.core.server.ArthasBootstrap#bind
->
com.alibaba.arthas.tunnel.client.TunnelClient#start
->
com.alibaba.arthas.tunnel.client.TunnelClient#connect
com.alibaba.arthas.tunnel.client.TunnelClientSocketClientHandler#channelRead0
server 端生成 agent-id
com.alibaba.arthas.tunnel.server.TunnelSocketFrameHandler#agentRegister
agent-id = appName_随机串,建议调整为 agent-id = appName_node_随机串
// generate a random agent id
String id = null;
if (appName != null) {
// 如果有传 app name,则生成带 app name前缀的id,方便管理
id = appName + "_" + RandomStringUtils.random(20, true, true).toUpperCase();
} else {
id = RandomStringUtils.random(20, true, true).toUpperCase();
}
拉取 apps
com.alibaba.arthas.tunnel.server.app.web.DetailAPIController#tunnelApps
server 中 agent 信息存储
apps.html
-> /api/tunnelApps
agent 信息通过下面方法拉取,TunnelClusterStore 在默认配置下用 caffeine 本地缓存
com.alibaba.arthas.tunnel.server.cluster.TunnelClusterStore#allAgentIds
spring.cache.type=caffeine
spring.cache.cache-names=inMemoryClusterCache
spring.cache.caffeine.spec=maximumSize=3000,expireAfterAccess=3600s
agent 在向 server 注册时,其信息被服务端存储
com.alibaba.arthas.tunnel.server.TunnelSocketFrameHandler#agentRegister
->
com.alibaba.arthas.tunnel.server.TunnelServer#addAgent
tunnel server 从本地缓存拉取所有 agent-id,然后按 “_” 切割解析 appName
若没有获取 appName,则 agent-id 被视为非法 id,不展示
因此,spring.application.name
必须配置(不推荐使用 arthas.appName)
生产环境如果希望大规模使用,必须增加权限控制,按不同登录人区数据权限
拉取 apps 下节点
agents.html?app=arthas-test-demo -> /api/tunnelAgentInfo?app=arthas-test-demo
tunnel server 从本地缓存拉取所有 agent-id,然后按 appName_ 为前缀从所有 agent-id 中匹配
按上文 agent-id 生成策略调整后,不影响此处逻辑,且 agents 页面上会自动带有 node 信息
权限
若使用意愿强烈,按公司实际情况,宜按如下思路
- 对接企业 ERP,erp 账号即登录账号
- 开发统一权限授权
- 分支项目1:权限定制、角色定义、数据权限界定
- 分支项目2:鉴权
- 分支项目3:统一申请
- arthas 默认使用的 spring security,按需更改