线上 | Arthas - [全书]

INDEX

§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.*

方法表达式

  • 支持全方法名,如 bb 是下面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,按需更改
  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值