19

中原银行 Arthas 实践之路

 4 years ago
source link: http://dockone.io/article/10432
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

【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

vaIreqM.png!web

/telnet访问:ip:3658

qeuaqyz.png!web

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

2.trace

查看方法内部调用路径,并输出方法路径上的每个节点上耗时

trace ClassName methodName

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

IRJNfaA.png!web

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

iq6BJf3.png!web

三. 应用实践案例

下面记录一些我行 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

f2MBF33.png!web

通过 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&lt;ResourceTreeVo> 的 map,每个节点查找子节点列表的时候可以从 map 中获取。这样整个构建树的时间算法为 O(n)。

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

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

iAJvUbm.png!web

1. thread 命令查看线程信息

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

查看线程情况

thread

uQNjq2e.png!web

fqEreqr.png!web

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

选择其中一个线程查看堆栈信息

thread id

v6nAzyJ.png!web

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

2. stack 命令查看堆栈信息

stack redis.clients.jedis.JedisSentinelPool$MasterListener

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

z6J3UjM.png!web

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

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

反编译RedisUtil类

jad cn.com.zybank.testredis.starter.RedisUtil

3eQFnyv.png!web

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

veqeqqA.png!web

通过分析发现,每次使用 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 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK