1. CPU 占用率高
当系统响应变慢,监控显示 CPU 持续飙升到 90%+ 时,通常遵循以下排查思路:
排查步骤
定位进程:使用
top命令查看哪个进程占用 CPU 最高。定位线程:使用
top -Hp <pid>查看该进程下哪个线程最耗资源。获取线程 ID:将线程 ID 转换为 16 进制(例如
printf "%x\n" <tid>)。查看堆栈:使用
jstack <pid> | grep -A 20 <16进制线程ID>打印堆栈信息,直接定位到出问题的代码行。
常见原因与对策
死循环或逻辑漏洞:代码中出现了没有出口的
while循环。策略:通过
jstack定位代码行,修复循环退出条件。如果是由于多线程并发修改非线程安全集合(如HashMap)导致的死循环,改用ConcurrentHashMap。
频繁 GC(尤其是 Full GC):JVM 频繁回收垃圾会导致 CPU 飙升。此时需要用
jstat -gcutil <pid>查看 GC 情况。策略:
代码层:减少短时间内产生大量临时对象(如在循环内
new对象)。调优层:增大堆内存
-Xmx,或更换更低延迟的垃圾回收器(如从 CMS 换成 G1 或 ZGC)。
计算密集型任务:如大规模加解密、序列化、复杂的正则匹配。
优化:改异步处理、引入缓存或使用更高效的库。
策略:使用更高效的序列化协议(如 Protobuf 代替 JSON/XML);对正则表达式进行预编译(
Pattern.compile)。
锁竞争激烈:大量线程在竞争同一个锁,导致上下文切换频繁。
策略:
线程池调优:不要创建过多线程。
改用无锁编程:使用
CAS(Atomic 类) 代替synchronized或ReentrantLock。
2. 数据库 SQL 查询时间长
分析工具
慢查询日志 (Slow Query Log):首先通过日志找到执行时间超过阈值(如 500ms)的 SQL。
EXPLAIN 执行计划:这是核心工具,重点看
type(访问类型)、key(命中索引)和rows(扫描行数)。
EXPLAIN详细介绍
当拿到一个慢 SQL,第一步永远是查看执行计划。需要重点关注以下几个字段:
type(连接类型):代表查询效率。从好到坏依次为:system>const(主键/唯一索引等值查询) >eq_ref>ref(非唯一索引扫描) >range(范围扫描) >index(全索引扫描) >ALL(全表扫描)。
key:实际使用的索引。如果是NULL,说明没用到索引。rows:预计要扫描的行数。数值越小越好。Extra(额外信息):Using index:覆盖索引(好,不需要回表)。Using filesort:文件排序(坏,需要在内存或磁盘排序,应通过索引优化)。Using temporary:临时表(坏,常见于GROUP BY或DISTINCT)。
常见优化策略
索引优化:
检查是否命中索引:避免全表扫描(
type=ALL)。最左前缀法则:联合索引要遵循字段顺序。
避免索引失效:不要在索引列上做运算、函数操作或使用
!=、LIKE '%xxx'。
查询重写:
减少返回字段:禁止
SELECT *,只取需要的列(覆盖索引,避免了“回表”操作)。深分页优化:
LIMIT 100000, 10会扫描前 10 万行。建议记录上次最大 ID,走WHERE id > last_id LIMIT 10。大表 JOIN 优化:小表驱动大表(
Straight Join),确保被驱动表的关联字段上有索引(触发Index Nested-Loop Join)
架构层面:
读写分离:主库写,从库读。
分库分表:数据量达到千万/亿级时考虑水平拆分。
引入缓存:Redis 挡在数据库前面。
典型问题
全表扫描(ALL)
策略:针对
WHERE和ORDER BY涉及的字段建立索引
回表(Lookup)频繁
策略:建立覆盖索引(Covering Index)。比如查询
SELECT a, b FROM table WHERE a = 1,则建立联合索引(a, b),这样数据直接从索引树获取,无需回表查整行
深分页(Deep Pagination)
策略:使用“标签查询”:
SELECT * FROM t WHERE id > 100000 LIMIT 10。或者先查出 ID 再JOIN。
临时表与排序
策略:
GROUP BY的字段确保有索引。适当增大
sort_buffer_size内存缓冲区。
锁等待(Lock Wait)
策略:
尽量缩短事务长度。
在高并发更新下,将一行数据拆分成多行(如分段汇总)减少行锁竞争。
3. 内存问题
主要表现为 OutOfMemoryError (OOM) 或频繁的内存抖动。
排查工具
jmap:
jmap -dump:format=b,file=heap.hprof <pid>导出堆快照。MAT (Memory Analyzer Tool):分析快照,查看哪个对象占用了绝大部分内存。
典型场景
内存泄漏 (Memory Leak):长生命周期的对象持有短生命周期对象的引用(如静态集合类不断添加元素)。
策略:
静态集合:及时清理不再使用的
staticList/Map。资源关闭:使用
try-with-resources确保数据库连接、I/O 流被关闭。ThreadLocal:手动调用
remove()防止线程复用导致的内存泄漏。
大对象申请:一次性从数据库查出几十万条数据到内存。
策略:
分页获取:严禁
SELECT *且不加限制。流式处理:处理大文件使用
InputStream逐行读取,不要一次性加载到内存。
元空间溢出:动态生成了太多的类(如反射、CGLIB 重复创建)。
策略:减少动态代理类的生成;调大
-XX:MaxMetaspaceSize
4. 磁盘 I/O 与网络 I/O
如果 CPU 并不高,但系统响应极慢,通常卡在 I/O 上。
磁盘 I/O 瓶颈:使用
iostat -x 1查看%util。优化:减少同步写盘、批量写入、使用 SSD。
策略:
批量读写:使用
BufferedOutputStream,或在数据库中使用批量插入batch insert。异步刷盘:将同步写操作改为消息队列(MQ)异步处理。
零拷贝(Zero Copy):在高性能传输场景下利用
FileChannel.transferTo()
网络 I/O 瓶颈:
检查是否存在大量短连接(导致
TIME_WAIT状态过多)。检查内网带宽是否打满。
策略:
连接池:使用数据库连接池(Druid/HikariCP)和 HTTP 长连接,减少 TCP 三次握手。
压缩传输:开启 Gzip 压缩,减少传输字节数。
合并请求:前端或服务间调用时,能批量获取不单点调用。
5. 系统性能
性能测试
性能测试不仅是看系统能扛多少量,更重要的是找到系统的拐点(Bottleneck)。
核心方法
基准测试(Baseline):在单用户或低负载下运行,确定系统运行的最佳状态。
压力测试(Load Testing):逐步增加用户数,直到系统达到预定的性能指标(如 CPU 80% 或 响应时间 500ms)。
极限压测(Stress Testing):继续加压,直到系统报错、崩溃或拒绝服务,观察系统的恢复能力。
稳定性测试(Soak Testing):在固定高负载下运行 24-72 小时,排查内存泄漏和资源溢出。
推荐工具
JMeter:功能最全,支持插件多,后端面试最常问到的工具。
Locust:基于 Python,支持协程,适合模拟超大规模(10万+)并发。
K6:现代化的性能测试工具,使用 JS 编写脚本,对开发者非常友好。
性能监控
监控就像是系统的“仪表盘”,主要分为四个层级:
A. 基础设施层 (Metrics)
指标:CPU 利用率、内存使用、磁盘 I/O、网络流量。
工具:Prometheus + Grafana(黄金组合)。Prometheus 抓取数据,Grafana 负责精美的可视化大屏。
B. 应用性能监控 (APM)
指标:方法调用耗时、JVM 状态、线程池负载、数据库连接池。
工具:
SkyWalking:国产之光,支持 Java 字节码增强,无侵入监控。
Arthas:阿里开源的诊断工具,支持线上直接监控方法耗时(
trace命令)和查看热点代码。
C. 链路追踪 (Tracing)
在高并发微服务架构中,必须知道请求在哪个服务卡住了。
工具:Jaeger 或 Zipkin。通过 TraceID 将整个调用链路串联起来。
D. 日志分析 (Logging)
工具:ELK Stack (Elasticsearch, Logstash, Kibana)。用于分析慢 SQL 日志或异常堆栈。
高并发情况下的关键指标
如果压测时发现 TPS 吞吐量上不去,但 CPU 占用率也很低,从哪些方面排查?
检查线程池/连接池:是否配置太小,导致请求在排队。
检查锁竞争:是否存在严重的
synchronized导致的线程阻塞。检查外部依赖:下游服务或数据库 I/O 是否达到了瓶颈。
网络带宽:内网网卡带宽是否打满。
保护措施
当监控发现系统快撑不住时,需要采取保护措施:
限流 (Rate Limiting):超过 QPS 阈值的请求直接返回失败,保护下游服务(常用 Sentinel 或 Guava RateLimiter)。
熔断 (Circuit Breaker):当某个下游服务慢或报错多时,暂时断开调用,避免雪崩效应。
降级 (Fallback):核心业务保命,非核心业务关停(例如:双 11 为了保证支付,可以暂时关闭评论功能)。