从慢SQL到OOM,一套日志驱动的问题定位方法论
线上问题不可怕,可怕的是你像无头苍蝇一样乱撞。
凌晨2点,报警群突然炸了。
"接口超时率飙升到30%!" "数据库CPU 98%!" "用户反馈页面打不开!"
你睡眼惺忪打开电脑,开始排查。看监控、查日志、登服务器……一顿操作猛如虎,一看进度原地杵。3小时过去了,问题还没定位到。
而隔壁工位的老王,10分钟就找到了原因——一条慢SQL把数据库拖垮了。
你和老王的差距在哪?不是技术,是思维方式。
一、90%的人排查问题的方式都是错的
先说一个反直觉的事实:大多数线上问题,不是靠"猜"解决的,而是靠"看"解决的。
什么叫"猜"?
"是不是缓存挂了?" → 去看缓存
"是不是数据库慢了?" → 去看数据库
"是不是代码有bug?" → 去看代码
这种排查方式有个专业名词——试错法。听起来很科学,实际上效率极低。因为你每次"猜"都要花时间验证,猜错了又要重新猜。
什么叫"看"?
先看日志,再下结论。
日志是什么?日志是系统给你写的"病历本"。系统哪里不舒服、什么时候开始不舒服、严重到什么程度,全都记在日志里。
高手排查问题,不是从"猜测"开始,而是从"阅读"开始。
二、日志分析的三层境界
第一层:看现象(青铜)
# 查看错误日志 tail -f /var/log/app/error.log | grep "Exception"
看到报错就去搜百度,搜到方案就去改。这是最基础的用法,但也是最低效的。
因为你只看到了"症状",没看到"病因"。
第二层:看关联(黄金)
# 查看错误发生前后的上下文 grep -B 10 -A 5 "OutOfMemoryError" /var/log/app/app.log
不只是看错误本身,还要看错误发生前10行、后5行。很多时候,真正的线索藏在"前因后果"里。
比如OOM之前,可能有一段大量对象创建的代码;慢SQL之前,可能有一个参数异常的请求。
第三层:看趋势(王者)
# 统计每分钟的错误数量 awk '/ERROR/{print $1,$2}' app.log | cut -d: -f1,2 | uniq -c | sort -rn不只看单条日志,而是看日志的分布和趋势。
错误是突然出现的,还是逐渐增多的?
错误集中在某个时间段,还是分散的?
错误和什么业务操作相关?
看现象是看病人的症状,看关联是看病人的病史,看趋势是看疾病的传播规律。
三、慢SQL排查:从发现到解决的完整流程
3.1 发现慢SQL
方式一:MySQL慢查询日志
# 查看慢查询配置 SHOW VARIABLES LIKE 'slow_query%'; # 开启慢查询日志(临时) SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; # 超过1秒记录 # 查看慢查询日志 tail -f /var/log/mysql/slow.log
方式二:Arthas在线诊断(推荐)
# 启动Arthas java -jar arthas-boot.jar # 监控SQL执行耗时 trace org.apache.ibatis.mapping.MappedStatement query -n 5
方式三:Druid监控
# Spring Boot配置 spring: datasource: druid: filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 1000
3.2 分析慢SQL
拿到慢SQL后,第一步不是优化,而是分析。
-- 查看执行计划 EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'PENDING' ORDER BY create_time DESC LIMIT 10;
重点关注:
| 字段 | 含义 | 警告值 |
|---|---|---|
| type | 访问类型 | ALL(全表扫描) |
| rows | 扫描行数 | > 10000 |
| Extra | 额外信息 | Using filesort, Using temporary |
常见问题诊断:
-- 问题1:索引失效(LIKE左模糊) SELECT * FROM user WHERE name LIKE '%张%'; -- ❌ 索引失效 -- 问题2:索引失效(函数操作) SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01'; -- ❌ 索引失效 -- 问题3:索引失效(类型转换) SELECT * FROM user WHERE phone = 13800138000; -- ❌ phone是varchar,传入int
3.3 解决慢SQL
方案一:添加索引
-- 添加复合索引 ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);
方案二:改写SQL
-- 优化前:索引失效 SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01'; -- 优化后:使用范围查询 SELECT * FROM orders WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00';
方案三:分页优化
-- 优化前:深分页(慢) SELECT * FROM orders ORDER BY id LIMIT 1000000, 10; -- 优化后:游标分页(快) SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;
四、OOM排查:从堆dump到定位泄漏
4.1 发现OOM
# 查看JVM日志 grep "OutOfMemoryError" /var/log/app/app.log # 查看GC日志 grep "Full GC" /var/log/app/gc.log
OOM类型判断:
// 堆内存溢出(最常见) java.lang.OutOfMemoryError: Java heap space // 元空间溢出(类加载泄漏) java.lang.OutOfMemoryError: Metaspace // 栈溢出(递归太深) java.lang.StackOverflowError
4.2 获取堆dump
# 方式一:手动dump jmap -dump:format=b,file=dump.hprof <pid> # 方式二:自动dump(推荐) # 启动参数添加: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app/dump.hprof
4.3 分析堆dump
使用MAT(Memory Analyzer Tool):
打开dump文件
查看Leak Suspects Report(自动分析泄漏点)
查看Dominator Tree(找出占用内存最大的对象)
常见泄漏场景:
// 场景1:集合只加不删 private static final List<Object> cache = new ArrayList<>(); public void addToCache(Object obj) { cache.add(obj); // ❌ 永远不清理 } // 场景2:未关闭的资源 public void readFile() { InputStream is = new FileInputStream("file.txt"); // ❌ 没有close,资源泄漏 } // 场景3:ThreadLocal未清理 private static final ThreadLocal<User> userHolder = new ThreadLocal<>(); public void setUser(User user) { userHolder.set(user); // ❌ 请求结束后没有remove }4.4 解决OOM
// 方案1:使用LRU缓存 private static final Cache<String, Object> cache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(); // 方案2:try-with-resources自动关闭 public void readFile() { try (InputStream is = new FileInputStream("file.txt")) { // 自动关闭 } } // 方案3:请求结束清理ThreadLocal @Around("@annotation(...)") public Object around(ProceedingJoinPoint point) throws Throwable { try { return point.proceed(); } finally { userHolder.remove(); // 必须清理 } }五、CPU飙高排查:从top到定位代码
5.1 排查流程
# 1. 找到CPU最高的Java进程 top -c # 2. 找到该进程中CPU最高的线程 top -Hp <pid> # 3. 将线程ID转为16进制 printf "%x\n" <tid> # 4. 导出线程栈,搜索该16进制ID jstack <pid> | grep <tid_hex> -A 30
5.2 常见原因
原因1:死循环
// ❌ 死循环 while (true) { // 没有break或return } // ✅ 正确写法 while (!Thread.currentThread().isInterrupted()) { // 可以被中断 }原因2:频繁GC
# 查看GC情况 jstat -gcutil <pid> 1000 # 如果YGC和FGC都很频繁,说明内存压力大
原因3:正则回溯
// ❌ 灾难性回溯 String regex = "(a+)+b"; input.matches(regex); // 输入"aaaaaaaaaaaaaaaac"会卡死 // ✅ 优化正则 String regex = "a+b";
六、接口超时排查:从链路追踪到定位瓶颈
6.1 分层排查
请求链路: 客户端 → Nginx → Gateway → 服务A → 服务B → 数据库/Redis 排查顺序: 1. Nginx日志:确认请求是否到达 2. Gateway日志:确认是否转发成功 3. 服务A日志:确认是否处理成功 4. 服务B日志:确认是否调用成功 5. 数据库/Redis:确认是否响应正常
6.2 日志分析技巧
# 查看请求耗时分布 awk '{print $NF}' access.log | sort -n | tail -10 # 查看超时请求的共同特征 grep "timeout" app.log | awk '{print $4}' | sort | uniq -c | sort -rn # 按接口统计耗时 grep "api/order" app.log | awk '{sum+=$NF; count++} END {print sum/count}'6.3 常见瓶颈
| 瓶颈 | 特征 | 解决方案 |
|---|---|---|
| 数据库慢 | SQL执行时间长 | 优化SQL、加索引、读写分离 |
| Redis慢 | 命令执行时间长 | 避免大key、使用pipeline |
| HTTP调用慢 | 第三方接口超时 | 设置超时、异步调用、降级 |
| 线程池满 | 日志出现"RejectedExecution" | 扩大线程池、异步化 |
七、日志分析的黄金法则
法则一:先看时间线
# 按时间排序查看日志 sort -k1,2 app.log # 查看某个时间段的日志 awk '/2024-01-01 10:00/,/2024-01-01 10:05/' app.log
为什么要看时间线?
因为很多问题是"并发"导致的。只有把日志按时间排列,才能看到事件之间的关联。
法则二:先看异常,再看正常
# 先看错误日志 grep -E "ERROR|Exception" app.log | tail -20 # 再看正常日志,对比差异 grep "INFO" app.log | tail -20
法则三:先看变化,再看静态
# 实时监控日志 tail -f app.log | grep --line-buffered "ERROR" # 监控GC jstat -gcutil <pid> 1000
法则四:先看全局,再看局部
# 查看错误分布 grep -c "ERROR" app.log # 总数 # 按小时统计 awk '/ERROR/{print $1,$2}' app.log | cut -d: -f1,2 | uniq -c日志分析的本质,是从海量信息中提取"异常模式"。
八、排查工具箱
| 工具 | 用途 | 常用命令 |
|---|---|---|
| tail/grep | 查看日志 | tail -f app.log \| grep ERROR |
| awk/sed | 日志统计 | awk '/ERROR/{count++} END {print count}' app.log |
| jps | 查看Java进程 | jps -lvm |
| jstat | 查看GC情况 | jstat -gcutil <pid> 1000 |
| jmap | 堆dump | jmap -dump:format=b,file=dump.hprof <pid> |
| jstack | 线程dump | jstack <pid> > thread.txt |
| Arthas | 在线诊断 | java -jar arthas-boot.jar |
| MAT | 堆分析 | 打开hprof文件 |
写在最后
线上问题排查,本质上是一场信息战。
你不是在和bug战斗,而是在和"信息不对称"战斗。系统知道问题在哪,它把答案写在了日志里。你的任务,不是"猜"答案,而是"读"答案。
高手和新手的差距,不是谁的工具多,而是谁更会"听"系统说话。
所以下次遇到线上问题,别急着猜,先打开日志,从头到尾读一遍。
你会发现,答案一直在那里。
📌 互动问题:你遇到过最难排查的线上问题是什么?最后是怎么解决的?欢迎在评论区分享你的故事。
如果你觉得这篇文章有价值,欢迎转发给需要的朋友。