阅读 469

中原银行 Arthas 实践之路

作者 | 于爽 中原银行系统研发工程师,目前在技术平台室中间件小组从事分布式缓存、消息队列等相关工作。

【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

1.png

/telnet访问:ip:3658

2.png

当线上应用出现问题时,可以将问题机器隔离起来,通过Arthas在启动时指定target-ip,多方技术人员可同时通过远程连接进行问题排查。

2.trace

# 查看方法内部调用路径,并输出方法路径上的每个节点上耗时 
trace ClassName methodName
复制代码

使用 trace 命令可以一层一层追踪耗时在哪里 ,在进行性能调优的时候十分有效。

3.png

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
复制代码

4.png

三. 应用实践案例

下面记录一些我行 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 主要消耗在 TreeUtilfindMenuChildren 方法上,通过 monitor 命令查看方法的具体调用次数与耗时。

# 5s为一个统计周期,统计TreeUtil中findMenuChildren方法的耗时
monitor -c 5 ***.TreeUtil findMenuChildren
复制代码

5.png

通过 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)。

案例二:应用线程连接数异常

问题描述:服务器句柄数耗尽,查看发现某个应用占用句柄数较多。

6.png

1. thread 命令查看线程信息

启动 Arthas,附加到对应的 java 进程,执行 thread 查看线程情况。

# 查看线程情况
thread 
复制代码

7.png

8.png

看到有大量的 MasterListener-mymaster-* 线程处于连接状态,一直没有释放。

# 选择其中一个线程查看堆栈信息
thread id  
复制代码

9.png

发现这些线程是由 redis.clients.JedisSentinelPool$MasterListener 产生的,那么接下来就来查看一下 JedisSentinelPool$MasterListener 的调用情况。

2. stack 命令查看堆栈信息

stack redis.clients.jedis.JedisSentinelPool$MasterListener
复制代码

触发一次应用请求,打印出如下堆栈信息:

10.png

通过调用链定位到 RedisUtil 类,发现每次请求否会触发 RedisUtil.getJedis 方法调用 JedisSentinelPool$MasterListener,那么下一步我们反编译一下 REedisUtil 类。

3. jad 命令反编译查看代码

# 反编译RedisUtil类
jad cn.com.zybank.testredis.starter.RedisUtil
复制代码

11.png

查看 getJedis 方法,发现 getJedis 每调用一次都会新建一个 JedisSentinelPool

12.png

通过分析发现,每次使用 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:t.tb.cn/2A5CbHWveOX…

  • 方式二:直接下载

地址:github.com/alibaba/art…

随着我行全面深化使用 Arthas,也发现了一些有待改进提升的功能,希望进一步优化完善。

  • 在进行 trace 的时候,只要调用链中有异步,堆栈就会断掉,无法 trace 到子线程内部,只能手动逐层跟进 trace,效率较低。
  • 希望 tt 命令能够添加异步开关,如果开关开启, 那么 COST 即可显示异步得到结果的耗时。

截至目前,Arthas GitHub Star 已经突破 2.4 万了,希望 Arthas 能够获得更多全球开发者的关注和喜爱,也期待更多像 Arthas 一样的国内优质项目能够开源。

Arthas 征文活动火热进行中

Arthas 官方正在举行征文活动,如果你有:

  • 使用 Arthas 排查过的问题
  • 对 Arthas 进行源码解读
  • 对 Arthas 提出建议
  • 不限,其它与 Arthas 有关的内容

欢迎参加征文活动,还有奖品拿哦~点击投稿

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”