慢sql排查
怎么排查
通过如下命令,开启慢 SQL 监控,执行成功之后,客户端需要重新连接才能生效。
-- 开启慢 SQL 监控
set global slow_query_log = 1;
默认的慢 SQL 阀值是10
秒,可以通过如下语句查询慢 SQL 的阀值。
-- 查询慢 SQL 的阀值
show variables like "long_query_time";
我们可以通过如下方式,将慢 SQL 阀值配置成0.2
秒。
-- 修改慢 SQL 的阀值
set global long_query_time = 0.2;
在 select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,从而在执行查询时,会返回执行计划的信息,而不是执行这条 SQL 。
explain 命令可以获取 MySQL 如何执行 SELECT 语句的信息,来查看一个这些 SQL 语句的执行计划,如该 SQL 语句有没有使用上了索引、有没有做全表扫描等。这是查询性能优化不可缺少的一部分,因此平时在进行 SQL 开发时,都要养成用 explain 分析的习惯。
expain 出来的信息有 10 列,分别是 id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra 。下面对这些字段出现的可能进行解释:
列名 | 说明 |
id | 执行编号,有几个 select 就有几个 id |
select_type | 表示本行是简单的还是复杂的 select |
table | 正在访问哪一个表(表名或别名) |
type | 表示关联类型或访问类型,即 MySQL 决定如何查找表中的行,all表示没有用索引 |
possible_keys | 哪些索引可以优化查询 |
key | 实际采用哪个索引来优化查询 |
key_len | 索引字段的长度 |
ref | 显示了之前的表在 key 列记录的索引中查找值所用的列或常量 |
rows | 为了找到所需的行而需要读取的行数(估算值,并不精确) |
Extra | 执行情况的额外描述和说明 |
partitions (MySQL 8 新增) |
如果查询是基于分区表的话,会显示查询将访问的分区 |
filtered (MySQL 8 新增) |
按表条件过滤的行百分比。 rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的 id 值比当前表 id 值小的表) |
原因及优化
1. 全表扫描(Full Table Scan)
原因:未使用索引,导致数据库逐行扫描全表。
示例:
SELECT * FROM orders WHERE user_id = 123; -- 假设 user_id 无索引
解决:
使用索引
为高频查询字段加索引:
避免索引失效
联合索引需遵循 最左前缀原则
2. 缺少合适的索引
原因:查询条件、排序或连接字段未建立索引。
示例:
SELECT name FROM users WHERE age > 30 ORDER BY create_time; -- age 和 create_time 无索引
使用索引
为高频查询字段加索引:
避免索引失效
联合索引需遵循 最左前缀原则
3. 索引失效
原因:索引使用不当(如隐式类型转换、函数操作、联合索引未遵循最左前缀)。
示例:
SELECT * FROM products WHERE YEAR(created_at) = 2023; -- 函数操作导致索引失效
使用索引
为高频查询字段加索引:
避免索引失效
联合索引需遵循 最左前缀原则
4. 复杂查询(多表连接、子查询)
原因:多表 JOIN 未优化、子查询嵌套过深或关联条件不合理。
示例:
SELECT a.* FROM orders a JOIN users b ON a.user_id = b.id WHERE b.country = 'China' AND a.amount > 1000; -- 若关联字段无索引或数据量大,性能极差
5. 排序和分组操作
原因:
ORDER BY
或GROUP BY
字段无索引,导致临时表和文件排序。示例:
SELECT department, AVG(salary) FROM employees GROUP BY department; -- 若 department 无索引
6. 分页查询效率低
原因:使用
OFFSET
分页时,偏移量过大导致扫描行数过多。示例:
SELECT * FROM logs ORDER BY id LIMIT 1000000 OFFSET 1000000; -- 扫描 200 万行
避免大偏移量 OFFSET:
-- 低效:OFFSET 1000000 SELECT * FROM logs ORDER BY id LIMIT 1000000 OFFSET 1000000; -- 高效:基于游标分页(记录上次查询的 ID) SELECT * FROM logs WHERE id > 1000000 ORDER BY id LIMIT 100;
7. 锁竞争与事务问题
原因:长事务、锁等待或未提交的事务阻塞查询。
示例:
BEGIN TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; -- 长时间未提交
避免使用长事务
8. 数据量过大
原因:单表数据量过大(如亿级),未分库分表。
示例:
SELECT * FROM big_table WHERE status = 'active'; -- 表中有 1 亿条数据且无索引
这里在提供一个思路
https://zhuanlan.zhihu.com/p/628368104从磁盘与内存的点上去考虑,详见这篇文章
当某一个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染。
把脏数据刷回磁盘的技术又称 checkpoint 技术。
MySQL 的脏页落盘是由后台线程定期异步执行的。
当 redo log 满了的情况下,会主动触发脏页刷新到磁盘;
Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
MySQL 认为空闲时,后台线程回定期将适量的脏页刷入到磁盘;即使非空闲时,也会见缝插针地刷盘;
MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;
在 MySQL 的使用过程中,可能会出现抖动(突然变得很慢,且 CPU 资源被大量占用),很大可能就是在刷盘,即情况 12。
maven依赖冲突
一、识别依赖冲突
使用
mvn dependency:tree
命令 通过命令生成依赖树,定位冲突的依赖项及其版本。例如:
mvn dependency:tree -Dverbose
输出示例:
[INFO] +- org.springframework:spring-context:jar:5.2.7.RELEASE:compile | \- org.springframework:spring-aop:jar:5.2.0.RELEASE:compile (conflict with 5.2.7.RELEASE)
此时
spring-aop
存在版本冲突分析依赖报告 使用
mvn dependency:analyze
生成HTML报告,明确未使用和冲突的依赖
二、解决依赖冲突的方法
1. 手动排除冲突依赖
在pom.xml
中通过<exclusions>
标签排除传递依赖:
<dependency> <groupId>com.example</groupId> <artifactId>example-library</artifactId> <version>1.0.0</version> <exclusions> <exclusion> <groupId>com.conflict.group</groupId> <artifactId>conflicting-artifact</artifactId> </exclusion> </exclusions> </dependency>
适用于明确知道冲突来源的场景
2. 统一版本管理
dependencyManagement
标签 在父POM中定义依赖版本,强制子模块使用统一版本:
<dependencyManagement> <dependencies> <dependency> <groupId>com.example</groupId> <artifactId>common-library</artifactId> <version>2.0.0</version> </dependency> </dependencies> </dependencyManagement>
适用于多模块项目
properties
管理版本 通过属性简化版本声明
3. 使用Maven插件自动化处理
Maven Enforcer Plugin 强制执行依赖版本一致性规则:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <version>3.0.0-M3</version> <executions> <execution> <id>enforce</id> <goals> <goal>enforce</goal> </goals> <configuration> <rules> <DependencyConvergence/> </rules> </configuration> </execution> </executions> </plugin>
构建阶段自动检测冲突
Full GC次数过多
什么时候发生
Full GC(Garbage Collection)是Java虚拟机中进行垃圾回收的一种类型,它会清理整个堆内存,包括新生代和老年代。Full GC通常发生在以下情况下:
老年代空间不足: 当老年代无法容纳新生代晋升过来的对象时,可能触发Major GC。这通常发生在年轻代的Minor GC后,存活的对象被移动到老年代,导致老年代的空间不足。
永久代空间不足: 在Java 7及之前的版本中,常量池等信息存放在永久代中。如果常量池或类的元数据占用的空间过大,可能导致永久代空间不足,触发Full GC。在Java 8及之后的版本中,永久代被元空间(Metaspace)取代。
使用CMS(Concurrent Mark-Sweep)垃圾回收器时的并发失败: CMS是一种以减少应用程序停顿时间为目标的垃圾回收器,但它可能会因为一些原因(比如老年代空间不足)而导致并发失败,从而触发Full GC。
System.gc()的显式调用: 调用System.gc()或Runtime.getRuntime().gc()并不能确保会立即进行垃圾回收,但它可能会触发Full GC。
永久代/Metaspace溢出: 如果Metaspace(Java 8及以后的版本)或永久代(Java 7及之前的版本)中的元数据信息溢出,可能触发Full GC。
分配担保失败: 在进行Minor GC时,虚拟机会检查老年代的剩余空间是否大于新生代的对象总大小。如果不大于,会尝试进行一次Full GC。这是为了确保在新生代GC后,存活的对象能够顺利晋升到老年代。
G1垃圾回收器的一些特殊情况: G1垃圾回收器在一些特殊情况下可能触发Full GC,例如在进行Mixed GC(混合收集)时,或者由于空间不足而放弃Mixed GC,转而执行Full GC。
Full GC是一种比较重量级的垃圾回收操作,会导致较长的停顿时间,因此在实际应用中,需要谨慎处理Full GC的情况,尽量避免频繁发生。
减少Full GC 频率的建议:
调整堆内存大小: 如果堆内存设置得太小,容易导致频繁的垃圾回收,特别是Full GC。增大堆内存可以减少垃圾回收的频率。可以通过 -Xms 和 -Xmx 参数分别设置初始堆大小和最大堆大小。
java -Xms512m -Xmx1024m -jar YourApplication.jar
合理设置新生代和老年代的比例: 年轻代存活对象晋升到老年代时会触发Full GC,合理设置新生代和老年代的比例可以影响对象晋升的速度。可以通过 -XX:NewRatio 参数来调整新生代和老年代的比例。
java -XX:NewRatio=2 -jar YourApplication.jar
选择合适的垃圾回收器: 根据应用程序的特性选择合适的垃圾回收器。不同的垃圾回收器有不同的特点,比如CMS(Concurrent Mark-Sweep)和G1(Garbage-First)是以减小停顿时间为目标的回收器,适用于对响应时间敏感的应用。
java -XX:+UseConcMarkSweepGC -jar YourApplication.jar
调整新生代的大小: 通过调整新生代的大小,可以影响对象在年轻代的存活时间,从而影响晋升到老年代的速度。可以使用参数 -Xmn 来设置新生代的大小。
java -Xmn256m -jar YourApplication.jar
避免过度使用Finalizer: 使用 finalize 方法可能导致对象在垃圾回收时的额外开销。尽量避免过度依赖 finalize 方法。
检查内存泄漏: 内存泄漏可能导致堆内存的不断增加,最终导致Full GC。使用内存分析工具(如VisualVM、MAT等)来检查和解决潜在的内存泄漏问题。
监控和调优: 定期监控应用程序的垃圾回收情况,通过日志或监控工具(如VisualVM、JConsole等)来分析GC日志,找到GC发生的原因,并根据实际情况进行调优。
线上 CPU 飙高如何排查?
1)首先确认哪个进程占用 CPU 过高,登录服务器利用 top 命令查看。
top 命令是 Linux 下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于 Windows 的任务管理器
2)确认 CPU 利用率很高的进程的 PID,假设为 1234 确实是 Java 进程,则通过 top -Hp 1234
查看具体的线程。
3)假设得到的线程 ID 是 5678,再将线程转为十六进制。
printf "%x\n" 5678
4)得到十六进制的 tid 为 162e,此时在利用 jstack 1234 | grep 162e -A 100
查看具体的栈信息。
jstack 命令用于生成虚拟机当前时刻的线程快照。 线程快照是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因, 如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。
grep (global regular expression) 命令用于查找文件里符合条件的字符串或正则表达式, -A 100
表示查找到匹配行后,额外再输出 100 行,便于我们查看堆栈信息。
5)根据堆栈信息就可以定位到具体是哪行代码导致了 CPU 飙高,对应分析修复即可!
接口变慢了应该如何排查?
接口变慢的排查思路
大部分情况下,接口变慢的排查思路按照以下的几点来回答面试官(注意,不要过度发散)
利用服务的监控(目前大部分公司项目都部署在云服务平台上,都会提供基础的监控大屏),或使用监控工具(如 Prometheus + Grafana、Zabbix等)查看系统的 CPU、内存、磁盘、网络等硬件资源的使用情况,看看是否有资源瓶颈。
检查网络是否存在延迟或带宽瓶颈(这个很常见,因为流量大了带宽打满了),特别是接口请求和响应涉及到跨服务、跨地区的调用。
查看接口的日志,确认接口是否一直慢,或者是否只在某些时间段或特定请求下变慢。
观察数据库情况,查询数据库的资源水平与是否存在慢查询、索引缺失或数据库锁等问题。
经过上面几个部分大致已经能确定方向,如果怀疑可能是某个新上的功能导致的,可以定位查看代码是否有性能瓶颈,例如是否有不必要的同步、循环、递归等,或者某些操作存在高时间复杂度。
大部分情况下可能导致接口变慢的原因如下:
资源瓶颈
1)CPU 使用过高:高 CPU 使用可能导致应用的计算能力受到限制,特别是当有计算密集型任务时(如加密、解密、大规模数据处理等)。可以使用 top、htop(Linux)查看系统的 CPU 占用率,查找消耗 CPU 的进程。
2)内存泄漏:内存泄漏导致系统内存逐渐被消耗完,最终触发 GC(垃圾回收) 频繁发生,导致接口响应慢。可以使用 JVM 内存分析工具(如 jvisualvm、JProfiler)来检测内存占用,查看是否存在内存泄漏或频繁的垃圾回收。
3)磁盘 I/O 负载过高:高磁盘 I/O 可能导致系统响应变慢,尤其是在需要频繁读写磁盘(如数据库操作、日志写入等)的场景中。需要查看磁盘的使用情况,使用 iostat、sar 等工具查看磁盘读写性能。
4)网络带宽不足或高延迟可能导致接口响应慢:尤其是跨服务、跨区域调用时。可以使用工具如 ping、traceroute、netstat 等检查网络状况(大部分情况下看云服务监控即可)。
数据库问题(业务上很多时候都是数据库问题导致的接口慢)
1)数据库查询慢:数据库查询过慢是接口变慢的常见原因!可能是由于 复杂的 SQL 查询、缺乏合适的索引 或 表锁 导致的。
可以检查数据库慢查询日志,查看是否有长时间执行的 SQL 查询;使用 EXPLAIN 查看 SQL 执行计划,分析是否存在全表扫描或缺少索引。
2)数据库连接池配置问题:数据库连接池的配置不合理,导致连接数不足或连接池过多,进而影响数据库性能。
需要检查数据库连接池的配置,查看是否存在连接池耗尽或连接过多的情况。使用 HikariCP、Druid 等数据库连接池的监控功能查看连接池的状态。
3)数据库锁竞争:如果多个请求争夺数据库中的同一行或表,可能会导致锁竞争,从而使接口响应时间变慢。
使用 SHOW ENGINE INNODB STATUS(MySQL)查看当前数据库的锁信息,分析是否有锁竞争的情况。
代码性能问题
1)高时间复杂度的算法:某些算法的时间复杂度较高,尤其是在处理大量数据时,可能导致接口响应缓慢。
需要检查代码(定位最近发版的代码)中是否存在循环、递归、排序等操作,尤其是在处理大规模数据时。使用 JProfiler 等工具进行性能分析,查看热点代码。
2)频繁的同步或死锁:使用同步机制(如 synchronized
、ReentrantLock
)可能导致线程阻塞,影响并发性能。
需要检查代码中是否有不必要的同步,分析是否存在死锁或高竞争的情况,要降低锁的粒度等。
3)缓存未命中或缓存过期:如果系统频繁访问数据库,而没有使用缓存,或者缓存配置不当,也可能导致接口响应慢。需要检查缓存的配置和命中率分析缓存的有效性。
外部服务依赖问题
1)外部服务的响应慢:如果接口调用了外部服务(如支付网关、第三方 API 等),外部服务响应慢会直接影响接口的响应时间。
检查外部服务的响应时间,可以使用 超时机制 和 熔断器 来处理外部依赖失败的情况。
实际上也有可能就是API 请求的并发量过高,接口可能承受了超出预期的并发请求,导致系统的负载过高,响应变慢。
此时可以通过负载均衡、限流等机制,控制并发请求量,避免单一接口被过多请求压垮。
OOM排查
OOM类型 | 原因分析 | 解决方案 |
堆内存溢出(Heap OOM) | 1. 大量对象创建未释放 2. 内存泄漏(对象被长期持有) 3. 缓存滥用 |
1. 调整堆大小(-Xmx/Xms) 2. 使用MAT/VisualVM分析堆转储 3. 优化数据结构与缓存策略 |
方法区/元空间溢出(Metaspace OOM) | 1. 类加载过多(如动态代理、反射) 2. 类加载器泄露 |
1. 增加元空间大小(-XX:MaxMetaspaceSize) 2. 检查类加载器实现并优化卸载逻辑 |
栈内存溢出(Stack Overflow) | 1. 递归调用过深 2. 线程栈设置过小 |
1. 优化递归算法或改用迭代 2. 调整线程栈大小(-Xss) 3. 使用线程池限制线程数 |
GC Overhead Limit Exceeded | 1. GC频繁但回收效率低 2. 内存泄漏或大对象占用 |
1. 禁用GC Overhead限制(-XX:-UseGCOverheadLimit) 2. 优化代码减少内存占用 |
本地方法栈溢出(Native Stack OOM) | 1. 创建过多本地线程 2. 本地代码内存泄漏 |
1. 降低线程栈大小(-Xss) 2. 检查JNI代码并修复泄漏 3. 增加系统线程限制 |
超大数组分配失败 | 1. 试图分配超过堆最大限制的数组(如new byte[Integer.MAX_VALUE]) | 1. 检查数组分配逻辑合理性 2. 增加堆内存(-Xmx) 3. 分块处理大数据 |
交换空间不足(Swap OOM) | 1. 物理内存耗尽 2. 本地代码占用过多内存 |
1. 扩大系统交换分区 2. 优化本地代码内存使用 3. 关闭不必要的后台进程 |
直接内存溢出(Direct Buffer OOM) | 1. NIO直接缓冲区未释放 2. 内存映射文件过大 |
1. 调整直接内存上限(-XX:MaxDirectMemorySize) 2. 及时释放DirectByteBuffer资源 |
排查方法
1 GC总体思路是尽可能在新生代进行回收,减少对象进入老年代
2 分析JVM内存占用(OOM或内存泄漏怎么办)
查看GC日志后若出现
1)频繁FullGC且没有内存泄漏,可能是Survivor空间太小或晋升年龄设置的太小,需要调整这两者的参数大小。
2)内存泄漏的特征
老年代持续增长,即使多次Full GC仍不下降
Full GC频率上升
GC后可用内存变少
最终老年代无法分配空间而OOM
可以在启动Java程序时添加JVM参数,自动生成OOM时堆内存快照的HeapDump文件并存在指定的路径。通过MAT等工具分析内存使用情况,找出哪些对象使用了大量内存,再定位到具体代码解决问题。
暂时总结到这里,后续有没有总结到的再回来补。