ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MySQL慢SQL排查:从五层模型到全链路性能优化实战

2026/9/8 5:27:41 拓冰建站 浏览量
MySQL慢SQL排查:从五层模型到全链路性能优化实战 一条SQL在MySQL里跑得慢绝大多数人第一反应是“加索引”、“看慢查询日志”、“调参数”。但真等你把所有常见手段都试了一遍可能问题还在那里。我做MySQL性能排查这些年最大的体会是**MySQL的快慢从来不是单点问题而是内存、CPU、算法、网络、OS调度这五个维度协同作用的结果。**今天这篇就想沿着一条SQL从客户端发出到服务器返回结果的完整路径把这五个维度挨个庖丁解牛讲清楚每个环节到底在干什么、瓶颈可能卡在哪里、以及如何判断到底是哪一层的锅。这篇文章适合几类人被慢SQL折磨但没有系统排查思路的后端开发、刚接手数据库维护想建立整体性能观的DBA、以及那些能看懂EXPLAIN但搞不懂为什么“有时候索引明明走了还是很慢”的同学。我会尽量把每个技术点的原理和排查手段都讲透全程干货不绕弯子。1. 内容整体设计与思路拆解1.1 为什么MySQL性能是个“五层问题”先建立一个整体观念一条SQL从发出去到拿到结果实际上要穿越五个完全不同的子系统。它们分别是内存、CPU、算法、网络、OS调度任何一个环节出问题都会表现为“这条SQL慢”。拿个最直观的例子来说你以为慢是因为没走索引结果你加上索引之后发现还是慢这时候问题可能根本不在索引。可能是这张表已经被OS换页换出内存了每次访问都触发磁盘IO也可能是网络层出了丢包每次往返都在重传也可能是你的MySQL线程数太多操作系统在疯狂的上下文切换中消耗了大部分CPU时间片真正留给SQL执行的CPU反而不够用了。我见过太多人排查慢SQL上来就开EXPLAIN看完索引就觉得已经定位了问题这是典型的单层思维。真实生产环境里瓶颈往往是跨层的。比如一个看似简单的COUNT查询慢你可能查了半天SQL最后发现是这台机器的CPU被同一宿主机上的其他虚拟机抢占严重跟MySQL本身的配置一毛钱关系都没有。所以建立“五层模型”的意义在于你能把慢SQL这个复杂问题拆解成可定位的子问题然后逐层排查。1.2 一条SQL的完整生命周期为了把后面的内容串起来我先简单描述一遍一条SELECT语句的完整旅程。整体分九个阶段客户端把SQL语句打包成网络包通过TCP连接发送到MySQL服务器MySQL的网络模块接收数据包放回内存缓冲区线程读取SQL文本交给解析器做词法分析和语法分析生成解析树优化器基于解析树生成执行计划选择访问路径全表扫描、索引扫描、多种Join顺序等执行器按照执行计划调用存储引擎接口读取数据InnoDB存储引擎先在Buffer Pool里找数据页找不到就去磁盘读读上来之后放入Buffer Pool执行器对读取的数据做排序、分组、Join等操作这部分可能需要临时表临时表又涉及内存或磁盘执行器把结果集通过网络返回给客户端最后是收尾阶段释放内存、记录日志、更新状态变量这个链条里第1和第8步主要受网络影响第2和第6步主要受内存影响第3到第7步特别是优化器的代价估算、排序分组、Join操作主要受算法影响任何一步的CPU指令执行包括解析、优化、比较、计算都受CPU以及OS调度影响。所以你看一条SQL但凡慢你根本没法只归因到一个层面。1.3 庖丁解牛的“刀法”如何给慢SQL做层次化拆解跟庖丁解牛的道理一样你得先看清楚“牛”的内部结构才知道刀往哪里下。对于慢SQL我的做法是把问题按层次切分每一层回答不同的问题网络层SQL在传输过程中是否花费了不该花的时间往返次数多不多包大不大内存层数据是否常驻内存Buffer Pool命中率如何临时表是否落盘算法层访问路径是否最优JOIN顺序是否合理排序是否避免了filesortCPU层CPU是否存在浪费是否存在无效计算是否因为数据分布问题导致CPU空转OS调度层MySQL线程被OS挂起/唤醒的次数多不多NUMA架构下是否存在跨节点访问这个拆解顺序有讲究。我通常建议从最便宜的手段开始排查——先看网络和内存因为它们的排查成本低、见效快然后再深入到算法层通过慢查询日志和EXPLAIN判断最后才到CPU和OS调度层因为这两层往往需要结合系统监控工具才能看清排查成本最高。2. 内存数据在不在内存里决定了你是在“读内存”还是“等磁盘”2.1 Buffer Pool是MySQL性能的第一道防线InnoDB的Buffer Pool缓冲池是MySQL内存管理的核心。MySQL做任何读写操作都不会直接跟磁盘打交道而是先把磁盘上的数据页读入Buffer Pool所有后续操作都在内存里完成。等到内存里的数据页被修改了再由后台线程异步刷写到磁盘。这里最关键的一个指标是Buffer Pool命中率——也就是你请求的数据页有多少比例直接能在内存里找到。命中率越高说明你的SQL主要是在“读内存”速度自然快命中率越低说明有大量请求需要去磁盘捞数据而一次磁盘随机读取的延迟大约是内存访问的10万倍内存几十纳秒磁盘随机IO通常要几毫秒到十几毫秒延迟直接拉满。Buffer Pool够不够用的量化判断可以这样算你已经分配给Buffer Pool的内存是innodb_buffer_pool_size而你的工作集也就是最频繁访问的数据总量如果大于这个值就一定有一部分数据会被频繁淘汰和重新加载。生产环境我一般建议-- 查看Buffer Pool相关状态8.0版本 SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_read_requests; -- 总读请求次数 SHOW GLOBAL STATUS LIKE Innodb_buffer_pool_reads; -- 从磁盘读的次数命中率公式是命中率 (read_requests - reads) / read_requests * 100%如果这个值长期低于99%你首先要考虑的不是优化SQL而是给MySQL加内存或者收缩工作集。比如一张几亿行的冷数据表被某些全表扫描SQL拖进Buffer Pool占掉了大量空间热数据反而被挤出去命中率就崩了。2.2 一个真实案例Buffer Pool被“污染”导致全库变慢曾经我处理过一个典型的Buffer Pool“污染”案例。某个业务上线了一个后台导数据功能每天定时跑一批大查询扫描近一年的订单表。这些查询本身是离线任务不需要很高的实时性但它们每次扫描都会把大量数据页加载到Buffer Pool里把真正高频访问的用户会话数据页给挤了出去。结果白天业务高峰期用户中心的查询命中率从99.8%掉到90%数据库整体响应时间翻了好几倍。这个问题的本质是InnoDB的LRU链表虽然做了冷热处理但大批量顺序扫描仍然可能把热数据挤出缓存。解决方案有几个层面把大查询挪到业务低峰期执行对大表扫描的SQL做限流或者在SQL层面强制走更高效的索引调大Buffer Pool让工作集和扫描集都能装下需要物理内存支持在MySQL 8.0里可以监控Innodb_buffer_pool_bytes_data和Innodb_buffer_pool_bytes_dirty观察数据分布另外MySQL的Buffer Pool淘汰策略默认是改进型LRU链表分为young子列表和old子列表比例默认是37%innodb_old_blocks_pct37。old区域里的页如果被第二次访问才会被提升到young区域并且有一个innodb_old_blocks_time参数默认1000毫秒防止刚读入的页立刻被提升。这个机制的设计意图是如果一次全表扫描的数据页只在扫描那一次被用到那它就不该污染热数据区。理解了这一点你就知道调参的方向在哪而不是盲目地把innodb_old_blocks_time调到0。2.3 排序缓冲与临时表隐藏的内存杀手除了Buffer PoolMySQL还有一类内存消耗大户——排序缓冲和临时表。当你的SQL包含ORDER BY、GROUP BY、DISTINCT、UNION这些操作且无法直接利用索引的有序性时MySQL需要额外排序。如果排序数据量不超过sort_buffer_size排序过程发生在内存中超过之后MySQL会把中间结果写到磁盘上的临时文件再执行归并排序。一落盘速度就会有数量级的下降。sort_buffer_size这个参数有个特点它是每个线程单独分配的不是全局共享的。也就是说如果同时有100个并发连接都在做排序理论上最多可能分配100份sort_buffer_size。所以网上很多人教你把sort_buffer_size调到128MB甚至更大解决单条SQL排序慢的问题在高并发场景下反而会害死你——内存瞬间被吃光系统开始大量swap整库性能雪崩。我通常的做法是先通过EXPLAIN看有没有Using filesort有的话先想办法用索引消除排序而不是急着调大排序缓冲。举例说明-- 如果这条查询需要按照create_time排序且过滤条件是user_id -- 那么建立联合索引 (user_id, create_time)就能避免filesort SELECT * FROM orders WHERE user_id 123 ORDER BY create_time DESC;同理GROUP BY如果处理不当也会产生临时表。临时表分为内存临时表和磁盘临时表分别由tmp_table_size和max_heap_table_size控制。内部临时表超过这两个参数的最小值时MySQL会自动转换为磁盘临时表MyISAM或InnoDB on disk。磁盘临时表的开销远高于内存临时表所以在慢查询日志里如果看到大量Created_tmp_disk_tables这就说明你的分组或去重操作产生了磁盘IO值得优化。2.4 内存排查实操怎么量化“内存够不够”量化内存压力不能只看Free Memory因为Linux会尽量用空闲内存做page cache这本身是好事。要看的关键指标是free -h重点看available那一列它代表在不触发swap的前提下还能分给应用程序多少内存。如果available很低甚至free趋近于零同时siswap in和soswap out不断增长说明系统面临严重的内存压力MySQL的Buffer Pool可能被OS换出到swap。一旦Buffer Pool的页被换到swap上访问那些页就相当于一次磁盘IO原本应该是微秒级的内存访问变成了毫秒级性能崩塌。另外MySQL自身也提供了一些很有价值的状态变量SHOW GLOBAL STATUS LIKE Created_tmp_disk_tables; SHOW GLOBAL STATUS LIKE Created_tmp_tables; SHOW GLOBAL STATUS LIKE Sort_merge_passes;Sort_merge_passes这个指标特别值得关注它表示排序过程中由于内存不足导致不得不做归并的次数。如果这个数值快速增长说明排序缓冲太小或者需要排序的数据量太大。3. CPU真正的计算瓶颈与效率陷阱3.1 CPU到底在MySQL里干什么活很多人以为MySQL的CPU消耗主要在执行SELECT的数学计算上其实完全不是。在MySQL的执行链路中CPU的主要开销集中在几个地方语法解析与优化SQL文本要词法分析、语法分析、生成执行计划优化器要做代价估算这些计算本身消耗CPU数据比较排序、去重、哈希连接、索引查找都需要比较键值锁管理与并发控制InnoDB的行锁、MVCC版本链、各种mutex和rwlock高并发下的锁竞争会消耗大量CPU网络协议处理读取网络包、解析MySQL协议、打包结果集发送数据拷贝与表达式计算将数据从存储引擎层拷贝到MySQL Server层计算表达式转换字符集其中最容易出问题的往往是第2和第4类。举个例子如果一个VARCHAR字段没加索引或者索引前缀区分度太低MySQL要做全表扫描把每一行取出来做字符串比较这里的CPU消耗比数值比较高一个量级因为字符串比较要逐字节比对还要考虑字符集和排序规则。3.2 从“等CPU”到“CPU跑满”的两种慢CPU问题导致的慢SQL有两种截然不同的表现排查方向完全不同。第一种CPU使用率很高但SQL执行效率很低。这说明有大量无效计算正在发生。常见原因包括索引失效导致全表扫描隐式类型转换比如varchar字段跟数字比较MySQL无法用上索引还要把每一行的字符串转成数字再比较函数包裹索引列比如WHERE DATE(create_time) 2024-01-01这个函数让索引完全失效MySQL只能扫描所有行对每个create_time调用DATE()函数计算CPU全部浪费在这些转换上第二种CPU使用率不高但SQL还是慢。这种情况往往不是CPU在计算而是SQL在“等”某样东西——等锁、等IO、网络等待。MySQL线程的大部分时间处于sleep或者等待状态CPU空闲但事务迟迟无法完成。所以排查的时候不能只盯着top看CPU百分比还要看%waIO等待和每个线程的状态。一条SQL如果在State列长期显示Statistics或Sending data但CPU很低说明它大概率在做大量行读取和回表也可能是在等待磁盘IO。3.3 CPU Cache容易被忽略的性能放大器关于CPU有一个被绝大部分MySQL调优文章忽略的点就是CPU Cache缓存的局部性原理。现代CPU有L1、L2、L3三级缓存访问速度差异很大L1大约1纳秒L2大约3-4纳秒L3大约12-40纳秒内存访问大约是80-100纳秒。虽然都是纳秒级但比例差距可以达到上百倍。MySQL的数据结构设计里大量用到了“局部性”的思维。比如InnoDB的索引使用B树而不是二叉树一个关键原因就是B树的叶子节点是连续存储的一个数据页里能放下更多键值做范围扫描时连续读取同一个页的数据大概率能命中CPU缓存减少对内存的访问次数。反过来说如果一条查询需要随机访问大量分散的行比如回表次数很多每行的读取都要跨越不同的数据页这些页分散在内存的各个位置CPU缓存的命中率就非常低每次都要访问内存效率自然下降。所以我一直强调一个观点很多时候优化SQL的“算法”就是优化CPU缓存的“局部性”。让你少回表、少扫描无效行不仅减少IO还减少了CPU Cache Miss性能提升是双重的。3.4 NUMA架构下的CPU调度陷阱现在的服务器基本都是多路CPU走的是NUMA架构。NUMA的意思是每个CPU有自己的本地内存访问本地内存比访问远端CPU的内存快得多。如果MySQL的线程被OS调度到了一个CPU上但它要访问的数据页却被分配在另一个CPU的本地内存上就会发生跨NUMA节点访问延迟显著增加。MySQL在NUMA架构下有个经典问题内存分配不均匀导致部分节点内存耗尽而其他节点内存闲置甚至触发swap。常见表现是free -h看还有内存但MySQL日志报“Out of memory”或者numastat显示某个节点的内存使用率接近100%。Linux的默认内存分配策略是prefer或者local在NUMA下可能造成上述问题。一些部署方案会给mysqld进程启用numactl --interleaveall让内存交错分配在所有节点上避免单个节点压力过大。但注意interleave只在分配时生效如果MySQL进程已经在运行一段时间了内存页已经分配完毕改策略对存量页没有作用。这也是为什么建议在MySQL进程启动时就用numactl做绑定numactl --interleaveall /usr/sbin/mysqld --defaults-file/etc/my.cnf不过要特别提醒现在云上很多虚拟化实例实际上屏蔽了底层NUMA拓扑你在虚拟机里numactl --hardware看到的信息可能和物理机不一致。遇到这种情况不要过度调优把精力放在SQL和内存层更有效。4. 算法同一份数据不同的算法就是天壤之别4.1 索引结构里的B树到底好在哪MySQL Innodb的索引数据结构是B树这几乎是每个程序员都背过的知识点。但真正理解B树为什么快需要把数据量和IO次数放在一起算。假设一张表有1亿条记录。如果是完全无序摆放你要找一条记录平均需要扫描5000万条这是灾难。加了主键索引后B树的非叶子节点只存键值和指针每个节点存储在InnoDB的一个页默认16KB里。以主键是BIGINT8字节为例每个键值对大约要占用键值8字节 指针6字节 ≈ 14字节一个16KB的页大约能存16 * 1024 / 14 ≈ 1170个键值对。如果B树的高度是3层它最多能索引的数据量是1170 * 1170 * 1170 ≈ 16亿条也就是说查询1亿条数据中的任意一条最多只需要3次磁盘IO第一次读根节点这个节点其实大概率常驻Buffer Pool第二次读中间层节点第三次读叶子节点。这个IO次数跟数据总量几乎无关这就是B树的可怕之处。这也是为什么有时候表数据量从100万涨到1个亿但走主键查询的SQL性能几乎没有劣化。理解了B树高度和扇出fanout的关系你自然就明白一个道理主键越短B树扇出越高树越矮IO次数越少。所以设计表时用自增BIGINT做聚簇主键比用UUID字符串做主键好得多不仅节省空间还能降低B树高度。4.2 JOIN算法从嵌套循环到哈希连接MySQL的JOIN算法演进很有代表性。传统上MySQL的JOIN只支持Nested Loop Join嵌套循环连接就是遍历驱动表的每一行去被驱动表里找匹配行。如果被驱动表的连接列上有索引这种找法退化为索引查找性能还可以如果没有索引那就是全表扫描性能惨不忍睹。我曾经处理过一个经典的慢JOINA表10万行B表100万行连接条件是A.user_id B.user_id但B表没建user_id索引。嵌套循环的实际开销是10万次对B表的全表扫描也就是10万 * 100万 1000亿次行比较这种查询跑几小时都正常。而加上B表索引后每次查找从全表扫面退化为索引查找树高2-3的索引查询整体开销骤降到大概30万次行比较10万次索引查找每次只需访问3-4个节点性能差了好几个数量级。MySQL 8.0.18开始正式支持Hash Join。Hash Join的思想是先把小表读入内存建立哈希表然后扫描大表用每一行去哈希表里探测。这个算法适合等值连接特别是被驱动表没有索引时Hash Join通常远快于Block Nested Loop。但要注意Hash Join建立哈希表需要内存。如果内表太大哈希表放不进join_buffer_size就会在磁盘上做分块哈希性能也会打折。实践中的经验是对于需要JOIN的SQL先看EXPLAIN的输出检查连接类型和是否使用索引如果被驱动表连接列没有索引优先考虑补索引而不是指望优化器一定选对算法。MySQL的优化器有时候会选错驱动表这时候可以通过STRAIGHT_JOIN强制驱动表顺序但这个操作要谨慎只在确有必要的时候用。4.3 排序算法内存中的快速排序与磁盘归并MySQL排序在内存里用的是快速排序这是平均复杂度O(nlogn)的排序算法里常数因子最小的之一。但如果待排序数据量超过sort_buffer_size就需要用归并排序的思想把数据分成多个块每个块在内存里排序后写回磁盘临时文件最后多路归并。每多一次归并就多一轮磁盘读写这就是前面那个Sort_merge_passes状态变量增长的来源。排序优化的首要思路不是调大排序缓冲而是让排序消失。怎么让排序消失让数据按照你要的顺序天然排好——这就是联合索引前缀的作用。比如SELECT * FROM orders WHERE status 1 ORDER BY create_time DESC LIMIT 100;如果建了(status, create_time)联合索引那么由于B树叶子节点本身按键值有序排列MySQL扫描索引时天然按status过滤、按create_time排序整个过程不需要任何额外的排序操作。这个优化往往能把一条几百毫秒的查询降到几毫秒因为它把一个O(nlogn)的排序算法彻底变成了O(n)的顺序扫描。4.4 优化器代价模型MySQL如何“猜”哪个快MySQL优化器选择执行计划的依据是代价模型。它会对每个可能的访问路径估算一个“成本值”包括IO成本、CPU成本、内存和网络成本然后选择成本最低的那个。但这个估算依赖统计信息ANALYZE TABLE更新的cardinality基数就是关键输入。一个常见坑是统计信息过期导致优化器选错索引。比如某张表通过UPDATE把某个字段的值从“全都是1”更新成了“每个值都不同”但优化器还拿着旧的“区分度极低”的统计信息以为索引没用结果选了全表扫描。这种问题可以通过刷新统计信息解决ANALYZE TABLE your_table;更麻烦的是MySQL优化器某些时候对多表JOIN顺序的估算并不准确特别是涉及多张表、多个索引可选时可能选择一个次优执行计划。这通常需要经验判断。我见过最典型的例子是两个表各有一个索引等值连接时优化器选错了驱动表导致被驱动表每次都要做代价更高的查找。这时候先用EXPLAIN分析当前的JOIN顺序然后手动调整关联顺序或使用STRAIGHT_JOIN往往有奇效。4.5 实操技巧用EXPLAIN读优化器的“内心戏”关于算法层面的排查最重要的工具就是EXPLAIN。但很多人只会看type和key忽略了更多信息。我列出几个需要重点关注的列select_type是否为子查询、DEPENDENT SUBQUERY等DEPENDENT子查询往往意味着逐行执行性能极差type从好到差大致是systemconsteq_refrefrangeindexALL。如果出现ALL全表扫描需要警惕possible_keys实际可选索引key实际使用的索引rows优化器估算的需要扫描的行数这个值跟实际值偏离太大说明统计信息可能过期filtered被WHERE条件过滤掉的比例值越低说明扫描了很多无用的行ExtraUsing filesort、Using temporary、Using index condition这些信息直接暴露了算法层的低效之处EXPLAIN SELECT o.order_id, u.user_name FROM orders o JOIN users u ON o.user_id u.id WHERE o.status 1 AND o.create_time 2024-01-01 ORDER BY o.create_time DESC;通过这个EXPLAIN输出你可以快速判断是走orders表的(status, create_time)索引做范围扫描再回表去users表做等值查询还是反过来以users表做驱动表全表扫描。如果优化器选了后者往往可以直接认定是统计信息偏差或索引缺失导致。5. 网络传输链路里最容易忽略的延迟黑洞5.1 网络往返不只是“慢”一个字很多开发者在本地开发时感觉SQL都很快一上生产环境就慢了第一反应往往是数据库服务器不行。但实际上生产环境数据库和应用服务器通常不在一台机器上中间隔着网络。即使是同机房内网单次RTT往返时间在0.1ms到0.5ms之间跨可用区的话一次RTT可能1ms以上。这个延迟对单个查询影响较小但对高并发OLTP系统影响巨大。**一个请求如果涉及10次网络往返每次往返0.5ms光网络延迟就5ms而SQL本身可能只需要1ms。**所以降低网络往返次数往往比优化SQL本身更有效。MySQL是半双工协议客户端和服务器之间每次只能有一方在发送数据。发出SQL后客户端必须等待服务端响应才能发送下一条。这意味着任何不必要的来回通信都会直接延长总耗时。对于OLTP场景我强烈建议使用连接池减少新建连接的三次握手开销尽量合并小查询但要注意别合并成又大又慢的批量查询导致锁持有时间过长避免SELECT *只取需要的列减少网络传输的数据量5.2 大结果集与max_allowed_packet一个很容易被忽视的问题是结果集过大导致网络传输时间过长。比如一条SQL查询出来的结果集有200MB在万兆内网上传输也要0.2秒但如果应用服务器到数据库是千兆网络那就要2秒再加上TCP的窗口限制和拥塞控制实际可能要更久。同时MySQL的max_allowed_packet参数限制了单次传输的最大包大小。如果包太大客户端和服务器都会报错。我见过不少人把max_allowed_packet调得极大以为能解决大包传输问题其实这个参数设置得是否合理要看你的实际业务场景。如果一条SQL确实需要返回较大结果集比如数仓ETL的抽取任务把参数调大是合理的但如果是OLTP线上业务出现大结果集通常意味着查询本身有问题——你不应该让在线业务一次拉几千上万行数据。5.3 网络排查实操先分清是慢在网络还是慢在SQL判断慢SQL是不是网络的问题有一个非常简单的办法在MySQL服务器本机执行同样一条SQL看看耗时在应用服务器上执行同一条SQL看看耗时两者相减大概就是网络层带来的额外开销。如果本机执行只要10ms而应用服务器执行要50ms那40ms的差距大概率是网络造成的。此时你该查的就不是SQL和MySQL参数而是网络链路质量。另一个实用工具是tcpdump抓包看数据库服务端的TCP重传率。如果抓包发现大量重复的ACK或者重传包说明网络链路有丢包TCP的拥塞控制和超时重传机制会直接把延迟放大几十倍。这种场景下优化SQL等于缘木求鱼先找网络团队解决链路问题才是正路。# 在数据库服务器上抓MySQL端口默认3306的流量 tcpdump -i eth0 -s 0 -w /tmp/mysql_trace.pcap port 3306抓完用Wireshark打开统计TCP重传数据包的比例。重传率超过0.5%就已经需要警惕了。6. OS调度隐藏在系统底层的“看不见的手”6.1 线程调度与上下文切换MySQL是典型的线程模型每个客户端连接对应一个线程。在高并发下MySQL可能同时存在数百甚至上千个线程。操作系统要在这么多线程之间分配CPU时间片每一次线程切换上下文切换都要保存现场、恢复现场这个过程本身就要消耗CPU时间。如果系统的context switches上下文切换数量高得离谱比如超过每秒几十万次那么大量CPU时间都被浪费在切换上了真正留给SQL执行的时间反而很少。判断方法很简单vmstat 1 5关注cs列context switches每秒次数和in列interrupts每秒次数。如果cs很高但us用户态CPU和sy系统态CPU都不高说明系统在疯狂切换线程但没有干多少实质性的计算工作。这种场景常见于连接数过多的OLTP系统每个线程只干一点点活但线程数量巨大切换开销成了主导。优化方向减小连接池大小控制并发线程数启用MySQL的thread_pool插件Percona分支或者MySQL Enterprise版本限制同时运行的线程数通过排队机制降低切换开销应用层做限流避免瞬间打满连接6.2 系统调用与用户态/内核态切换MySQL的每次磁盘IO、每次网络收发最终都要通过系统调用进入内核态。如果用户态和内核态之间频繁切换系统态CPUsy列占比就会升高。正常情况下sy占比不应该超过30%。如果sy很高说明MySQL在频繁做系统调用比如大量的小型随机IO、频繁的内存申请释放、频繁的锁操作。一个比较隐蔽的例子是把innodb_flush_log_at_trx_commit设置为1时每次事务提交都要调用fsync把日志刷到磁盘。为了数据安全这通常是必要的但如果你跑的是批量导入任务可以通过合理分组提交innodb_log_write_ahead_size、innodb_log_buffer_size等参数 适当调整sync_binlog来减少fsync次数进而降低用户态/内核态切换开销。不过这种调整一定要权衡数据安全生产环境不能无脑关掉。6.3 I/O调度与Page Cache的叠加效应最后还要说一个和OS调度相关但容易被忽略的点Linux Page Cache页高速缓存。MySQL的InnoDB有自己的Buffer Pool但其上层的文件读写还是要经过操作系统的Page Cache。简单说MySQL从磁盘读一个页其实是先读入Page Cache再从Page Cache拷贝到Buffer Pool。这意味着即使Buffer Pool没命中如果Page Cache命中了比如刚才有别的进程读过同一份数据文件还在Page Cache里这一轮IO也是纯内存操作不需要真正的磁盘寻道。所以有时候我们看到磁盘IO很低但SQL还是慢可能就是卡在了Page Cache到Buffer Pool的数据拷贝上或者是Page Cache本身被truncate导致每一次都重新走磁盘。调优时要特别注意MySQL的innodb_flush_method参数。在Linux上建议好好学习O_DIRECT和fsync的语义。O_DIRECT模式让InnoDB的数据文件读写绕过操作系统Page Cache由InnoDB自己的Buffer Pool管理这种模式减少了内存的双重拷贝在大内存、高并发环境下通常表现更好。如果设置的是fsync不绕过Page Cache那么OS的脏页回写策略、内存回收策略都会直接影响MySQL的表现。6.4 从系统层定位SQL慢的综合命令关于OS调度层面的排查我提供一个组合拳# 1. 看系统整体负载情况注意截取业务高峰期的数据 sar -u -r 1 5 # 2. 看上下文切换和运行队列 vmstat 1 10 # 3. 看CPU和中断是否均衡分布 mpstat -P ALL 1 5 # 4. 查看哪个线程在消耗CPU top -Hp $(pgrep mysqld)其中top -Hp可以列出mysqld进程内的所有线程及CPU消耗配合performance_schema.threads表有时候还能直接对应到具体连接的SQL。7. 综合排查从现象到根因的实战路径7.1 一套可复制的MySQL慢查询排查流程把五个维度串起来我总结了一套“从现象到根因”的排查流程分享出来供参考。这个流程的关键点在于先排除低成本因素再深入高成本因素避免在一棵树上吊死。第一步确认“慢”的定义和范围。打开慢查询日志统计慢查询数量、平均耗时、耗时分布确认是偶发慢还是持续慢。第二步抓现场。开启performance_schema或者临时打开slow_query_log并调大long_query_time记录下具体是哪些SQL慢避免“凭感觉猜”。第三步做“本机快照”对比。在MySQL服务器本机执行同一条SQL看耗时变化排除网络因素。第四步看系统层指标。执行top、vmstat、iostat判断是CPU忙、IO忙、还是上下文切换频繁。这里的重点是观察慢SQL发生时段的系统状态单独看某一时刻的值没有意义要拿峰值时段的采样数据。第五步用EXPLAIN分析慢SQL的执行计划检查索引使用、JOIN顺序、临时表、filesort。第六步看内存层指标。Buffer Pool命中率、Sort_merge_passes、Created_tmp_disk_tables判断数据是否在工作集内、临时数据是否落盘。第七步定位到具体层面后针对性优化。比如是索引问题就改索引内存不够就扩容或缩小工作集网络问题就联系网络团队OS调度问题就调整线程池、NUMA绑定等。7.2 一个综合案例一个慢查询牵出的“五层”连锁反应我印象最深的一次排查是某个核心查询在高峰期从10ms暴涨到2秒。刚开始怀疑SQL问题EXPLAIN显示走主键查询按理说不可能慢成这样。最后通过top发现MySQL所在机器整体wa指标很高磁盘IO持续在95%以上。再看Buffer Pool命中率因为服务器内存被另一组大数据任务挤占Buffer Pool的可用内存减少命中率掉到90%大量查询开始走磁盘。接着看网络抓包发现网络本身倒没有问题。IO高企导致SQL等待磁盘而SQL等待又导致连接数堆积连接数一多上下文切换飙升CPU的sy占比也上去了。最终一个单一的内存不足问题连带触发了IO、连接、调度等多层连锁反应。这个案例给我的启发是不要只看一层也不要只信一个工具。慢SQL往往是一个因素触发多个因素共同恶化的结果。你必须把五层模型当成一个整体才能看到问题的全貌。7.3 常用监控指标速查维度关键指标正常参考范围异常信号内存Buffer Pool命中率≥99%低于95%需重点关注低于90%基本是内存不足或扫描污染内存可用内存available20%物理内存持续低于10%有swap交换风险CPU用户态系统态CPU总和80%持续超过80%说明计算密集CPUCPU等待IOwa10%超过30%说明存在严重IO瓶颈网络ping/RTT延迟内网1ms超过2ms需关注链路质量网络TCP重传率0.5%超过1%需联系网络团队OS调度上下文切换cs5万次/秒持续超过10万次/秒需关注线程数算法Sort_merge_passes基本为0持续增长说明排序内存不足或需优化排序逻辑算法Created_tmp_disk_tables与总临时表比例10%比例过高说明临时表落盘频繁7.4 集群环境下的额外考量如果MySQL是主从复制架构还要特别关注网络和OS调度对复制链路的影响。binlog的传输依赖网络从库的SQL thread回放binlog也涉及磁盘IO和内存。如果主库写并发很高binlog量很大而主从之间的网络带宽不够复制延迟就会逐步加大。这种副作用表面上看起来是“从库查询慢”实际上根因又在网络和主库写入压力上排查逻辑是完全一样的。8. 写在最后一些实操心得写了这么多最后分享几个我在实际操作中的体会。第一调参永远排在优化SQL后面。我见过太多人先改innodb_buffer_pool_size、改sort_buffer_size结果问题压根不是参数问题而是少建了一个索引。先把SQL写对、把索引建对再谈系统参数。因为参数调错了影响面是全局的而SQL优化是局部的、安全的。第二让数据尽量在内存里让查询尽量走索引让排序尽量天然有序让网络尽量少来回。这四句话基本覆盖了MySQL性能优化的核心思路其余的都是细节。哪怕你记不住复杂参数把这几条原则刻在脑子里排查问题的方向就不会跑偏。第三监控数据一定要留历史。很多问题都是在高峰期突发的等你去现场时现场已经没了。平时把performance_schema的关键指标、slow_query_log、系统层的sar数据都记录下来出问题时才有据可查不至于手忙脚乱。第四不要迷信某一个玄学参数要迷信可复现的测试。生产环境改动之前先在测试环境构造真实数据量和并发跑一遍压测和对比确认收益再上线。MySQL的优化没有任何银弹适合别人的参数不一定适合你的业务。最后再送一个压箱底的小技巧EXPLAIN ANALYZEMySQL 8.0.18可以实际执行查询并输出每个步骤的真实执行时间和行数很多EXPLAIN估算不准的场景用它一眼就能看出问题出在哪个算子。先用它做量化分析再动手优化比拍脑袋改配置靠谱得多。MySQL的性能优化是一场持久战但只要把这五层架构吃透了任何慢SQL在你眼里都能拆解成一个个可定位的小问题。剩下的就是不断地实践、积累、复盘。希望这篇能帮你把MySQL这头“牛”真正解剖明白。