ARTICLE DETAIL

资讯详情

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

Java线上故障排查实战:CPU飙高、慢SQL与日志分析技巧

Java线上故障排查实战:CPU飙高、慢SQL与日志分析技巧 “告诉你个秘密千万别让同行知道”这类标题多少有点标题党但它背后确实对应着一类值得沉淀的内容不是官方文档里的 API 说明而是线上故障发生时靠时间换回来的排查经验。很多技巧并不复杂组合起来却能在 CPU 飙高、接口超时、慢 SQL 成堆、日志混乱的现场里帮你快速把“现象”翻译成“线索”再定位到具体代码或配置。本文就以 Linux 环境下的 Java 后端服务为例整理一组实用且可复现的排查技巧。目标读者是已经写过接口、部署过服务但在线上问题面前还缺少完整排查思路的开发者。读完并跟着操作一遍后你会知道遇到资源占用异常、网络连接异常、数据库慢查询和日志分析问题时第一步做什么、第二步看什么以及哪些命令在生产环境不能随便执行。1. 为什么很多实用技巧不会写进官方文档1.1 文档回答“标准用法”经验回答“异常现场”官方文档的核心任务是讲清楚一个工具或框架“应该怎么用”。比如jstack的文档会告诉你这个命令用于打印 Java 进程的线程栈但文档不会告诉你线上 CPU 飙升时应该先用top定位进程再用top -Hp定位线程把线程 ID 转成十六进制才能在一堆nid里找到真正忙的线程。这个转换关系是文档和命令之间那道“最后一公里”的桥。同样MySQL 文档会说明慢查询日志有long_query_time参数但不会告诉你生产环境开启后磁盘可能被日志打满需要在测试环境验证好阈值再低峰期上线。这类“从实际报错和故障现场倒推出来的用法”很难完整出现在官方文档里只能靠项目积累。1.2 排查的本质是“现象到证据”的转换线上问题排查不是一个线性过程更像是一个不断提出假设、寻找证据、推翻或验证假设的循环。前面提到的技巧核心价值在于提供“证据采集路径”。举个例子现象接口响应变慢。假设一数据库慢查询。证据慢查询日志里出现目标 SQL。假设二请求线程被阻塞。证据jstack线程栈显示大量线程处于BLOCKED。没有这些工具组合你只能靠猜。猜得多了排查时间就会被拉长恢复时间自然也会变长。这也是为什么很多团队会把常见的排查命令写进内部手册它们不是为了炫技而是为了在紧张时不让大脑一片空白。1.3 本文适用的技术栈和边界本文命令主要面向 Linux 系统、Java 后端服务、Spring Boot 应用和 MySQL 数据库。如果你使用 Windows Server或者服务已经全部容器化部分命令需要调整但排查思路是通用的先确认影响范围再采集现场证据然后从资源层、网络层、应用层、数据层逐步缩小范围最后修复并补充监控。下面每个章节都会先给出适用场景再给命令再解释原因。2. 先定位进程和线程CPU 飙高和线程卡住的基本功2.1 第一步确认到底是哪个进程的问题当告警提示某台机器负载升高先不要直接抓日志应该先确认是哪个进程在消耗资源。登录服务器后第一件事是运行top进入top交互界面后按P键按 CPU 使用率排序找到占用率最高的进程 PID。这一步解决的是“是不是 Java 进程”的问题。如果最高的是数据库、Nginx 或某个脚本后续排查方向完全不同。也可以使用更精确的ps命令确认进程的启动信息和归属ps -p PID -o pid,ppid,user,etime,%cpu,%mem,cmdetime表示进程已经运行了多久用来判断是否发生过重启。如果进程运行时间很短说明服务被反复拉起问题可能出在健康检查、启动失败或调度策略上而不是当前时刻的代码逻辑。这里要注意一个常见误区top看到的 CPU 使用率是瞬时值持续观察几秒后再下结论。只抓一次不代表稳定状态。2.2 线程级定位用 top -Hp 找到线程 ID确认是 Java 进程后需要进一步定位到具体线程。使用top -Hp PID然后按P排序找到 CPU 使用率最高的线程的 PID记为 TID。假设 TID 是28858。为什么还要做一步转换因为jstack输出里的线程编号nid是十六进制而top -Hp给出的是十进制线程 ID。不转换你会在线程栈文件里找不到对应线程。转换命令printf %x\n 28858输出70ba得到的70ba就是稍后在jstack输出中要搜索的十六进制线程号。这一步虽然简单却是很多人第一次排查时卡住的地方。2.3 用 jstack 对上线程栈找出代码位置抓取线程栈jstack PID /tmp/jstack_$(date %Y%m%d_%H%M%S).log然后搜索对应线程grep -n nid0x70ba /tmp/jstack_*.log附近会看到类似这样的内容http-nio-8080-exec-11 #27 daemon prio5 os_prio0 ... java.lang.Thread.State: RUNNABLE at com.example.service.OrderService.queryOrder(OrderService.java:88) at com.example.controller.OrderController.detail(OrderController.java:24) ...到这里CPU 飙高的代码路径基本就能定位了。如果是TIMED_WAITING或WAITING状态且大量线程堆在同一点往往说明线程被锁或连接池阻塞。还需要关注线程栈里的“整体分布”。建议用下面的命令统计线程状态grep java.lang.Thread.State /tmp/jstack_*.log | sort | uniq -c输出示例18 java.lang.Thread.State: RUNNABLE 12 java.lang.Thread.State: TIMED_WAITING (parking) 5 java.lang.Thread.State: WAITING (on object monitor) 2 java.lang.Thread.State: BLOCKED (on object monitor)如果BLOCKED数量持续大于 0优先查看阻塞在哪个 monitor 上然后顺着引用找到持锁线程。2.4 线程排查的常见坑问题现象常见原因检查方式处理建议top -Hp找到线程 ID但在 jstack 中搜不到线程刚好结束或抓取的是另一个进程重新抓取观察线程是否稳定使用top -Hp多次确认再抓jstackJava 进程 CPU 高但线程栈全在RUNNABLE可能是 GC 线程或 JIT 编译线程用jstat -gc PID 1000观察 GC 频率若 GC 频繁抓jmap -histo分析对象分布容器内执行jstack报错容器里没有 JDK 工具或 JVM 版本不匹配检查 JDK 和运行环境使用与镜像同版本的 JDK或用jhsdb等替代工具实际生产环境中jstack在极少数情况下会对进程造成短暂停顿。抓取前要确认服务是否有多个副本优先在低峰期或流量较小实例上操作。3. 再检查网络和端口连接超时、端口占用、大量 TIME_WAIT3.1 先看监听端口和进程关系服务启动失败时最常见的报错是“端口被占用”。先确认端口监听情况ss -lntp输出示例State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:((java,pid12345,fd72))-l只显示监听中的连接-n不做域名解析-t表示 TCP-p显示进程信息。如果系统没有ss可以使用netstat -ltnp。如果只想确认某个端口lsof -i :8080没有lsof时可以先安装也可以在只读环境中用ss代替。ss是内核提供的接口通常比netstat更轻量。3.2 连接超时怎么分阶段排查接口连接超时不能只盯着应用层。先手动请求一次观察卡顿发生在哪个阶段curl -v -o /dev/null -w connect:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n https://example.com/api/health关键输出time_connectTCP 建连耗时time_starttransfer从开始到收到响应头的时间包括 TLS 握手和服务端处理time_total总耗时。如果time_connect就很高问题大概率在网络上。先确认域名解析getent hosts your-service.example.com再确认目标端口能否连通nc -vz -w 3 目标IP 目标端口-w 3表示最多等待 3 秒。如果 TCP 能连通但 HTTPS 请求慢下一步观察 TLS 握手阶段服务端证书链长度、SSL 会话复用配置、网关层的连接复用是否开启都会影响握手耗时。如果连接本地服务都很慢则要检查服务线程池是否被打满、请求队列是否堆积。这里经常出现一个误区把“网络慢”和“服务端处理慢”混在一起。用curl -w把阶段拆开能快速区分责任方。3.3 大量 TIME_WAIT 和 CLOSE_WAIT 怎么看查看系统总体连接状态ss -s看处于TIME_WAIT的连接ss -tan state time-wait | head -n 20看处于CLOSE_WAIT的连接ss -tan state close-wait | head -n 20状态含义常见原因处理方向TIME_WAIT主动关闭连接的一方等待 2MSL 后释放客户端或网关高频创建短连接开启连接复用或调整内核参数但优先从代码侧解决CLOSE_WAIT对端已关闭本端还没有关闭 socket应用没有读取完数据或没有调用 close检查代码中连接释放逻辑特别是异常分支ESTABLISHED已建立连接正常关注数量是否超过文件句柄限制大量CLOSE_WAIT通常是应用代码问题不能在网络层忽略处理。比如使用 HTTP 客户端时响应流未关闭、数据库连接池泄漏、Socket 异常时没有执行finally关闭逻辑。此时可以抓线程栈确认连接持有位置。3.4 网络排查清单排查网络类问题建议按这个顺序推进确认告警对象是进程、容器、Pod 还是物理机确认整个链路的域名解析是否一致确认目标端口是否有监听确认 TCP 三次握手是否成功确认 TLS 握手是否正常确认应用层超时配置和线程池水位确认连接关闭时是否走异常分支。这个清单的价值在于避免跳跃性排查。很多人一遇到超时就先调超时时间如果问题出在 DNS 或端口不可达调应用超时只会掩盖症状。4. 数据库慢查询从日志到执行计划再到索引建议4.1 打开慢查询日志并观察数据库慢查询是最容易被日志发现的性能问题。MySQL 中可以通过参数动态开启测试环境可以这样验证SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 2; SET GLOBAL slow_query_log_file /var/log/mysql/mysql-slow.log;需要持久化时在my.cnf中配置[mysqld] slow_query_log ON slow_query_log_file /var/log/mysql/mysql-slow.log long_query_time 2 log_queries_not_using_indexes ONlong_query_time 2表示执行超过 2 秒的 SQL 会被记录。生产环境不建议一开始就设置成 0那会让日志增长速度非常快增加磁盘 IO 压力。可以先设置 2 秒观察一天再逐步调低。分析慢日志时使用工具mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log-s t表示按执行时间排序-t 10表示取前 10 条。如果慢日志量巨大先看摘要不要直接打开全文。4.2 用 EXPLAIN 读执行计划拿到慢 SQL 后加上EXPLAIN再执行一遍EXPLAIN SELECT order_id, amount, status FROM t_order WHERE user_id 10086 AND status PAID ORDER BY create_time DESC LIMIT 20;重点看这几列列名关键值含义与风险typesystem, const, eq_ref, ref, range, index, ALL从优到差ALL 是全表扫描key实际使用的索引名NULL表示没有使用索引rows预估扫描行数值越大成本越高ExtraUsing index, Using filesort, Using temporaryUsing filesort说明排序没有走索引Using temporary说明可能产生临时表typeALL且rows很大时优化方向通常是加索引。但加索引之前要确认 SQL 的等值条件、排序字段和回表数量。例如user_id是高频查询条件就非常适合作为联合索引的前导列。4.3 常见索引失效场景错误写法原因推荐写法WHERE DATE(create_time) 2025-01-01对索引列使用函数破坏索引结构改成范围条件create_time 2025-01-01 AND create_time 2025-01-02WHERE user_id 10086但user_id是 int隐式类型转换导致索引失效参数类型与字段类型保持一致WHERE name LIKE %张%通配符在前无法匹配 B 树前缀确认业务是否允许前缀匹配或使用全文本检索方案WHERE a 1 OR b 2只给 a 建了索引OR 任一条件无索引可能全表扫描拆成两条 SQL 用 UNION 合并或给 b 也建索引一个容易被忽略的问题是联合索引的字段顺序。(user_id, status)索引可以覆盖WHERE user_id ? AND status ?但无法高效支持只按status查询。设计索引前先列出业务中最常见的查询组合。4.4 优化 SQL 时不能只看索引索引不是银弹。即使走了索引分页深翻页仍然会慢SELECT * FROM t_order ORDER BY id LIMIT 100000, 20;这段 SQL 会扫描 100020 行再丢弃前 100000 行。推荐使用游标分页或延迟关联SELECT t.* FROM t_order t JOIN ( SELECT id FROM t_order WHERE create_time 2025-01-01 ORDER BY id LIMIT 100000, 20 ) tmp ON t.id tmp.id;把主查询的扫描范围压缩到索引页再回表拿完整数据扫描和回表量都会下降。生产环境执行 DDL 加索引要控制节奏。早期的 MySQL 版本中ALTER TABLE ADD INDEX可能锁表即使使用ALGORITHMINPLACE也可能占用额外空间和 IO。建议低峰期执行或在有pt-online-schema-change等工具的团队中走线上变更流程。5. 日志和调用链从关键词到时间线的整理方法5.1 用 grep 和 awk 快速抽取异常信息应用日志是排查问题的第一现场但也是噪音最多的地方。不要打开整个日志文件先按关键字过滤grep -E ERROR|Exception /var/log/app/app.log | tail -n 200如果日志量很大直接grep全量文件可能很慢。推荐先用ls -lh看文件大小确定是否需要分段处理。按时间窗口提取日志awk $2 10:30:00 $2 10:30:59 /var/log/app/app.log | head -n 100这里的$2是日志格式中时间字段的列。如果日志格式不同需要先head -n 3确认字段位置。查看历史日志时如果文件按天轮转并压缩zcat /var/log/app/app.log.1.gz | grep NullPointerException | head -n 50zcat会把压缩内容解压到标准输出适合临时查询不占用额外磁盘空间。5.2 把日志串成时间线没有 traceId 就补一条单条异常只是片段排查真实请求链路时必须能把同一次请求的日志串起来。最有效的方式是使用traceId。注意不要等线上出问题再临时加应在项目初始阶段就做。Java 后端常用MDC实现import org.slf4j.MDC; String traceId generateTraceId(); MDC.put(traceId, traceId); try { // 业务逻辑 } finally { MDC.remove(traceId); }logback 配置中增加输出占位符pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} traceId%X{traceId:-} - %msg%n/pattern日志输出示例2025-01-01 10:30:01.123 [http-nio-8080-exec-1] INFO com.example.controller.OrderController traceId6f3a1c9d - query order start有了traceId后排查一个请求就变成一条命令grep traceId6f3a1c9d /var/log/app/app.log如果服务之间调用则需要把traceId透传到下一个服务通过 HTTP Header 传递并在入口统一解析。5.3 日志量大的时候怎么过滤如果日志里混着健康检查、批量任务、定时器输出可以先排除grep -E ERROR|Exception /var/log/app/app.log | grep -v health-check | tail -n 200实时观察新日志tail -F /var/log/app/app.log按行范围提取指定区间sed -n 1000,1200p /var/log/app/app.log真实场景中经常要结合时间窗口、线程名、traceId 三个条件组合。不要一次性追求精确结果先用宽条件拿到样本再逐步收紧。5.4 日志排查的三个常见坑问题现象常见原因检查方式处理建议两个服务的日志对不上时间服务器时区或日志时间格式不统一对比/etc/localtime和日志时间戳统一输出 UTC 或统一使用Asia/Shanghai搜索到的异常时间不准确日志没有经过 buffer打印顺序乱看日志是否有线程并发打印使用异步 Appender 时保证位置信息完整明明有异常但日志文件没有日志级别设置过高或异常被吞掉检查配置中的root level和 catch 块全链路日志级别外置化避免上线改配置一个很重要的排查原则不要在没有 traceId 的情况下凭日志猜请求顺序那样效率极低。如果系统还没有 traceId先补上再谈后续优化。6. 线上环境使用排查工具的安全边界和最佳实践6.1 哪些命令在什么环境才能用开发环境随便执行命令生产环境要评估风险。下面这份清单可以作为团队内部约定的参考工具/命令开发环境生产环境建议风险说明top/ps安全安全只读无副作用ss/lsof/nc安全安全但注意权限只读确认端口范围再执行jstack pid安全低峰期或单副本时谨慎进程卡死时可能短暂停顿jmap -histo pid安全谨慎可能触发 Full GC影响性能kill -9 pid避免使用禁止先使用进程不会清理资源可能丢失数据strace -p pid可用来学习高风险需评估可能拖慢进程执行时限制时长和跟踪范围sysctl修改内核参数可尝试必须走变更流程参数影响全机器不一定能平滑回滚这里要特别说明很多命令不是不能用而是要在“采集证据”和“影响业务”之间做取舍。比如容器环境里宿主机上的top看到的 CPU 指标和容器内不完全一致需要借助监控系统或docker stats确认。盲目在宿主机上对容器内进程执行jcmd可能拿到错误视角。6.2 排查前先备份现场证据发现问题后不要急着重启服务。重启意味着现场的线程栈、堆内存、日志、网络连接都可能会丢失。推荐按下面顺序保存证据保留故障时间窗口记录开始时间、恢复时间、影响范围保存线程栈jstack抓 2 到 3 份间隔 5 秒保存进程快照top -H -b -n 1 -p PID保存网络状态ss -tan | head -n 200保存关联日志慢 SQL、错误日志、接入层访问日志保存配置版本应用配置、数据库参数、最近发布记录。“先保留现场再开始排查”是线上故障处理中最容易被忽略的一步。很多团队在慌乱中直接重启服务后面连根因都找不到。6.3 可复用清单线上故障排查准备表可以把以下清单打印为团队内部文档每次出问题时对照执行阶段动作是否完成确认影响明确故障服务、影响范围、用户群体通知协作通知研发、运维、测试负责人保留现场采集线程栈、堆信息、网络快照、日志建立时间线从日志中整理问题开始时间和关键时间点定位根因按资源、网络、应用、数据层逐层排查修复验证最小变更生效验证监控指标恢复复盘归档输出根因报告、补充监控、更新手册这份清单的价值在于故障发生时情绪紧张很容易跳过某个环节。按表格逐项打勾能保证关键证据不遗漏。6.4 从“会排查”到“不用排查”排查技巧再多也只是补救手段。一个健康的系统应该用监控和演练把常见故障消灭在发生之前。建议按以下方向持续投入基础监控CPU、内存、磁盘、网络、JVM GC 指标链路追踪接入 traceId打通入口、应用、数据库和外部服务慢 SQL 监控自动采集慢日志并统计高频 SQL日志治理统一日志格式、级别外置化、日志轮转策略容量压测定期压测核心链路提前发现线程池、连接池、索引瓶颈故障演练模拟进程宕机、数据库主从切换、磁盘写满验证应急预案。真正值得收藏的“秘密”不是哪条命令本身而是把命令、工具、流程组织成一套可复用的排查方法。建议在测试环境把本文的命令全部跑一遍熟悉输出格式后再整理一份适合自己团队的排查清单。下次线上出问题时就不会从零开始摸索了。
返回列表