ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Arthas实战:Java线上服务故障诊断与性能调优指南

Arthas实战:Java线上服务故障诊断与性能调优指南 1. 为什么需要Arthas线上调试的痛点是真实存在的先问一句你们线上出问题的时候是不是还在用最原始的方式排查打日志、加System.out、改代码重启、把测试环境数据翻来覆去复现……我干了十来年Java后端这些路都走过说实话每次都像在黑暗里摸开关运气好摸到了运气不好折腾一整天。先说清楚Arthas是什么。阿里开源的一款Java诊断工具名字取自《魔兽世界》里的阿尔萨斯口号是开箱即用、线上诊断。它能让你直接连上线上运行的Java进程在不重启、不改代码、不重新发布的情况下实时查看方法调用参数、返回值、异常堆栈、类加载信息、JVM状态、线程状态甚至还能动态修改已加载的类的字节码逻辑。对你没听错就是线上直接改逻辑。我最早接触Arthas是2019年前后那时候公司的微服务已经拆了几十个每次线上告警都跟拆盲盒一样。有一次用户反馈某个接口偶发超时日志里什么都没有我们几个后端围在屏幕前看了一下午最后只能灰溜溜地加日志重新发布第二天又复现又加日志来来回回折腾了三版才定位到问题。后来我在技术群里看到有人提到Arthas抱着试试看的心态装了一下用trace命令盯了几分钟接口调用直接看到了耗时瓶颈在哪那一刻我才意识到以前那么多无效加班都是工具没选对。这篇文章适合谁看只要你是做Java后端开发的或者正在维护线上服务、中间件、微服务哪怕你只是刚接触Java生态我都建议你花半小时把这套工具的基本用法过一遍。它不需要你在项目里引入任何依赖不需要改代码只需要一个命令行终端和一个能找到目标Java进程的环境。下面我把自己的使用经验和踩过的坑完整写出来。2. Arthas快速上手从下载到第一次attach2.1 环境要求与几种安装方式Arthas对运行环境的要求非常低JDK 6及以上版本都支持这意味着哪怕你还在维护老掉牙的JDK 6系统它照样能工作。它的原理是通过Java的Instrumentation机制JDK 5之后就有动态attach到目标JVM所以不需要目标应用做任何改造也不会影响应用的启动过程。安装方式我试过三种最简单的是直接下载官方发布的zip包curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar跑起来之后它会列出当前机器上所有正在运行的Java进程你输入序号选择要attach的目标进程就进入了Arthas的交互式命令行。第二种方式是使用as.sh脚本适合Linux服务器环境下载后赋予执行权限再执行./as.sh效果和上面一样。第三种方式是把它集成到项目的启动脚本里这通常用于固化团队内部的排查流程我个人更推荐临时使用的时候选前两种轻量、不污染项目。这里有个小地方要提醒arthas-boot.jar只是一个引导程序它运行时会自动下载完整的Arthas包并缓存在~/.arthas目录下。有些公司内网环境访问不了外网第一次启动会卡在下载环节这时候可以找一台能访问外网的机器把完整包下载好整体拷贝到内网用。2.2 首次启动attach到目标进程的完整演示我拿一个典型的Spring Boot服务来演示。先在服务器上执行java -jar arthas-boot.jar输出一般长这样[INFO] arthas-boot version: 3.7.x [INFO] Process 4521 is the process of the current shell, skip. [INFO] Found existing java process, please choose one and input the serial number: * [1]: 8120 com.example.demo.DemoApplication [2]: 9033 org.apache.zookeeper.server.quorum.QuorumPeerMain输入1回车Arthas会开始attach看到Artahs的Logo和$提示符就说明连接成功了。这个过程中会自动应用一个Java Agent到目标JVM理论上会有一次短暂的JVM停顿一般几毫秒到几十毫秒级别对于绝大多数线上服务来说无感知。不过如果是对停顿极其敏感的金融交易系统建议先在压测环境验证一下。从交互式命令行退出输入quit或exit就行但这里有个很多人忽略的点Arthas attach之后默认会在后台保留一个服务端方便下次快速重连。如果你彻底不想让它在目标JVM上继续驻留需要执行stop命令才能真正卸载Agent否则进程重启之前它都一直在。这个细节影响到一些安全合规要求严格的场景后面我会单独展开。2.3 连接不到目标进程的几种常见原因第一次用的时候十有八九会遇到连不上的情况不用慌大多数是环境和权限问题。我碰到过的典型场景第一个是权限不够。Arthas attach的过程涉及对目标进程的内存操作Linux下必须和目标是同一个系统用户否则会报Can not attach to target process之类的错误。解决办法是用sudo -u 运行Java进程的用户切换到对应用户再执行Arthas或者干脆用和目标应用一样的账号登录服务器。第二个是容器环境。现在服务大部分跑在Docker或者K8s里宿主机上执行java -jar arthas-boot.jar是看不到容器内的Java进程的。正确姿势是docker exec -it 容器名 /bin/bash进去容器内操作或者用kubectl exec进入到Pod里去。如果你使用的是精简镜像容器里可能连curl都没有打包的时候记得把基础工具带上或者用Arthas的Agent方式远程attach这个相对复杂普通场景下先进容器操作就够了。第三个是JDK版本差异。Arthas依赖tools.jar里的一些类来做attach如果你用的是纯JRE环境跑Java应用可能缺少相关类导致attach失败。解决方法要么是安装完整的JDK要么让Java进程启动时的JAVA_HOME指向完整JDK路径。这一点在老系统上特别常见。3. 核心命令实战从入门到高频使用的那些命令3.1 dashboard一眼看尽JVM全局状态attach成功之后我建议新手第一件事就是敲dashboard这个命令会以仪表盘的形式展示目标JVM的实时状态包括内存堆和非堆的使用情况、GC次数与耗时、线程数量、JIT编译统计以及各个线程的CPU占用排行。效果类似你打开了一个命令行版的JConsole或者VisualVM但不需要任何GUI环境服务器上就能看。我每次定位线上问题第一步永远是dashboard。为什么因为它能最快告诉我这个进程到底哪里不对劲——内存是不是快到上限了老年代GC是不是频繁得离谱哪个线程在疯狂吃CPU这些信息是后续排查方向的指南针。有一个真实的例子当时一个服务告警频繁Full GC我连上之后看到Eden区瞬间被占满、老年代持续增长、CPU排行第一的是某个业务线程顺着这个线去查发现是某个定时任务里有个大List被反复加载优化之后GC曲线立刻平缓下来。dashboard默认1秒刷新一次你可以暂停刷新或者加-n参数控制执行次数比如dashboard -n 5只刷新5次适合看完就退出继续做别的操作。附带一说dashboard里还能看到每个CPU占用高的线程的id配合接下来的thread命令可以直接定位到是哪个业务代码在跑。3.2 thread线程级问题的定位利器线程问题尤其是死锁和线程池耗尽是线上服务最让人头疼的一类问题。从前的做法是jstack抓一把线程快照然后对着上百行堆栈慢慢找眼睛都看花了。Arthas的thread命令把这事简化了不少。直接敲thread -n 3它会按照CPU占用率从高到低列出当前最忙的3个线程包括线程ID、名称、状态、CPU占用百分比以及对应的堆栈摘要。之前排查过一个CPU飙到300%的问题就是靠这条命令直接定位到某个线程卡在一个正则表达式的匹配逻辑里出不来——那个正则写得太糟糕灾难性回溯把CPU烧穿了。thread -b则是专门用来找死锁的它会检测当前JVM里是否存在死锁存在的话直接打印出死锁涉及的线程和它们相互等待的锁信息省去了手动分析多个线程堆栈的麻烦。还有thread --state WAITING这种用法可以筛选出处于特定状态的线程比如想看有多少线程卡在等待IO上一条命令就出来。顺带提一个排查线程池队列堆积的小技巧先用thread -n 1找到最忙的线程再用watch或stack去观察它正在执行的代码路径往往能快速找到任务堆积的源头。这套组合拳在我日常排障中使用频率极高。3.3 watch与trace方法级别的显微镜如果说dashboard和thread是看面和线那watch和trace就是精准到点的观测工具也是Arthas的核心战斗力。watch命令可以观测指定方法的入参、返回值、抛出的异常甚至是方法执行后的对象内部字段。最基础的用法watch com.example.demo.OrderService createOrder {params, returnObj, throwExp} -x 2{params, returnObj, throwExp}是我指定的观测表达式意思是把入参、返回值、异常都打印出来-x 2控制嵌套对象的展开深度。这样当你怀疑某个接口的数据有问题但又不确定传进来的参数是什么时直接watch方法就能看到每一次调用的实际情况。trace命令则用来分析方法内部各段代码的耗时。它会把方法内部所有子调用的耗时和调用次数一层层展示出来。比如一个下单接口整体耗时800ms你用trace一看发现80%的时间花在了调用库存服务的那一行问题定位就变得非常直接。我自己最常用的是这个组合先trace看整体瓶颈在哪然后对瓶颈方法watch看具体参数再结合stack查看这个方法的调用来源。这三个命令配合起来几乎可以把一个接口从前端请求到DB落地的完整链路在几分钟之内摸清楚。需要提醒的是这些命令都支持条件过滤比如watch com.example.OrderService createOrder {params} params[0] 100当第一个参数大于100时才打印。这个能力在线上流量大的时候特别有用可以只观测你关心的那部分流量减少对系统的影响。3.4 其他出场率很高的命令速览除了上面三个主角Arthas还有一批场景化命令我按使用频率排个序sc和sm是用来查看类的加载信息和类的方法列表的效果类似JVM层面的反射查询器。当你需要确认某个类或方法在线上到底存不存在、由哪个ClassLoader加载的可以用它们快速确认。jad是反编译命令能把线上已加载的类直接反编译成Java源码。用过之后你就知道有多爽——不用再翻代码仓库排查是不是不是最新版本直接线上看字节码反编译出来的真相。比如排查一个我明明改了代码重启了为什么还是老逻辑的问题jad一下就能看出线上实际加载的类到底是什么样的。redefine是热更新命令可以临时把线上某个类替换成新的字节码。注意我说的是临时——JVM重启之后修改就会丢失这个特性适合应急止血比如某个方法里有明显的空指针可以快速修掉让业务先恢复再走正规流程去发版。ognl命令可以在线执行OGNL表达式它实际是个瑞士军刀可以用来读取任意对象的属性、调用方法甚至修改静态变量。这个命令对老手来说威力巨大但对新手有破坏风险我建议没把握的时候不要随便往里写值。heapdump命令可以触发一次堆内存快照导出后可以用MAT等工具离线分析内存泄漏点比用jmap命令要方便一些因为它直接在Arthas交互式环境里就能触发和管理。最后还有monitor命令可以对方法进行周期性统计显示某段时间内的调用次数、成功次数、失败次数和平均耗时。它适合用来快速判断某个方法是不是在持续报错配合告警使用效果很好。4. 实战案例一次典型的线上接口超时排查全记录4.1 问题背景和初步排查这里我完整拆解一个我在生产环境实际处理过的案例帮你把这些命令串起来。场景是这样的一个电商促销活动上线后订单查询接口的P99延迟从原本的150ms飙到了2秒以上监控系统大量告警用户反馈页面转圈越来越久。所有排查都是从确认现象开始的。我先登录服务器用dashboard看了一眼这个订单服务的JVM状态发现老年代GC平均耗时从几十毫秒涨到了300多毫秒而且CPU排行靠前的线程几乎都在GC相关线程上。当时的第一直觉是内存出问题了可能是堆里有大量对象堆积。但GC只是表象真正要回答的问题是谁在制造这些垃圾对象。来看具体操作。我执行thread -n 3发现CPU占用最高的业务线程停在一个叫OrderEsMapper.queryByCondition的方法上而且显示的堆栈中有一段明显的大集合遍历。这给了我一个很强的怀疑方向。接着用trace接住这个思路trace com.example.order.dao.OrderEsMapper queryByCondition结果很快就跳出来这个方法内部执行了一次深度分页查询——pageNo5000, pageSize20也就是说它从ES里取出了10万条数据再在内存里做后置过滤。这个查询的逻辑本身在测试环境数据量小的时候完全没问题但线上数据量一上来每次请求就要拉全量数据再过滤堆内存被反复塞满GC就炸了。这就是典型的代码在数据量小时没问题数据量大了问题就暴露的情况。4.2 用Arthas定位根因的完整过程定位到深度分页之后我并没有马上改代码——因为这种问题通常不是单点而是一整类查询都可能有隐患。我继续用watch去观察这个方法的入参分布watch com.example.order.dao.OrderEsMapper queryByCondition {params[0].getPageNo(), params[0].getPageSize()} -x 2跑了几分钟之后发现不只是订单查询商品列表、用户订单历史等多个查询都存在类似的深度分页调用说明这是公共查询组件层的设计问题不是一个接口能修复的事。接着为了确定哪些调用方触发了这些大查询我用stack命令跟踪了其中一次调用来源stack com.example.order.dao.OrderEsMapper queryByCondition这条命令会打印出触发该方法时完整的调用栈从外部接口一路到数据库访问层。通过聚合多次调用栈我整理出了三个外部接口的入口并且确认了各自的触发概率。这样产品和技术就能针对具体的入口做分页策略调整或者增加查询条件约束。整个过程大约花了半个小时。对比以前一套加日志→发版→等待复现→重新抓日志的流程省下的时间不是一点半点。这也是我为什么强烈建议后端团队把Arthas作为线上排障的标配工具——它解决的不是能不能搞定问题的问题而是能不能在问题影响扩大之前搞定的问题。4.3 善后与复盘时的Arthas用法定位到根因、修复发布之后Arthas依然有用。我在修复版本上线后用monitor对之前超时的几个方法做了持续观察monitor com.example.order.dao.OrderEsMapper queryByCondition -c 10-c 10表示每10秒统计一次调用情况。这个方法会持续打出调用次数、成功次数、失败率、平均耗时。确认QPS回来了、P99曲线降回正常值才算真正close掉这个case。复盘的时候Arthas也帮了大忙——因为它能在线确认修复后的代码是否真的生效而不用靠我觉得应该没问题来安慰自己。尤其在多实例部署的时候如果发布不均或者灰度有问题直接用jad对比各个实例上同一个方法的字节码就能发现哪些实例还是老代码这对于排查为什么明明修复了还是有告警这类问题实在是太有用了。5. 进阶技巧热更新、性能影响与容器环境的使用经验5.1 热更新redefine的正确使用姿势redefine是Arthas里最危险也最有价值的命令之一因为它能直接替换线上类的字节码。我的态度是可以用来应急但不要当成常规操作而且必须知道它的边界。使用过程一般是这样本地把有问题的类改好编译出对应的.class文件然后传到服务器在Arthas里执行redefine -c classloaderHash /path/to/OrderServiceImpl.class类的ClassLoader哈希可以用sc -d查看。执行成功后后续进入该方法的调用会走新逻辑。但它有几个硬性限制我踩过之后印象很深。第一不能新增或删除方法、字段只能修改方法体内部的逻辑否则报UnsupportedOperationException。第二修改后的结果只存在于当前JVM进程重启就没了所以它是个临时补丁而不是线上发布。第三如果目标类被多个ClassLoader加载你得分别对每个ClassLoader的实例执行redefine。第四Spring等框架的代理类经常会包一层你需要redefine的是真正的实现类而不是代理类否则改了跟没改一样。我用redefine的场景基本都是线上有个明显空指针但重新发版要排队等审批这种紧急时刻。先快速把if (xx null) return这种逻辑塞进去止血业务恢复后再正常走发版流程。记住这只是给团队争取时间的手段不是解决问题的终点。5.2 Arthas对线上进程的性能开销到底多大很多人不敢在线上用Arthas担心它影响性能。这个担心可以理解但实际情况没那么可怕。Arthas Agent常驻状态对JVM的开销非常小它主要是在你需要的时候才织入观测代码观测结束之后对观测的类可以进行撤销恢复增强。所以关键点不在于用不用Arthas而在于你开着哪些观测命令、观测了多久。比如watch一个每分钟调用几十万次的方法且不做条件过滤、不做耗时限制那确实会引入明显的性能损耗。但如果你加上条件过滤只观测特定参数或特定异常发生时的情况对正常流量的影响完全可以忽略。我日常用的原则是控制台观测命令默认加过滤条件观测完成立即用stop或reset清理增强逻辑。reset命令可以把所有被增强过的类恢复为原始状态这是我最常用的善后操作。用完watch和trace之后我都会执行一次确保Arthas的探针不会残留在热点方法上。还有一个shutdown命令可以彻底关闭Arthas服务端并释放相关资源比stop更彻底。5.3 容器环境下的几个实战细节现在大部分Java服务都是在容器里跑的Arthas在容器环境使用有几个细节值得说。先说K8s。Arthas官方目前没有特别为K8s做一键插件所以常见的做法还是kubectl exec进Pod再操作。问题是很多基础镜像为了精简体积连unzip、curl这些工具都没有更别说JAVA_HOME指向的完整JDK了。我见过最坑的一次镜像里只有JRE没有JDKArthas attach直接失败。后来我们的做法是基础镜像统一换成带JDK的版本或者至少预留一个带调试工具的备用Pod平时缩容到0排查的时候临时扩容一个。还有一个多实例对不上的问题K8s里的服务往往有多个副本你attach进去的那一个Pod可能根本不在告警链路上。我建议先通过监控系统确认具体哪个实例异常再定向连进去。如果实在锁定不了可以先对每个实例执行dashboard看哪个的CPU、内存、GC指标明显异常再深入排查。容器环境的另一个小坑是PID namespace。Pod里的第一个进程PID通常是1Arthas attach的时候如果提示找不到目标进程先确认你用的是不是docker exec进入了正确的容器有些时候项目启动脚本会先拉起一个Shell再启动Java进程导致Java进程不是PID 1但Arthas会列出所有Java进程这影响不大。真正要注意的是如果你用docker exec进入容器后执行ps发现看不到Java进程很可能是容器没有安装ps命令不代表Java没在运行别被误导了。5.4 批量排查与脚本化使用的小心得Arthas除了交互式命令行还支持批处理方式。你可以把命令写进一个脚本文件然后用-f参数执行java -jar arthas-boot.jar -f /tmp/check.as脚本文件里每行一条Arthas命令适合做标准化的健康检查流程。比如把dashboard、thread -n 5、heapdump这几条固定打一遍其他同事只要跑一个脚本就能拿到一份基础的体检报告。这个方式在团队协作里非常实用我把它封装成了排查文档的一部分每次线上出问题大家先执行这个脚本再一起讨论效率高很多。脚本执行模式默认是非交互的命令输出会直接打印到终端方便重定向到日志文件。不过要注意有些命令比如watch因为需要等待触发在批处理模式下也支持但你需要指定观测时长或条件避免脚本挂在那里不结束。Arthas官方也提供了--async等参数来管理异步执行具体可以参考官方文档我这里不展开太多。6. 常见问题与避坑经验实录6.1 用Arthas过程中最常见的问题速查我把这些年身边同事和我自己踩过的坑整理成一个速查表基本上覆盖了90%的Arthas怎么不好用了的情况问题现象常见原因解决办法attach时报权限错误当前用户与目标进程运行用户不一致切换到目标进程同账号或使用root/对应权限执行提示Can not find tools.jar目标JVM使用纯JRE缺少tools.jar改用完整JDK运行目标应用或配置JAVA_HOME命令执行后无输出数据量小或方法未被调用检查方法签名、类名是否准确可用sc确认attach成功但命令全部报ClassNotFound目标应用使用了自定义/隔离ClassLoader用-c classLoaderHash参数指定类的加载器容器内无法找到Java进程未进入正确的容器或缺少ps指令确认Pod名称kubectl exec进入后检查热更新不生效redefine了代理类而非实现类jad确认类的真实身份redefine前再核实命令观测后性能明显下降未加过滤条件观测热点方法添加条件表达式完成后立即reset恢复退出Arthas后端口仍被占用服务端进程仍在目标JVM上运行执行stop命令停止服务端和Agent使用ognl修改静态变量后出问题表达式误操作或类型不匹配先读后写确认类型和线程安全必要时重启恢复脚本模式命令卡住不回显部分命令在非交互模式下行为不同使用-c执行单条命令或设置合理的超时/条件这张表是我在团队内部培训时反复更新过的版本每条都是真实遇到过的问题不是文档上抄来的。6.2 几个我踩过的、值得单独说说的坑第一个坑是ClassLoader的问题。我们当时的服务大量使用自定义可视化配置的插件机制每个插件由独立的ClassLoader加载。第一次用watch观测插件里的方法怎么都提示找不到类。后来才知道Arthas默认从系统ClassLoader扫描对于这种隔离环境必须用sc -d先查出目标类的ClassLoader哈希然后在命令里加上-c hash参数。这个细节在文档里写得很浅但实际操作中几乎每个人都要踩一次。第二个坑是Arthas的日志侵蚀。默认情况下Arthas会在用户目录下产生日志文件比如~/logs/arthas/arthas.log如果你用watch打印了大量数据这些日志会同步落盘。有一次排查的时候没注意本来是想观测几分钟结果命令忘了停一晚上下来用户目录被日志撑爆了差点把磁盘占满。现在我每次开长时间的观测都会先设置日志输出并定时检查磁盘空间。第三个坑是ognl执行了有副作用的方法。有一次我为了方便直接通过ognl调用了线上某个Service的一个内部方法去清理缓存结果这个方法内部依赖了Spring的代理机制直接调用绕过了一系列必要的事务逻辑导致了脏数据。那一次让我彻底记住了ognl是非常底层的诊断工具不是拿来绕过业务逻辑的除非你非常清楚自己在做什么否则不要调用有副作用的方法。第四个坑是Arthas版本太旧导致兼容问题。Arthas迭代速度很快JDK 11、JDK 17的模块化系统对旧版本Arthas的支持并不友好会出现莫名奇妙的attach失败或者命令结果为空。我后来养成了习惯定期在测试环境升级Arthas版本确保线上排查的时候用的版本是经过验证的。特别是当你维护的新老服务跨越多个JDK大版本时统一Arthas版本能少很多麻烦。6.3 给新手的建议和我们的团队落地经验最后聊点团队层面的经验。工具再好没人用也是白搭。让团队从jstack加日志切换到Arthas不是技术问题是习惯问题。我们当时做了三件事效果还不错。第一把Arthas的常用命令做成了团队Wiki文档配上真实的排查案例而不是官方文档的机械翻译。有案例、有背景、有结论同事遇到类似问题就知道该抄哪一段命令。第二建立了线上排障工具箱的概念把Arthas、jmap、jstack、jstat这些工具的适用场景做了一张区分表。Arthas解决的是方法级、代码级的定位jmap解决的是内存快照jstack解决的是线程快照各有分工互相补充不强求一个工具打天下。第三在团队内部搞过一次故障演练用一台测试环境模拟线上问题让每个后端开发轮流用Arthas去排查。经历过的同学都反馈说练完之后再遇到线上告警手不再抖了。从知道有Arthas这个东西到敢在线上用Arthas中间需要一个刻意练习的过程演练就是最好的催化剂。还有个细节尽量别在非常核心、流量极高的类上用watch打印全量信息即便是加了条件过滤也要留意。我见过同事在网关的核心路由方法上watch了所有请求直接把GC情况搞恶化的案例。观测本身也会产生对象分配在一些极端热路径上这个开销会被放大。所以我的习惯是先trace看看哪个方法值得深入再对目标方法精确加条件观测用完立刻reset。我自己从第一台生产服务器上战战兢兢敲下java -jar arthas-boot.jar到现在几乎每个排障周期都会用到它最大的感受是Arthas给后端开发者的不是一个命令而是一种线上代码可观测的思考方式。以前我们面对线上故障是盲猜加碰运气现在是可以有条不紊地取证、定位、修复。如果你所在的项目还停留在打日志发版等复现的阶段真心建议你从今天开始找一台测试环境服务装上Arthas先跑一遍dashboard和watch感受一下直接看到线上方法调用细节是什么体验。工具本身的学习成本并不高真正值钱的是你把它用进日常排障流程之后省下来的时间和少挨的骂。
返回列表