作者 | 于爽 中原银行系统研发工程师,目前在技术平台室中间件小组从事分布式缓存、消息队列等相关工作。
【Arthas 官方社区正在举行征文活动,参加即有奖品拿哦~点击投稿】
Arthas 是一款 Java应用开源诊断工具,由于其强大的问题排查及诊断能力,自其开源以来广受开发者的关注和使用,多次登顶 GitHub Trending,并得到国内多家技术媒体的推荐分享。
一. 定制化功能改造
Arthas 可以通过简单的命令交互模式,接入运行的 JVM,快速定位和诊断线上程序运行问题。在不重启服务的情况下,实时、动态的修改相关 code,并实时生效。具体工作原理如下:
1. 连接JVM:通过attach机制,通过attach pid连接正在运行的JVM;
2. 查看及修改JVM字节码:通过instrument技术对运行中的JVM附加或修改字节码来实现增强的逻辑。
2018 年底,中原银行开始投入人员对 Arthas 进行调研,在开源社区了解了主要功能,并通过阅读 Arthas 工程大纲,明晰整体工程结构,整个执行过程如下:Arthas 底层调用 rt.jar 包的 ManagementFactory 获取整个 jvm 内部信息,通过命令集成与后端交互,执行,返回结果,整个工程简单清晰,容易上手。
2019 年初,中原银行技术团队开始使用推广 Arthas 定位和诊断线上问题。
出于保护客户敏感信息的严格要求,同时切实保障生产环境业务系统的稳定运行,我们对 Arthas 的部分功能进行了定制化改造,对一些命令进行了隐藏:
1. watch:watch方法可以在没有打印日志的情况下,看到方法的入参和返回值,有可能暴露客户的敏感信息;
2. mc、redefine:mc组合rdefine可以对代码进行热更新,不能满足我行生产运行管理规范要求。
同时,出于使用需要,定制化开发了 gc 等命令:
1. gc:实时动态展示年轻代,年老代垃圾百分比,回收次数及耗时等情况。
下一步,我行计划在全部开发测试环境、部分生产环境推广使用 Arthas 来进行问题排查与定位诊断。同时采用内部技术分享的形式向行内应用开发团队普及推广 Arthas 的使用。
二. 重点使用功能
除了日常问题排查使用到的方法外,Arthas 还有一些强大的功能,深受中原银行技术团队喜爱。
1.target-ip
# target-ip 为指定绑定的IP,如果不指定IP,Arthas只listen 127.0.0.1,所以如果想从远程连接,则可以使用 --target-ip参数指定listen的IP
java -jar arthas-boot.jar --target-ip IP
绑定远程访问IP后,可以在通过telnet或http的方式远程连接 Arthas 进行问题排查。
web端访问地址:ip:8563
/telnet访问:ip:3658
当线上应用出现问题时,可以将问题机器隔离起来,通过Arthas在启动时指定target-ip,多方技术人员可同时通过远程连接进行问题排查。
2.trace
# 查看方法内部调用路径,并输出方法路径上的每个节点上耗时
trace ClassName methodName
使用 trace 命令可以一层一层追踪耗时在哪里 ,在进行性能调优的时候十分有效。
3.ognl
ognl 是应用于 Java 中的一个开源的表达式语言,作用是对数据进行访问,它拥有类型转换、访问对象方法、操作集合对象等功能,通过 ognl 可以完成一些列强大的操作。
- 执行静态方法
# 使用ognl调用静态方法
ognl “@类名@方法名(参数)”
- 获取静态属性
# 使用ognl获取静态属性
ognl “@类名@属性名”
- 示例:修改日志等级
# 查找当前类的classloader hashcode
sc -d 类名 | grep classLoaderHash
# 用OGNL获取logger
ognl -c ***** '@类名@logger'
# 单独设置该类的logger level
ognl -c ***** '@类名@logger.setLevel(@ch.qos.logback.classic.Level@DEBUG)'
#全局设置logger level
ognl -c ***** '@org.slf4j.LoggerFactory@getLogger("root").setLevel(@ch.qos.logback.classic.Level@DEBUG)'
4.gc
gc 是我行定制化开发的功能,源自于 jstat -gcutil pid timeinterval 命令,其中 pid 可以从 Arthas 中获取,timeinterval(单位为毫秒)表示 gc 每次时间间隔,默认为 1s。
# 查看应用gc情况(timeinterval表示间隔时间,单位毫秒,默认为1S)
gc -i timeinterval -n 5
三. 应用实践案例
下面记录一些我行 Arthas 应用实践案例(由于行内代码保密性要求,下文所示案例均为场景复现所写示例代码)
案例一:系统 CPU 使用率高
问题描述:业务人员反馈后台管理系统其中一个页面响应时间很长,登录服务器上发现 CPU 使用率较高,达到 80% 左右。
1. 启动 Arthas,附加到对应的 java 进程
注意:Arthas 启动时要使用与 Java 进程相同的启动用户。
# 启动Arthas
java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.2.0
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 11360 org.gradle.launcher.daemon.bootstrap.GradleDaemon
[2]: 12196 com.durian.ddp.Application
# 选择要附加的java进程编号
2
...
2. thread 命令查看 CPU 使用率高的线程
启动 Arthas,附加到对应的 java 进程,执行 thread -n 5 查看 CPU 使用率最高的 5 个线程的堆栈。
# 查看CPU使用率最高的5个线程
thread -n 5
at ***.TreeUtil.findMenuChildren(TreeUtil.java:94)
at ***.TreeUtil.findMenuChildren(TreeUtil.java:92)
at ***.TreeUtil.findMenuChildren(TreeUtil.java:92)
at ***.TreeUtil.findMenuChildren(TreeUtil.java:92)
at ***.TreeUtil.recursiveTree(TreeUtil.java:74)
at ***.getOwnerDeparmentTree(DepartmentServiceImpl.java:550)
...
at ***.TreeUtil.findMenuChildren(TreeUtil.java:94)
at ***.TreeUtil.findMenuChildren(TreeUtil.java:92)
at ***.TreeUtil.findMenuChildren(TreeUtil.java:92)
at ***.TreeUtil.findMenuChildren(TreeUtil.java:92)
at ***.TreeUtil.recursiveTree(TreeUtil.java:74)
at ***.getOwnerDeparmentTree(DepartmentServiceImpl.java:550)
...
...
3. 通过 monitor 命令查看方法的调用次数与耗时
通过 thread 命令已经定位到 CPU 主要消耗在 TreeUtil
的 findMenuChildren
方法上,通过 monitor 命令查看方法的具体调用次数与耗时。
# 5s为一个统计周期,统计TreeUtil中findMenuChildren方法的耗时
monitor -c 5 ***.TreeUtil findMenuChildren
通过 monitor 命令可以明确该方法单次调用平均耗时为 17 ~ 20ms,但是调用次数多,所以整体上页面响应慢。
4. 通过 jad 命令反编译 TreeUtil 类,查看源码
[arthas@12196]$ jad com.durian.ddp.utils.TreeUtil
ClassLoader:
+-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@244038d0
Location:
/*
* Decompiled with CFR.
*
* Could not load the following classes:
* ***.ResourceTreeVo
*/
...
public class TreeUtil {
public static ResourceTreeVo findMenuChildren(ResourceTreeVo resourceTreeVo, List<ResourceTreeVo> treeNodes) {
for (ResourceTreeVo resource : treeNodes) {
if (!resourceTreeVo.getResourceId().equals(resource.getResourceParentId())) continue;
if (resourceTreeVo.getChildResourceVo() == null) {
resourceTreeVo.setChildResourceVo(new ArrayList());
}
resourceTreeVo.getChildResourceVo().add(TreeUtil.findMenuChildren(resource, treeNodes));
}
return resourceTreeVo;
}
public static List<ResourceTreeVo> recursiveTree(List<ResourceTreeVo> list) {
ArrayList<ResourceTreeVo> trees = new ArrayList<ResourceTreeVo>();
for (ResourceTreeVo treeNode : list) {
if (!StringUtils.isEmpty(treeNode.getResourceParentId())) continue;
trees.add(TreeUtil.findMenuChildren(treeNode, list));
}
return trees;
}
}
通过 jad 命令查看源码可以发现,此处的业务逻辑大致是通过 ResourceTreeVo
对象的 resourceParentId
字段把一个列表构建一个树。在 findMenuChildren
方法中存在递归调用,而且每一次调用都需要遍历整个 ResourceTreeVo
列表来查找子节点,时间复杂度为 O(n)。所以在 ResourceTreeVo
列表元素比较多的时候,会很耗时。
5. 解决问题
定位到问题就方便解决了,可以通过提前基于 list 构建一个 parentId->List<ResourceTreeVo>
的 map,每个节点查找子节点列表的时候可以从 map 中获取。这样整个构建树的时间算法为 O(n)。
案例二:应用线程连接数异常
问题描述:服务器句柄数耗尽,查看发现某个应用占用句柄数较多。
1. thread 命令查看线程信息
启动 Arthas,附加到对应的 java 进程,执行 thread 查看线程情况。
# 查看线程情况
thread
看到有大量的 MasterListener-mymaster-*
线程处于连接状态,一直没有释放。
# 选择其中一个线程查看堆栈信息
thread id
发现这些线程是由 redis.clients.JedisSentinelPool$MasterListener
产生的,那么接下来就来查看一下 JedisSentinelPool$MasterListener
的调用情况。
2. stack 命令查看堆栈信息
stack redis.clients.jedis.JedisSentinelPool$MasterListener
触发一次应用请求,打印出如下堆栈信息:
通过调用链定位到 RedisUtil
类,发现每次请求否会触发 RedisUtil.getJedis
方法调用 JedisSentinelPool$MasterListener
,那么下一步我们反编译一下 REedisUtil
类。
3. jad 命令反编译查看代码
# 反编译RedisUtil类
jad cn.com.zybank.testredis.starter.RedisUtil
查看 getJedis
方法,发现 getJedis
每调用一次都会新建一个 JedisSentinelPool
。
通过分析发现,每次使用 redis 时,都会调用 getJedis
方法创建一个新的 JedisSentinelPool
,从而启动一个 MasterListener-mymaster-*
线程,由于该线程会一直保持监听,不会自动释放,故随着应用请求的增加线程数一直增加从而导致连接数占满。
4. 解决问题
针对该问题,只需创建一个全局的 JedisSentinelPool
,每次获取 redis 连接时都从该连接池获取即可,这里不再对代码进行展示。
四.总结建议
我行在使用 Arthas 以前,线上问题排查往往需要查网络、jps、jstack、jmap、jhat、jstat、hprof 等一系列操作,费时费力。目前,大多数的常见问题都可以使用 Arthas 轻松定位,迅速解决。
一键安装并启动 Arthas
- 方式一:通过 Cloud Toolkit 实现 Arthas 一键远程诊断
Cloud Toolkit 是阿里云发布的免费本地 IDE 插件,帮助开发者更高效地开发、测试、诊断并部署应用。通过插件,可以将本地应用一键部署到任意服务器,甚至云端(ECS、EDAS、ACK、ACR 和 小程序云等);并且还内置了 Arthas 诊断、Dubbo工具、Terminal 终端、文件上传、函数计算 和 MySQL 执行器等工具。不仅仅有 IntelliJ IDEA 主流版本,还有 Eclipse、Pycharm、Maven 等其他版本。
推荐使用 IDEA 插件下载 Cloud Toolkit 来使用 Arthas:http://t.tb.cn/2A5CbHWveOXzI7sFakaCw8
- 方式二:直接下载
地址:https://github.com/alibaba/arthas。
随着我行全面深化使用 Arthas,也发现了一些有待改进提升的功能,希望进一步优化完善。
- 在进行 trace 的时候,只要调用链中有异步,堆栈就会断掉,无法 trace 到子线程内部,只能手动逐层跟进 trace,效率较低。
- 希望 tt 命令能够添加异步开关,如果开关开启, 那么 COST 即可显示异步得到结果的耗时。
截至目前,Arthas GitHub Star 已经突破 2.4 万了,希望 Arthas 能够获得更多全球开发者的关注和喜爱,也期待更多像 Arthas 一样的国内优质项目能够开源。
Arthas 征文活动火热进行中
Arthas 官方正在举行征文活动,如果你有:
- 使用 Arthas 排查过的问题
- 对 Arthas 进行源码解读
- 对 Arthas 提出建议
- 不限,其它与 Arthas 有关的内容
欢迎参加征文活动,还有奖品拿哦~点击投稿
“阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”