中原银行Arthas实践之路

3,120 阅读7分钟

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主要消耗在TreeUtilfindMenuChildren方法上,通过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(n2)。所以在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,也发现了一些有待改进提升的功能,希望进一步优化完善。

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

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