从慢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: 10003.2 分析慢SQL拿到慢SQL后第一步不是优化而是分析。-- 查看执行计划 EXPLAIN SELECT * FROM orders WHERE user_id 123 AND status PENDING ORDER BY create_time DESC LIMIT 10;重点关注字段含义警告值type访问类型ALL全表扫描rows扫描行数 10000Extra额外信息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传入int3.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.logOOM类型判断// 堆内存溢出最常见 java.lang.OutOfMemoryError: Java heap space // 元空间溢出类加载泄漏 java.lang.OutOfMemoryError: Metaspace // 栈溢出递归太深 java.lang.StackOverflowError4.2 获取堆dump# 方式一手动dump jmap -dump:formatb,filedump.hprof pid # 方式二自动dump推荐 # 启动参数添加 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/var/log/app/dump.hprof4.3 分析堆dump使用MATMemory Analyzer Tool打开dump文件查看Leak Suspects Report自动分析泄漏点查看Dominator Tree找出占用内存最大的对象常见泄漏场景// 场景1集合只加不删 private static final ListObject cache new ArrayList(); public void addToCache(Object obj) { cache.add(obj); // ❌ 永远不清理 } // 场景2未关闭的资源 public void readFile() { InputStream is new FileInputStream(file.txt); // ❌ 没有close资源泄漏 } // 场景3ThreadLocal未清理 private static final ThreadLocalUser userHolder new ThreadLocal(); public void setUser(User user) { userHolder.set(user); // ❌ 请求结束后没有remove }4.4 解决OOM// 方案1使用LRU缓存 private static final CacheString, Object cache CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(); // 方案2try-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 305.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 ab;六、接口超时排查从链路追踪到定位瓶颈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、使用pipelineHTTP调用慢第三方接口超时设置超时、异步调用、降级线程池满日志出现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 ERRORawk/sed日志统计awk /ERROR/{count} END {print count} app.logjps查看Java进程jps -lvmjstat查看GC情况jstat -gcutil pid 1000jmap堆dumpjmap -dump:formatb,filedump.hprof pidjstack线程dumpjstack pid thread.txtArthas在线诊断java -jar arthas-boot.jarMAT堆分析打开hprof文件写在最后线上问题排查本质上是一场信息战。你不是在和bug战斗而是在和信息不对称战斗。系统知道问题在哪它把答案写在了日志里。你的任务不是猜答案而是读答案。高手和新手的差距不是谁的工具多而是谁更会听系统说话。所以下次遇到线上问题别急着猜先打开日志从头到尾读一遍。你会发现答案一直在那里。 互动问题你遇到过最难排查的线上问题是什么最后是怎么解决的欢迎在评论区分享你的故事。如果你觉得这篇文章有价值欢迎转发给需要的朋友。
线上问题排查:为什么你查了3小时,别人只用了10分钟?
发布时间:2026/7/5 14:45:20
从慢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: 10003.2 分析慢SQL拿到慢SQL后第一步不是优化而是分析。-- 查看执行计划 EXPLAIN SELECT * FROM orders WHERE user_id 123 AND status PENDING ORDER BY create_time DESC LIMIT 10;重点关注字段含义警告值type访问类型ALL全表扫描rows扫描行数 10000Extra额外信息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传入int3.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.logOOM类型判断// 堆内存溢出最常见 java.lang.OutOfMemoryError: Java heap space // 元空间溢出类加载泄漏 java.lang.OutOfMemoryError: Metaspace // 栈溢出递归太深 java.lang.StackOverflowError4.2 获取堆dump# 方式一手动dump jmap -dump:formatb,filedump.hprof pid # 方式二自动dump推荐 # 启动参数添加 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/var/log/app/dump.hprof4.3 分析堆dump使用MATMemory Analyzer Tool打开dump文件查看Leak Suspects Report自动分析泄漏点查看Dominator Tree找出占用内存最大的对象常见泄漏场景// 场景1集合只加不删 private static final ListObject cache new ArrayList(); public void addToCache(Object obj) { cache.add(obj); // ❌ 永远不清理 } // 场景2未关闭的资源 public void readFile() { InputStream is new FileInputStream(file.txt); // ❌ 没有close资源泄漏 } // 场景3ThreadLocal未清理 private static final ThreadLocalUser userHolder new ThreadLocal(); public void setUser(User user) { userHolder.set(user); // ❌ 请求结束后没有remove }4.4 解决OOM// 方案1使用LRU缓存 private static final CacheString, Object cache CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(); // 方案2try-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 305.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 ab;六、接口超时排查从链路追踪到定位瓶颈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、使用pipelineHTTP调用慢第三方接口超时设置超时、异步调用、降级线程池满日志出现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 ERRORawk/sed日志统计awk /ERROR/{count} END {print count} app.logjps查看Java进程jps -lvmjstat查看GC情况jstat -gcutil pid 1000jmap堆dumpjmap -dump:formatb,filedump.hprof pidjstack线程dumpjstack pid thread.txtArthas在线诊断java -jar arthas-boot.jarMAT堆分析打开hprof文件写在最后线上问题排查本质上是一场信息战。你不是在和bug战斗而是在和信息不对称战斗。系统知道问题在哪它把答案写在了日志里。你的任务不是猜答案而是读答案。高手和新手的差距不是谁的工具多而是谁更会听系统说话。所以下次遇到线上问题别急着猜先打开日志从头到尾读一遍。你会发现答案一直在那里。 互动问题你遇到过最难排查的线上问题是什么最后是怎么解决的欢迎在评论区分享你的故事。如果你觉得这篇文章有价值欢迎转发给需要的朋友。