排查工具与方法
开篇:线上问题排查,光会看日志是不够的
线上出了问题,第一反应是看日志。日志能解决大部分问题,但总有些时候日志不够用——没打日志的地方出了 bug、性能瓶颈藏在某个不起眼的方法里、第三方库的行为不符合预期。这时候你需要更强大的武器。
这篇文章从 Arthas 的核心用法讲起,深入到字节码增强的原理,再到排查方法论的完整思维框架。不追求面面俱到,但力求每个知识点都能在真实的线上排查中派上用场。
一、Arthas 核心命令实战
Arthas 统计方法耗时的原理
Arthas 在统计方法耗时时用的是字节码插桩技术——在需要监控的方法中插入代码,方法执行前记录开始时间,执行完记录结束时间,算差值就是执行时长。
相比 AspectJ 等 AOP 框架,Arthas 的优势在于动态插桩——不需要改代码、不需要重启应用、不需要提前埋点。直接在运行中的 JVM 上操作,无侵入式监控。这在线上排查场景中价值巨大。
很多人说线上不能用 Arthas。其实这玩意是阿里开源的,我们线上定位慢接口都在用。当然有些操作需要注意——拉堆 dump、线程 dump 可能导致 STW,但这和 Arthas 本身没关系,是这些操作本身的特性,你用其他命令执行也一样。
用 trace 定位接口慢在哪
trace 是 Arthas 中定位性能问题最常用的命令,它展示方法内部每个调用的耗时:
# 安装
curl -L http://start.alibaba-inc.com/install.sh | sh
# 启动
sh as.sh
# 查看方法内部调用耗时,只捕获耗时超过50ms的请求
trace com.example.service.OrderService createOrder '#cost > 50' -n 3输出示例:
`---[264.85ms] com.example.service.OrderService:createOrder()
+---[0.01ms] com.example.request.OrderRequest:getTenant() #95
+---[0.001ms] com.example.request.OrderRequest:getProduct() #96
+---[221.88ms] com.example.domain.PriceDomainService:queryPrice() #167
+---[0.002ms] com.example.service.OrderService:calcDiscount() #170
`---[0.01ms] com.example.service.OrderService:buildResponse() #170一眼就能看出来——总耗时 265ms 中有 222ms 花在 queryPrice() 上,这就是性能瓶颈。接下来就去看这个方法的实现,分析是 SQL 慢了、缓存没命中,还是外部调用慢了。
几个实用技巧:-n 1 只捕获一次就退出;'#cost > 1000' 只看耗时超过 1 秒的请求;如果方法调用层次很深可以多次 trace 逐层深入。
用 sc 命令确定中间件版本
线上排查经常遇到一个问题:这台机器上跑的中间件到底是什么版本?用 Arthas 的 sc 命令可以快速确认:
sc -d com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer输出会显示类的 classloader、jar 包路径和版本信息。在排查 jar 包冲突、版本不一致等问题时非常有用。
用 jstack 分析死锁
jstack 是 JDK 自带的线程堆栈分析工具。在排查死锁、线程阻塞等问题时非常好用。
写一段经典的死锁代码:两个线程分别锁住 o1 和 o2,然后互相等待对方持有的锁。程序输出两行后就不再打印了——死锁发生了。
用 jstack 查看线程堆栈:
jstack <pid>输出会非常明确地告诉你:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f01... (object 0x00000007d6aa2c98)
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f01... (object 0x00000007d6aa2ca8)
which is held by "Thread-1"堆栈信息清清楚楚:Thread-1 锁着 o2 等 o1,Thread-0 锁着 o1 等 o2。两个线程互相持有对方需要的资源,死锁确认。找到原因后就可以针对性修改代码了。
Arthas 常用命令速查表
除了 trace 和 sc,Arthas 还有一系列高频命令覆盖不同排查场景:
| 命令 | 用途 | 典型用法 |
|---|---|---|
dashboard | 实时系统面板:线程、内存、GC 一览 | dashboard |
thread | 查看线程状态,定位死锁和阻塞 | thread -n 5(CPU 最高的 5 个线程)thread -b(查找阻塞线程) |
watch | 观察方法入参、返回值、异常 | watch com.example.Service method '{params, returnObj, throwExp}' -x 3 |
trace | 方法内部调用耗时链路 | trace com.example.Service method '#cost > 100' |
stack | 查看方法调用栈(谁调了这个方法) | stack com.example.Service method |
jad | 反编译类的源码(确认线上代码版本) | jad com.example.Service |
sc | 查看类加载信息(jar 路径、版本) | sc -d com.example.Service |
ognl | 执行表达式(查看静态变量、调用方法) | ognl '@com.example.Config@timeout' |
heapdump | 导出堆快照 | heapdump /tmp/dump.hprof |
memory | 查看 JVM 各区域内存占用 | memory |
monitor | 统计方法调用次数、成功率、RT | monitor com.example.Service method -c 5 |
tt | 时空隧道,记录方法调用并可回放 | tt -t com.example.Service methodtt -p -i 1001(回放第 1001 次调用) |
classloader | 查看类加载器树和加载的类数量 | classloader -t |
watch 命令详解(线上排查最灵活的命令之一):
# 看入参和返回值,展开 3 层
watch com.example.OrderService createOrder '{params, returnObj}' -x 3
# 只看抛异常的调用
watch com.example.OrderService createOrder '{params, throwExp}' -e -x 3
# 条件过滤:只看金额大于 1000 的请求
watch com.example.OrderService createOrder '{params, returnObj}' 'params[0].amount > 1000' -x 3thread 命令详解(CPU 飙高排查利器):
thread -n 3 # 显示 CPU 占用最高的 3 个线程及其堆栈
thread -b # 直接定位阻塞其他线程的线程(死锁检测)
thread --state WAITING # 查看所有 WAITING 状态的线程
thread 42 # 查看指定线程 ID 的堆栈ognl 命令详解(不改代码查看运行时状态):
# 查看静态变量的值
ognl '@com.example.config.AppConfig@MAX_RETRY'
# 调用静态方法
ognl '@java.lang.System@getProperty("java.version")'
# 查看 Spring 容器中的 Bean
ognl '#context=@com.example.App@context, #context.getBean("orderService").toString()'二、字节码增强原理
为什么需要字节码增强?
假设你要对一个方法做性能监控,最直接的方式是在方法入口和出口加计时代码:
public void businessMethod() {
long start = System.nanoTime();
// 业务逻辑
long elapsed = System.nanoTime() - start;
System.out.println("耗时: " + elapsed + "ns");
}问题是:如果有几百个方法都要监控,你不可能每个都手动加。代码重复、可读性差、容易漏。
字节码增强技术就是用来解决这个问题的——在编译期或运行期自动修改字节码,向方法入口和出口插入监控代码,不改源码就能实现统一的性能监控。
ASM 实战:自动插入计时代码
Java 字节码插桩的步骤:
- 生成目标类的字节码(javac 编译)
- 解析字节码,识别需要插桩的代码区域
- 用字节码生成库(ASM、Javassist 等)插入额外字节码
- 将修改后的字节码写回磁盘或内存
以 ASM 为例,下面的代码对 Example 类的 method() 方法做字节码插桩,自动在方法入口插入 Monitor.start(),在方法出口插入 Monitor.end():
public class MonitorTransformer {
public static byte[] transform(byte[] classBytes) {
ClassReader reader = new ClassReader(classBytes);
ClassWriter writer = new ClassWriter(
ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = new ClassVisitor(Opcodes.ASM5, writer) {
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(
access, name, desc, signature, exceptions);
if ("method".equals(name) && "()V".equals(desc)) {
mv = new MethodVisitor(Opcodes.ASM5, mv) {
@Override
public void visitCode() {
super.visitCode();
// 方法入口:插入 Monitor.start()
mv.visitMethodInsn(INVOKESTATIC,
"Monitor", "start", "()V", false);
}
@Override
public void visitInsn(int opcode) {
// 方法出口:插入 Monitor.end()
if (opcode == RETURN) {
mv.visitMethodInsn(INVOKESTATIC,
"Monitor", "end", "()V", false);
}
super.visitInsn(opcode);
}
};
}
return mv;
}
};
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
}
}Arthas 底层就是用类似的技术实现的——通过 Java Agent 机制在运行时动态修改字节码,插入计时逻辑。这就是为什么 Arthas 不需要改代码、不需要重启就能监控任意方法。
三、排查方法论总结
从现象到根因的思维框架
线上排查最怕的不是问题难,而是没有方向。有了方法论,至少不会手忙脚乱。
第一步:确认现象。 不要听别人转述,自己去看监控、看日志。"接口慢了"到底是 RT 从 50ms 变成 500ms 还是从 50ms 变成 5s?差别巨大。
第二步:缩小范围。 是所有接口都慢还是某个接口慢?是所有机器都有还是某台机器?是持续的还是间歇性的?每一个维度的排除都在缩小排查范围。
第三步:定位根因。 用工具验证假设。怀疑是 SQL 慢就去看慢查询日志和执行计划,怀疑是 GC 就看 GC 日志,怀疑是外部依赖就看调用链。
第四步:解决问题。 如果能快速修复就修复,如果需要时间就先做应急处理(降级、限流、扩容),再慢慢修根因。
常见排查场景速查
服务器 SSH 连不上:先确认网络是否通畅。常见原因——磁盘满了(SSH 无法写日志/临时文件)、CPU 打满(无法处理新连接)、网络问题(本地到服务器不通)、SSH 端口未开或被防火墙拦截(较少见)。
服务器磁盘满了:先用 df -h 查哪个分区满了,du -sh /* 找占用最大的目录。大多数情况是日志太多——到日志目录下看文件大小,历史日志如果已完成 ELK 采集就直接 rm -rf。如果只有一个日志文件且很大(logback 没配好),不能直接删(进程还在用),用 > file_name 清空内容。然后配好日志轮转、定期清理脚本、磁盘监控告警(80% 就告警)。
# 查看磁盘使用
df -h
du -sh /*
# 找大于1G的文件
find / -type f -size +1G -exec ls -lh {} \;端口冲突:两个程序抢同一个端口,后启动的 bind 失败。用 lsof -i:8080 或 netstat -tulnp | grep 8080 查谁在用。要么杀掉占用的进程,要么改端口号。建议用 1024-49151 范围的端口。
四、常见场景面试题精选
RocketMQ 消费堆积排查实录
这是一个真实的线上问题。业务需要将 Spring Cloud Stream 的消息配置改成 RocketMQ native,改了一个 consumer 后发布了一半机器开始灰度,结果出现消息堆积。
现象:灰度观察时发现消息堆积,队列中的消息没有被及时消费。
定位过程:根据经验怀疑是订阅关系不一致。去 MQ 控制台查看,果然——已发布的机器和未发布的机器订阅关系不同。已发布的机器只有 RocketMQ native 的订阅,而未发布的机器多了 Spring Cloud Stream 的订阅。
根因分析:深挖源码发现,RocketMQ 要求一个 ConsumerGroup 只能对应一个 MQConsumerInner(1:1 映射)。但 Spring Cloud Stream 启动时会自己 new 一个 MetaPushConsumer——这样同一个 ConsumerGroup 就有了两个 MQConsumerInner,RocketMQ 用新的替代了旧的。结果 Spring Cloud Stream 的订阅关系被 RocketMQ native 的覆盖掉了。
集群中一半机器有这个订阅关系、一半没有,MQ broker 不确定该不该投递消息,只能先堆积。
解决方案:给 Spring Cloud Stream 的消费者用不同的 ConsumerGroup ID,不和 RocketMQ native 共用。
# 修改前:共用 CID_CONSUMER_A(出问题)
spring.rocketmq.consumers[0].consumer-group=CID_CONSUMER_A
spring.cloud.stream.bindings.consumerB.group=CID_CONSUMER_A
# 修改后:分开用不同的 group
spring.rocketmq.consumers[0].consumer-group=CID_CONSUMER_A
spring.cloud.stream.bindings.consumerB.group=CID_CONSUMER_B教训:问题原因不复杂,但很多人分析到第一层(订阅关系不一致导致堆积)就不继续了。要有更深入的探索精神。另外生产环境尽量不要搞两套配置,增加理解成本。
运行期发生 ClassNotFoundException
编译正常但运行期报 ClassNotFoundException,通常和类加载有关。
动态加载类:Class.forName()、ClassLoader.loadClass()、反射等方式在运行期加载的类不会在编译期检查,如果运行时找不到就抛异常。
jar 包冲突(最常见):多处依赖了同一个包的不同版本,Maven 仲裁后只保留了一个版本。如果仲裁结果是旧版本,新版本中新增的类就找不到了。线上出现 ClassNotFoundException 时优先考虑 jar 包冲突。
类路径动态变化:运行时类被动态移除或替换,尝试加载被移除的类就会报错。
Java 模块化系统:JDK 9+ 的模块系统中,模块依赖声明不正确或模块不存在,会导致类不可见。
服务器被注入挖矿木马
这是个人云服务器遇到的真实案例。收到告警说挂了挖矿程序。
排查过程:
top查看进程——发现kswapd0占 CPU 异常高。这个本来是 Linux 的内存交换后台进程,但正常情况下不会持续高 CPU,是个"老演员"。netstat -antlp看网络连接——发现一个瑞士 IP 在和 kswapd0 通信,还有两个罗马尼亚 IP 在和 rsync 进程通信。进入
/proc/<pid>找进程文件路径——最终定位到是 git 用户下的一个文件夹。
处理步骤:删除恶意文件夹 -> kill 掉进程 -> 检查并清除 crontab 定时任务 -> 删除被黑的账号 -> 清除该用户的 SSH 密钥 -> 恢复正常。
防范措施:定期检查异常进程和网络连接、限制 SSH 登录方式(禁用密码登录只用密钥)、及时更新系统补丁、非必要不开放公网端口。
Java 进程突然挂了怎么排查
先区分是假死还是真挂了。
假死:进程还在但不响应请求。常见原因是死锁(互相等待资源)、活锁(不断重复操作但无进展)、无限等待(等待永远不会满足的条件)、IO 阻塞、长时间 GC 暂停。用 jstack 查线程状态,看有没有 BLOCKED 或 WAITING 状态的线程。
真挂了:OOM(检查有没有 OutOfMemoryError,增加堆内存或优化代码)、系统资源耗尽(CPU/磁盘打满)、宿主机挂了(容器或虚拟机的物理机异常)、被 kill 了(尤其是 kill -9,查看系统的 dmesg 日志确认)。
小结
线上排查是一门手艺,光知道工具命令远远不够,更重要的是思维方式。
三条核心原则:
不要猜,要验证。 每一个结论都应该有对应的数据或日志支撑。怀疑是 GC 问题就去看 GC 日志,怀疑是 SQL 慢就去看执行计划,不要凭感觉下结论。
先止血,再治本。 线上问题第一优先级是恢复服务,可以通过降级、限流、扩容等手段快速止血。根因分析可以恢复后慢慢做。
工具只是手段,方法论才是武器。 Arthas、jstack、jmap 都是工具,从现象到分类到定位到解决的思维框架才是真正的武器。掌握了方法论,换任何工具都能排查问题。
Arthas 是排查利器,字节码增强是它的底层原理。理解了原理你会知道 Arthas 能做什么、不能做什么、什么时候该用什么命令。这比死记硬背命令参数有用得多。