数据库慢的时候,我最先看哪 5 个地方?(线上经验总结)先说个真实场景:某天凌晨 1 点,线上告警突然响起来,接口超时率从 0.5% 飙到 15%,数据库 CPU 使用率直接顶到 99%。群里一片慌乱,第一反应就是"数据库出问题了"。但数据库慢,从来都不是数据库自己慢,而是整条链路某个环节堵住了。我做了这么多年数据库运维和架构优化,线上告警处理过几百次,总结下来就是:排查数据库慢,最先看 5 个地方,基本能覆盖 80% 以上的问题。这篇文章就把我这套排查顺序和具体操作完整写出来,不整虚的,全是线上摸爬滚打出来的经验。这 5 个地方分别是:慢查询日志、数据库连接数、系统资源(CPU/内存/磁盘 IO)、锁等待与阻塞、主从复制与网络延迟。为什么是这 5 个,不是因为教科书上这么说,而是因为每一条都是我在线上真实踩过坑、见过血的。这篇文章适合正在做后端开发、应用运维、数据库管理的人参考,尤其适合那些一遇到"数据库慢"就手足无措、不知道从哪下手的同学。我会把每个检查点要看的指标、要执行的命令、要避开的坑都讲清楚,最后再附一个完整的线上排查案例复盘。1. 为什么是这 5 个地方:数据库变慢的排查总纲1.1 数据库变慢的本质是什么先把这个事想明白:一条 SQL 从客户端走到数据库再返回结果,中间经历的过程大致是——应用通过连接池拿连接、把 SQL 发给数据库、数据库做语法解析、生成执行计划、执行(可能涉及索引扫描、内存排序、磁盘读写)、最后把结果集返回给应用。这条链路里任何一个环节出问题,最终表现都是"数据库慢"。所以排查数据库慢,不能只盯着数据库进程本身,而是要把整条链路的各个环节都过一遍。我常拿开餐厅打比方:客人反馈上菜慢,原因可能是点单的人太多(连接数爆了)、可能是后厨切菜太慢(SQL 执行效率低)、可能是灶台不够用(CPU 瓶颈)、可能是备菜区堵住了(锁等待)、也可能是外卖配送环节出了问题(网络/主从延迟)。只问"厨师是不是太慢了",永远找不到真正的瓶颈。这也是为什么我不建议一上来就抓着一两条慢 SQL 猛优化,更不建议直接重启数据库,盲目重启只会掩盖问题,等下一次故障再教你做人。正确的做法是:先确认问题到底出在链路哪一段,再针对性地处理。1.2 我的排查顺序和理由每个人的排查习惯不一样,我的顺序是固定的:先看连接数,再看慢查询,然后看系统资源,接着查锁等待,最后看主从和网络。这个顺序的优先级是"从入口到内部、从整体到局部"来的。连接数是最外层的入口,如果连接池都满了,所有新请求根本进不来,这时候里面 SQL 再快也没用,所以最先看。连接数正常的话,下一步自然要看数据库到底在处理什么 SQL,也就是慢查询日志。慢 SQL 往往是 CPU 和 IO 飙高的根因,看完慢 SQL 再看系统资源,就能把"因"和"果"对应上。如果 SQL 不快、资源也不高,但业务就是卡,那十有八九是锁等待或者外部链路问题,再往锁和主从方向查。[!提醒] 这个顺序不是死的。如果你拿到告警的第一时间就能看到 CPU 直接打满,那肯定是先看慢 SQL;如果告警提示的是too many connections,就先从连接数入手。关键是脑子里要有一条清晰的链路地图,而不是碰到问题就乱撞。2. 第一站:慢查询日志,先把“嫌疑 SQL”捞出来2.1 慢日志到底看什么:时间、频率、样本三件套慢查询日志相当于数据库自带的黑匣子,记录所有执行时间超过阈值(long_query_time)的 SQL。这是排查数据库慢问题最直接的入口,因为大部分线上数据库变慢,根因都是某几条 SQL 不健康。打开慢日志看的时候,我一般只看三样东西:单条耗时、出现频率、扫描行数。单条耗时超过 1 秒(具体阈值看业务规模)是硬指标,但比这个更重要的是频率——一条 SQL 跑 10 秒但一天只跑一次,和一条 SQL 跑 2 秒但每秒跑 100 次,显然后者才是真正拖垮数据库的元凶。扫描行数则是判断 SQL 效率最核心的指标,很多慢 SQL 表面上看只花了几百毫秒,但实际上扫描了几百万行,只不过数据量还没大到让它彻底卡死而已。我在某电商系统排查过一个问题:订单查询接口每到整点就超时,慢日志里发现一条查询订单详情的 SQL 经常出现。单看执行时间五六百毫秒,不算特别夸张,但它每天调用几十万次,累积起来把 CPU 吃满了。这种事如果你只看慢日志里最慢的那几条而忽略高频的中等耗时 SQL,很容易漏掉真正的病根。所以看慢日志一定要做聚合,按 SQL 模板把执行次数、总耗时、平均耗时、扫描行数排个序,优先处理那些"又频又慢的"。2.2 拿到慢 SQL 后别急着改,先看执行计划很多人一看到慢 SQL 就凭经验加索引,这是最容易翻车的操作之一。我不止一次见过加完索引反而更慢的情况。正确的做法是拿到慢 SQL 之后先EXPLAIN看执行计划,搞清楚数据库执行这条 SQL 时到底是怎么走的。EXPLAIN输出里我最关注这么几个字段:type:访问方式,从system、const、eq_ref、ref、range到index、ALL,越往后越差。看到ALL就是全表扫描,基本可以断定问题出在这。key:实际用到的索引。NULL代表没走索引。rows:预估扫描行数。这个数跟实际执行时间强相关,几万行以内通常没事,超过百万行就要警惕。Extra:附带的额外信息,重点看有没有Using filesort(文件排序)和Using temporary(临时表)。我遇到过一个排序导致性能雪崩的案例,SQL 本身不慢,但ORDER BY的字段没覆盖在索引里,导致每次查询都要额外做文件排序,数据一多就炸。[!注意]EXPLAIN里的rows是预估值,不是精确值。尤其是在多表关联、带子查询的场景下误差异常大。所以看完执行计划只代表有了方向,最终还得结合实际运行情况和耗时数字下结论。2.3 慢日志的开启与采样实操很多生产库默认没开慢日志,或者阈值设得太高形同虚设。我的做法是至少在核心库开启慢日志,阈值先设置 1 秒,再根据业务线情况逐步调整。MySQL 下可以直接在线开启:-- 查看当前配置 SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time'; -- 开启慢日志(动态参数,线上可用) SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; SET GLOBAL log_queries_not_using_indexes = ON;注意long_query_time在 MySQL 里单位是秒,默认是 10 秒,这个值对大多数互联网业务来说太大了,等它记下来故障早就结束了。我一般从 1 秒起步,如果慢日志量特别大再逐步提高到 2 秒或 3 秒。另外线上开慢日志一定要留意磁盘空间,日志产生速度超过磁盘写入速度会直接把数据库拖垮,这种事故我见过不止一次。所以要么做好日志切割和定期清理,要么用pt-query-digest这类工具做离线分析,不要长时间让慢日志裸奔。拿到慢日志原始文本后,推荐用mysqldumpslow做粗筛,再用pt-query-digest做聚合分析。后者能把 SQL 按指纹归类,输出每个模板的总执行时间、平均耗时、出现次数,直接顺着它排行靠前的 SQL 去优化就可以了。3. 第二站:连接数与连接池,最先崩溃的往往是入口3.1 连接数打满的症状与真实原因连接数问题在线上非常典型:数据库明明没在执行特别重的 SQL,但新请求就是进不来,应用日志里报各种连接超时,严重的直接报Too many connections。为什么会这样?因为连接数是数据库的入口闸门,一旦打满,所有新连接都会被拒之门外。但连接数打满往往是结果不是原因。我看到过最多的场景是:某条慢 SQL 把连接长时间占住,一个请求拿一个连接在那慢慢跑,连接池里的连接全被耗光;应用侧连接池泄漏,拿连接没归还,连接数随时间慢慢涨满;流量突增,超出了数据库的并发处理上限。很多人遇到Too many connections的第一反应是调大max_connections,这只能应急,不能根治。因为连接数打满的背后通常藏着慢 SQL 或者连接泄漏,不处理这些根因,就算把max_connections调到 1000,也只是把爆炸推迟而已。3.2 四个必看的连接指标连接数检查我用的是下面这几个指标,每一个都有不同的含义,不要混着看:指标含义正常范围参考异常情况Threads_connected当前打开的连接数低于max_connections的 80%接近上限说明连接快耗光Threads_running正在执行的连接数通常个位数到几十持续高于 CPU 核数且伴随 CPU 飙高,说明有 SQL 跑不动Threads_created累计创建的线程数增长平缓快速上涨说明频繁创建新连接,可能是连接池配置不当Connection_errors_max_connections因连接数超限被拒绝的次数0大于 0 说明已经出现过连接被打满的情况查看命令是:SHOW GLOBAL STATUS LIKE 'Threads%'; SHOW GLOBAL STATUS LIKE 'Connection_errors%'; SHOW VARIABLES LIKE 'max_connections';这里特别提醒一个容易忽略的点:很多监控面板只看Threads_connected,但这个指标正常不代表没有连接问题。真正需要关注的是Threads_running。比如某次故障里连接数 300 但Threads_running长期 80+,明显是有一堆 SQL 卡住了,只是它们还没超时释放连接而已。我排查线上问题时会先看Threads_running有没有异常波动,再顺着它定位到底是谁卡住了。3.3 连接池参数要不要调,怎么调连接池问题分两段:数据库端的max_connections和应用端的连接池配置。数据库端max_connections不是越大越好,每个连接都要占内存,连接数太多反而会让数据库上下文切换变慢。一个常规的处理思路是:先保持max_connections不变,把连接打满的原因找出来处理掉;非必要不动它。应用端连接池大小是一个经久不衰的讨论话题。早年有个说法叫"连接池大小 = CPU 核心数 × 2 + 1",这个公式在那个机器核数少的年代还行,现在高核数机器上照搬容易踩坑。更合理的参考标准是:连接数 = 数据库能支撑的并发处理能力 × 每条 SQL 的平均耗时。实际上普通业务场景下连接池设 20~50 基本够用,但不要想当然,最好通过压测找到那个临界点。压测时观察应用侧的连接池等待时间,如果等待时间明显增加而数据库 CPU 还不高,说明连接池太小;反过来如果数据库 CPU 已经吃满,连接池再大反而是帮凶。另外,连接池的maxWait(获取连接的超时时间)一定要设,不要设成无限等。线上见过没有设置等待超时,连接池打满后所有线程全部挂起等待,最后整个应用雪崩的案例,非常惨烈。4. 第三站:CPU、内存、磁盘 IO,数据库跑不动了先看资源4.1 怎么快速区分 CPU 打满和 IO 打满看完连接数和慢 SQL,接下来要看数据库所在的系统资源。这一步的目的是确认瓶颈到底在计算层面还是存储层面,因为处理方式完全不同。用top或pidstat观察的时候,重点看几个指标:CPU 使用率:用户态 + 内核态是否接近 100%。iowait:CPU 等待 IO 完成的时间占比。iowait 高但 CPU 使用率不高,是典型的磁盘瓶颈特征。负载(load average):这个要结合 CPU 核数看。核数为 16 时负载 15 不算异常,核数为 4 时负载 15 就是严重过载。判断逻辑很简单:如果是 CPU 使用率持续 95% 以上,那基本是 SQL 计算量太大,比如大量排序、聚合、全表扫描;如果是 iowait 飙到 20% 以上而 CPU 空闲,那大多是磁盘读写跟不上,要么是临时表落盘,要么是慢 SQL 触发大量随机读,要么是日志刷盘太频繁。我处理过一个典型的 IO 打满案例:某业务库的监控显示 iowait 长期 30%,CPU 却只有 20%。一开始以为是慢 SQL,但慢日志里没什么重量级选手。后来iostat一看,磁盘读 IOPS 逼近万级,再用strace和perf定位,发现是一个后台统计任务在循环扫一张历史大表,每次扫描都触发几千次随机 IO。后来把统计任务改成走索引、分批处理,iowait 立刻掉到 5% 以下。这个案例说明:不区分 CPU 和 IO 瓶颈,方向错了,再怎么优化都是白费力气。4.2 内存与缓冲池命中率的关系数据库内存这块,最重要的不是系统可用内存,而是数据库自己的缓冲池命中率。以 MySQL 的 InnoDB 为例,缓冲池(innodb_buffer_pool_size)负责缓存数据页和索引页,如果命中率高,大部分读操作直接走内存,不会触碰磁盘。查看命中率:SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'; -- 命中率 = (Innodb_buffer_pool_read_requests - Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests * 100%如果命中率长期低于 95%,说明缓冲池偏小或者查询扫描的数据量过大。前者可以尝试调大innodb_buffer_pool_size,我一般建议设置为物理内存的 50%~70%,但要留足系统和应用本身所需的内存;后者就要回到慢 SQL 层面去优化了,因为无论如何加大缓冲池,也架不住天天全表扫描。顺便提醒一个新手容易犯的错误:看到内存还有剩余就以为没问题,忽略了 swap 的使用情况。如果系统开始用 swap,数据库性能会急剧下降。检查free -h里的Swap值,长期大于 0 就要警惕了。4.3 一个容易误判的案例:IO 延迟高但 CPU 很闲真实线上有一个特别容易误判的场景——磁盘 IO 延迟高,但数据库 CPU 一点都不高。现象是接口偶发超时,数据库监控看不出明显问题,SQL 也不慢,但就是隔几分钟抖一下。这种问题通常不在数据库 SQL 层,而在存储层。我排查时习惯先用iostat -x 1看磁盘的await(平均 IO 响应时间)。SSD 正常await应该在个位数毫秒以下,如果经常跳到几十毫秒甚至上百毫秒,就说明磁盘接管请求的速度已经跟不上了。接下来要分清是磁盘本身性能不够,还是邻居贡献了太多 IO。如果是云上的共享存储或虚拟机磁盘,还要考虑是否为"吵闹的邻居"导致。这个时候单纯的数据库优化已经没用了,要么扩容磁盘、要么迁移到独立盘、要么把大查询错峰跑,才能把延迟压下来。除此之外,数据库日志写入(如 binlog、redo log)也经常是 IO 瓶颈点。遇到log相关文件所在磁盘 IO 高,可以检查是不是innodb_flush_log_at_trx_commit或sync_binlog设置过严。这两个参数都设为 1 是最安全的,但每次事务提交都要同步刷盘,性能开销非常大。如果业务允许牺牲极小的数据丢失风险,可以调整为2或0来换取大幅性能提升。不过这类参数涉及数据安全,改动前一定要拉着 DBA 和数据负责人一起评估,不能自己拍板。5. 第四站:锁等待与阻塞,数据库“卡住”和“变慢”是两回事5.1 锁等待出现时数据库的真实表现有一种典型的故障现象:数据库 CPU 不高、磁盘也不忙、慢日志里没有明显的慢 SQL,但业务就是卡——具体表现是某个或某几个 SQL 一直处于Waiting状态,后面的请求全部排队堆积。这种情况十有八九是锁等待问题。锁等待的本质是事务并发冲突:一个事务持有了某行数据的锁还没释放,另一个事务也想更新同一行,只能原地等待。如果持有锁的事务迟迟不提交也不回滚,等待的事务就会越积越多,最后把连接池和资源全部耗光。我见过最夸张的一次,是一张订单表只有一行数据被某个开发人员手动SELECT FOR UPDATE锁住,后来忘了事务没提交,结果线上所有涉及该订单的更新全部堵死,应用侧积压了几千个线程。定位命令(MySQL 8.0 及以上推荐用performance_schema):-- 查看当前有哪些锁等待 SELECT * FROM performance_schema.data_lock_waits; SELECT * FROM performance_schema.data_locks; -- 经典 InnoDB 状态(含最近死锁信息) SHOW ENGINE INNODB STATUS;再看SHOW PROCESSLIST里的State字段。如果大量线程显示Waiting for table metadata lock、Updating、Locked,基本可以确认锁问题就在眼前。这时候要第一时间找到那个"老赖"事务,用它的线程 ID 去information_schema.innodb_trx里查它已经运行了多久、处于什么状态,确认是业务代码忘了提交还是确实在跑超长事务。5.2 死锁和长时间锁等待要怎么区分死锁和长时间锁等待症状类似,但处理方式完全不同。死锁是两方互相持有对方需要的锁,谁都不让谁。MySQL 的死锁检测机制会在检测到死锁时主动回滚其中一方,应用通常会收到Deadlock found when trying to get lock错误。这种情况虽然影响单个请求,但通常不会导致大面积故障。看到SHOW ENGINE INNODB STATUS里有LATEST DETECTED DEADLOCK段落时,重点看事务 SQL 和持有索引,调整事务的执行顺序或索引选择就可以规避。长时间锁等待则更隐蔽,它本质上是"锁合法持有者迟迟不放"。处理方式不是等待数据库自己处理,而是果断介入——找到持有锁的事务,确认业务情况后KILL掉对应线程,先把业务恢复,再从代码层排查为什么事务没及时提交。所以看到锁相关现象时,第一件事永远是:搞清楚谁持锁、持了多久、该不该 KILL。5.3 业务侧要怎么配合减少锁冲突锁问题往往不是数据库能单独解决的,更多要从业务侧设计来规避。我总结三条实际有用的建议:事务要短小精悍:一个事务里尽量只包含必要的 SQL,不要在事务里做远程调用、外部接口等待。事务时间越长,持锁时间越长,冲突概率越高。同一批数据按固定顺序更新:多个事务如果都按同样的顺序(比如先更新字典表再更新业务表)去操作,死锁概率会小很多。索引设计要覆盖访问路径:更新操作如果没走索引,会从前锁升级为锁更多行甚至锁表,这个问题优化索引往往比改代码更立竿见影。6. 第五站:主从复制与网络延迟,别忘了数据库外面的事6.1 主从延迟要怎么查、怎么定位很多场景下业务感受到的"数据库慢",其实是主从架构的延迟问题。主库写完数据要异步复制到从库,如果从库的复制赶不上主库的写入速度,从库提供的数据就会过期,或者读从库的请求在等待新数据时时灵时不灵。查看主从状态的核心指标是Seconds_Behind_Master(MySQL 5.7 及以下)或Seconds_Behind_Source(MySQL 8.0 及以上):SHOW SLAVE STATUS\G这个字段越大说明主从延迟越严重。但必须说一个坑:Seconds_Behind_Master只有在 SQL 线程正在执行时才更新,如果复制因为某种原因卡住,它反而可能显示为 0,误以为延迟不存在。所以不能只看这一个字段,还要配合看Relay_Log_File、Relay_Log_Pos、Slave_IO_Running、Slave_SQL_Running这几个值,确认 IO 线程和 SQL 线程都是Yes且中继日志在推进。主从延迟常见的根因有:大事务在主库执行太久(比如一次批量更新几十万行)、从库磁盘性能太差、从库上跑了重分析任务抢资源、DDL 执行造成复制阻塞。碰到延迟先看有没有明显的大事务,把大事务拆小或放到业务低谷执行,往往能解决一大半问题。6.2 网络抖动怎么和数据库自身问题区分最后这个检查点,也是容易被忽略的:数据库本身完全没问题,但应用就是觉得"数据库慢"。这种情况通常是网络链路抖动、连接被防火墙或负载均衡干扰、TCP 重传率上升导致的。判断方法其实不难。在应用服务器上执行:ping 数据库地址 mtr -n 数据库地址如果看到丢包率大于 0 或者延迟明显升高,那么问题就不在数据库而在网络。再更深一步,用ss -i查看 TCP 连接的重传率,retrans比例超过 0.2% 就要高度怀疑网络质量了。还有一种典型的场景是应用和数据库之间的连接没有做健康检查,网络闪断后 TCP 连接处于半开状态,应用不知道连接已失效,每次请求都等超时才报错——这种问题表面上是"数据库慢",实际上要从连接池的空闲连接检测参数去调。[!提醒] 排查到这里如果还是找不到原因,就大胆回头看看是不是真的"数据库"慢。有些慢是应用层的问题,比如序列化耗时、GC 停顿、代码写递归,根本和数据库无关。不要被"数据库慢"四个字套住了思路。7. 线上实战:一次“数据库慢”的完整排查记录7.1 现场情况还原某天晚上 9 点,某个电商模块的核心接口开始告警,P99 延迟从 200ms 涨到 3 秒,错误率飙升。第一波告警信息显示:数据库 CPU 90%+,活动连接数翻了三倍,主库负载明显上涨。接到告警后我没有急着去调配置,而是按上面的 5 个检查点逐步过了一遍。7.2 逐步排查过程第一步,先看连接数。Threads_connected从平时的 100 涨到 400,Threads_running一直在 60 以上,明显不对劲。400 个连接里大部分都在执行同一条语句模板,立刻意识到是某条 SQL 出了问题。第二步,翻慢查询日志。用pt-query-digest聚合最近 30 分钟的慢日志,排名第一的 SQL 是某个商品列表页的分页查询,单条执行 2 秒多,一小时执行了几万次,累计耗时巨大。这条 SQL 平时也不快,但数据量涨到一定程度后终于爆发了。第三步,看执行计划。EXPLAIN发现这条 SQL 里关联了商品主表和库存表,库存表的关联字段sku_id没走索引,rows预估扫了 30 万行,还带了Using temporary。难怪数据库 CPU 会打满——每次查询都在做全表关联和临时表排序,量一大直接压垮 CPU。第四步,把 CPU 飙高的"果"和这条 SQL 的"因"对上之后,先做应急处理:把这条 SQL 对应的接口开启了限流和降级,同时将慢查询时间阈值临时从 1 秒调到 2 秒,避免慢日志记录量过大加剧磁盘压力。然后让 DBA 给库存表的sku_id字段补上索引,并在 SQL 层面去掉不必要的排序字段。7.3 复盘与总结补了索引之后,这条 SQL 的执行时间从 2 秒降到 80ms,数据库 CPU 回落到 20%,接口延迟也恢复了。整个排查过程从接到告警到恢复业务用了大概 40 分钟。复盘的时候最想强调的一个点:这次故障如果一开始就盲目调大max_connections,或者直接重启数据库,结果是能短暂恢复,但下一次流量高峰还会以同样方式炸掉。真正解决问题的是把"打满连接的 SQL"挖出来并优化掉。另外这次还暴露出监控层面的问题——之前只截图了数据库 CPU 和连接数,没有对慢查询做聚合告警,导致 SQL 隐患长期潜伏。事后我推动把核心库的慢查询聚合指标接到了监控平台,设置按 SQL 模板维度的告警,后来类似的隐患提前暴露了不少次,这个投入非常值。8. 写在最后:几条血泪教训和排查建议8.1 我最容易误判的三种情况经验是踩坑换来的。我把自己最容易误判的三种情况列出来,希望大家少走弯路:一是连接数打满的时候只想到调参数。前面说过很多次,连接数是结果不是原因,真正要处理的是背后那批不健康的 SQL 或者连接泄漏。调参数只是止血,根因不除下次照炸。二是把锁等待当成慢 SQL。慢 SQL 是执行效率低,锁等待是资源被人占住。方向错了会浪费大量时间,而且处理方式完全不同。遇到数据库卡死第一时间看SHOW PROCESSLIST的State,别急着分析执行计划。三是只盯着数据库内部指标,忽略存储和网络。尤其到了云环境,磁盘性能、网络链路的问题占比更高。数据库各项指标都正常但业务就是慢的时候,大胆往外面看。8.2 给新手的几条建议如果你刚开始接触这类问题,我建议你有空就做三件事:一个是把上面 5 个检查点的常用命令整理成自己的排查手册,反复练习形成条件反射;二是把数据库的慢查询日志、连接数、CPU、锁等待、主从延迟这几个关键指标都接入监控并配上告警,做到问题一冒头你就知道;三是定期做一次慢 SQL 巡检,不要等故障来了才想起看慢日志。最后再分享一个我个人的习惯:每一次线上故障处理完,我都会把现象、排查过程、根因、修复措施写成一篇简短的复盘记录,存到团队的知识库里。下次再遇到类似问题,先查知识库,五分钟就能定位,远比从头开始猜快得多。这套方法坚持下来,处理数据库慢问题的速度和准确率会有非常明显的提升。
数据库慢的时候,我最先看哪 5 个地方?(线上经验总结)先说个真实场景:某天凌晨 1 点,线上告警突然响起来,接口超时率从 0.5% 飙到 15%,数据库 CPU 使用率直接顶到 99%。群里一片慌乱,第一反应就是"数据库出问题了"。但数据库慢,从来都不是数据库自己慢,而是整条链路某个环节堵住了。我做了这么多年数据库运维和架构优化,线上告警处理过几百次,总结下来就是:排查数据库慢,最先看 5 个地方,基本能覆盖 80% 以上的问题。这篇文章就把我这套排查顺序和具体操作完整写出来,不整虚的,全是线上摸爬滚打出来的经验。这 5 个地方分别是:慢查询日志、数据库连接数、系统资源(CPU/内存/磁盘 IO)、锁等待与阻塞、主从复制与网络延迟。为什么是这 5 个,不是因为教科书上这么说,而是因为每一条都是我在线上真实踩过坑、见过血的。这篇文章适合正在做后端开发、应用运维、数据库管理的人参考,尤其适合那些一遇到"数据库慢"就手足无措、不知道从哪下手的同学。我会把每个检查点要看的指标、要执行的命令、要避开的坑都讲清楚,最后再附一个完整的线上排查案例复盘。1. 为什么是这 5 个地方:数据库变慢的排查总纲1.1 数据库变慢的本质是什么先把这个事想明白:一条 SQL 从客户端走到数据库再返回结果,中间经历的过程大致是——应用通过连接池拿连接、把 SQL 发给数据库、数据库做语法解析、生成执行计划、执行(可能涉及索引扫描、内存排序、磁盘读写)、最后把结果集返回给应用。这条链路里任何一个环节出问题,最终表现都是"数据库慢"。所以排查数据库慢,不能只盯着数据库进程本身,而是要把整条链路的各个环节都过一遍。我常拿开餐厅打比方:客人反馈上菜慢,原因可能是点单的人太多(连接数爆了)、可能是后厨切菜太慢(SQL 执行效率低)、可能是灶台不够用(CPU 瓶颈)、可能是备菜区堵住了(锁等待)、也可能是外卖配送环节出了问题(网络/主从延迟)。只问"厨师是不是太慢了",永远找不到真正的瓶颈。这也是为什么我不建议一上来就抓着一两条慢 SQL 猛优化,更不建议直接重启数据库,盲目重启只会掩盖问题,等下一次故障再教你做人。正确的做法是:先确认问题到底出在链路哪一段,再针对性地处理。1.2 我的排查顺序和理由每个人的排查习惯不一样,我的顺序是固定的:先看连接数,再看慢查询,然后看系统资源,接着查锁等待,最后看主从和网络。这个顺序的优先级是"从入口到内部、从整体到局部"来的。连接数是最外层的入口,如果连接池都满了,所有新请求根本进不来,这时候里面 SQL 再快也没用,所以最先看。连接数正常的话,下一步自然要看数据库到底在处理什么 SQL,也就是慢查询日志。慢 SQL 往往是 CPU 和 IO 飙高的根因,看完慢 SQL 再看系统资源,就能把"因"和"果"对应上。如果 SQL 不快、资源也不高,但业务就是卡,那十有八九是锁等待或者外部链路问题,再往锁和主从方向查。[!提醒] 这个顺序不是死的。如果你拿到告警的第一时间就能看到 CPU 直接打满,那肯定是先看慢 SQL;如果告警提示的是too many connections,就先从连接数入手。关键是脑子里要有一条清晰的链路地图,而不是碰到问题就乱撞。2. 第一站:慢查询日志,先把“嫌疑 SQL”捞出来2.1 慢日志到底看什么:时间、频率、样本三件套慢查询日志相当于数据库自带的黑匣子,记录所有执行时间超过阈值(long_query_time)的 SQL。这是排查数据库慢问题最直接的入口,因为大部分线上数据库变慢,根因都是某几条 SQL 不健康。打开慢日志看的时候,我一般只看三样东西:单条耗时、出现频率、扫描行数。单条耗时超过 1 秒(具体阈值看业务规模)是硬指标,但比这个更重要的是频率——一条 SQL 跑 10 秒但一天只跑一次,和一条 SQL 跑 2 秒但每秒跑 100 次,显然后者才是真正拖垮数据库的元凶。扫描行数则是判断 SQL 效率最核心的指标,很多慢 SQL 表面上看只花了几百毫秒,但实际上扫描了几百万行,只不过数据量还没大到让它彻底卡死而已。我在某电商系统排查过一个问题:订单查询接口每到整点就超时,慢日志里发现一条查询订单详情的 SQL 经常出现。单看执行时间五六百毫秒,不算特别夸张,但它每天调用几十万次,累积起来把 CPU 吃满了。这种事如果你只看慢日志里最慢的那几条而忽略高频的中等耗时 SQL,很容易漏掉真正的病根。所以看慢日志一定要做聚合,按 SQL 模板把执行次数、总耗时、平均耗时、扫描行数排个序,优先处理那些"又频又慢的"。2.2 拿到慢 SQL 后别急着改,先看执行计划很多人一看到慢 SQL 就凭经验加索引,这是最容易翻车的操作之一。我不止一次见过加完索引反而更慢的情况。正确的做法是拿到慢 SQL 之后先EXPLAIN看执行计划,搞清楚数据库执行这条 SQL 时到底是怎么走的。EXPLAIN输出里我最关注这么几个字段:type:访问方式,从system、const、eq_ref、ref、range到index、ALL,越往后越差。看到ALL就是全表扫描,基本可以断定问题出在这。key:实际用到的索引。NULL代表没走索引。rows:预估扫描行数。这个数跟实际执行时间强相关,几万行以内通常没事,超过百万行就要警惕。Extra:附带的额外信息,重点看有没有Using filesort(文件排序)和Using temporary(临时表)。我遇到过一个排序导致性能雪崩的案例,SQL 本身不慢,但ORDER BY的字段没覆盖在索引里,导致每次查询都要额外做文件排序,数据一多就炸。[!注意]EXPLAIN里的rows是预估值,不是精确值。尤其是在多表关联、带子查询的场景下误差异常大。所以看完执行计划只代表有了方向,最终还得结合实际运行情况和耗时数字下结论。2.3 慢日志的开启与采样实操很多生产库默认没开慢日志,或者阈值设得太高形同虚设。我的做法是至少在核心库开启慢日志,阈值先设置 1 秒,再根据业务线情况逐步调整。MySQL 下可以直接在线开启:-- 查看当前配置 SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time'; -- 开启慢日志(动态参数,线上可用) SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; SET GLOBAL log_queries_not_using_indexes = ON;注意long_query_time在 MySQL 里单位是秒,默认是 10 秒,这个值对大多数互联网业务来说太大了,等它记下来故障早就结束了。我一般从 1 秒起步,如果慢日志量特别大再逐步提高到 2 秒或 3 秒。另外线上开慢日志一定要留意磁盘空间,日志产生速度超过磁盘写入速度会直接把数据库拖垮,这种事故我见过不止一次。所以要么做好日志切割和定期清理,要么用pt-query-digest这类工具做离线分析,不要长时间让慢日志裸奔。拿到慢日志原始文本后,推荐用mysqldumpslow做粗筛,再用pt-query-digest做聚合分析。后者能把 SQL 按指纹归类,输出每个模板的总执行时间、平均耗时、出现次数,直接顺着它排行靠前的 SQL 去优化就可以了。3. 第二站:连接数与连接池,最先崩溃的往往是入口3.1 连接数打满的症状与真实原因连接数问题在线上非常典型:数据库明明没在执行特别重的 SQL,但新请求就是进不来,应用日志里报各种连接超时,严重的直接报Too many connections。为什么会这样?因为连接数是数据库的入口闸门,一旦打满,所有新连接都会被拒之门外。但连接数打满往往是结果不是原因。我看到过最多的场景是:某条慢 SQL 把连接长时间占住,一个请求拿一个连接在那慢慢跑,连接池里的连接全被耗光;应用侧连接池泄漏,拿连接没归还,连接数随时间慢慢涨满;流量突增,超出了数据库的并发处理上限。很多人遇到Too many connections的第一反应是调大max_connections,这只能应急,不能根治。因为连接数打满的背后通常藏着慢 SQL 或者连接泄漏,不处理这些根因,就算把max_connections调到 1000,也只是把爆炸推迟而已。3.2 四个必看的连接指标连接数检查我用的是下面这几个指标,每一个都有不同的含义,不要混着看:指标含义正常范围参考异常情况Threads_connected当前打开的连接数低于max_connections的 80%接近上限说明连接快耗光Threads_running正在执行的连接数通常个位数到几十持续高于 CPU 核数且伴随 CPU 飙高,说明有 SQL 跑不动Threads_created累计创建的线程数增长平缓快速上涨说明频繁创建新连接,可能是连接池配置不当Connection_errors_max_connections因连接数超限被拒绝的次数0大于 0 说明已经出现过连接被打满的情况查看命令是:SHOW GLOBAL STATUS LIKE 'Threads%'; SHOW GLOBAL STATUS LIKE 'Connection_errors%'; SHOW VARIABLES LIKE 'max_connections';这里特别提醒一个容易忽略的点:很多监控面板只看Threads_connected,但这个指标正常不代表没有连接问题。真正需要关注的是Threads_running。比如某次故障里连接数 300 但Threads_running长期 80+,明显是有一堆 SQL 卡住了,只是它们还没超时释放连接而已。我排查线上问题时会先看Threads_running有没有异常波动,再顺着它定位到底是谁卡住了。3.3 连接池参数要不要调,怎么调连接池问题分两段:数据库端的max_connections和应用端的连接池配置。数据库端max_connections不是越大越好,每个连接都要占内存,连接数太多反而会让数据库上下文切换变慢。一个常规的处理思路是:先保持max_connections不变,把连接打满的原因找出来处理掉;非必要不动它。应用端连接池大小是一个经久不衰的讨论话题。早年有个说法叫"连接池大小 = CPU 核心数 × 2 + 1",这个公式在那个机器核数少的年代还行,现在高核数机器上照搬容易踩坑。更合理的参考标准是:连接数 = 数据库能支撑的并发处理能力 × 每条 SQL 的平均耗时。实际上普通业务场景下连接池设 20~50 基本够用,但不要想当然,最好通过压测找到那个临界点。压测时观察应用侧的连接池等待时间,如果等待时间明显增加而数据库 CPU 还不高,说明连接池太小;反过来如果数据库 CPU 已经吃满,连接池再大反而是帮凶。另外,连接池的maxWait(获取连接的超时时间)一定要设,不要设成无限等。线上见过没有设置等待超时,连接池打满后所有线程全部挂起等待,最后整个应用雪崩的案例,非常惨烈。4. 第三站:CPU、内存、磁盘 IO,数据库跑不动了先看资源4.1 怎么快速区分 CPU 打满和 IO 打满看完连接数和慢 SQL,接下来要看数据库所在的系统资源。这一步的目的是确认瓶颈到底在计算层面还是存储层面,因为处理方式完全不同。用top或pidstat观察的时候,重点看几个指标:CPU 使用率:用户态 + 内核态是否接近 100%。iowait:CPU 等待 IO 完成的时间占比。iowait 高但 CPU 使用率不高,是典型的磁盘瓶颈特征。负载(load average):这个要结合 CPU 核数看。核数为 16 时负载 15 不算异常,核数为 4 时负载 15 就是严重过载。判断逻辑很简单:如果是 CPU 使用率持续 95% 以上,那基本是 SQL 计算量太大,比如大量排序、聚合、全表扫描;如果是 iowait 飙到 20% 以上而 CPU 空闲,那大多是磁盘读写跟不上,要么是临时表落盘,要么是慢 SQL 触发大量随机读,要么是日志刷盘太频繁。我处理过一个典型的 IO 打满案例:某业务库的监控显示 iowait 长期 30%,CPU 却只有 20%。一开始以为是慢 SQL,但慢日志里没什么重量级选手。后来iostat一看,磁盘读 IOPS 逼近万级,再用strace和perf定位,发现是一个后台统计任务在循环扫一张历史大表,每次扫描都触发几千次随机 IO。后来把统计任务改成走索引、分批处理,iowait 立刻掉到 5% 以下。这个案例说明:不区分 CPU 和 IO 瓶颈,方向错了,再怎么优化都是白费力气。4.2 内存与缓冲池命中率的关系数据库内存这块,最重要的不是系统可用内存,而是数据库自己的缓冲池命中率。以 MySQL 的 InnoDB 为例,缓冲池(innodb_buffer_pool_size)负责缓存数据页和索引页,如果命中率高,大部分读操作直接走内存,不会触碰磁盘。查看命中率:SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%'; -- 命中率 = (Innodb_buffer_pool_read_requests - Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests * 100%如果命中率长期低于 95%,说明缓冲池偏小或者查询扫描的数据量过大。前者可以尝试调大innodb_buffer_pool_size,我一般建议设置为物理内存的 50%~70%,但要留足系统和应用本身所需的内存;后者就要回到慢 SQL 层面去优化了,因为无论如何加大缓冲池,也架不住天天全表扫描。顺便提醒一个新手容易犯的错误:看到内存还有剩余就以为没问题,忽略了 swap 的使用情况。如果系统开始用 swap,数据库性能会急剧下降。检查free -h里的Swap值,长期大于 0 就要警惕了。4.3 一个容易误判的案例:IO 延迟高但 CPU 很闲真实线上有一个特别容易误判的场景——磁盘 IO 延迟高,但数据库 CPU 一点都不高。现象是接口偶发超时,数据库监控看不出明显问题,SQL 也不慢,但就是隔几分钟抖一下。这种问题通常不在数据库 SQL 层,而在存储层。我排查时习惯先用iostat -x 1看磁盘的await(平均 IO 响应时间)。SSD 正常await应该在个位数毫秒以下,如果经常跳到几十毫秒甚至上百毫秒,就说明磁盘接管请求的速度已经跟不上了。接下来要分清是磁盘本身性能不够,还是邻居贡献了太多 IO。如果是云上的共享存储或虚拟机磁盘,还要考虑是否为"吵闹的邻居"导致。这个时候单纯的数据库优化已经没用了,要么扩容磁盘、要么迁移到独立盘、要么把大查询错峰跑,才能把延迟压下来。除此之外,数据库日志写入(如 binlog、redo log)也经常是 IO 瓶颈点。遇到log相关文件所在磁盘 IO 高,可以检查是不是innodb_flush_log_at_trx_commit或sync_binlog设置过严。这两个参数都设为 1 是最安全的,但每次事务提交都要同步刷盘,性能开销非常大。如果业务允许牺牲极小的数据丢失风险,可以调整为2或0来换取大幅性能提升。不过这类参数涉及数据安全,改动前一定要拉着 DBA 和数据负责人一起评估,不能自己拍板。5. 第四站:锁等待与阻塞,数据库“卡住”和“变慢”是两回事5.1 锁等待出现时数据库的真实表现有一种典型的故障现象:数据库 CPU 不高、磁盘也不忙、慢日志里没有明显的慢 SQL,但业务就是卡——具体表现是某个或某几个 SQL 一直处于Waiting状态,后面的请求全部排队堆积。这种情况十有八九是锁等待问题。锁等待的本质是事务并发冲突:一个事务持有了某行数据的锁还没释放,另一个事务也想更新同一行,只能原地等待。如果持有锁的事务迟迟不提交也不回滚,等待的事务就会越积越多,最后把连接池和资源全部耗光。我见过最夸张的一次,是一张订单表只有一行数据被某个开发人员手动SELECT FOR UPDATE锁住,后来忘了事务没提交,结果线上所有涉及该订单的更新全部堵死,应用侧积压了几千个线程。定位命令(MySQL 8.0 及以上推荐用performance_schema):-- 查看当前有哪些锁等待 SELECT * FROM performance_schema.data_lock_waits; SELECT * FROM performance_schema.data_locks; -- 经典 InnoDB 状态(含最近死锁信息) SHOW ENGINE INNODB STATUS;再看SHOW PROCESSLIST里的State字段。如果大量线程显示Waiting for table metadata lock、Updating、Locked,基本可以确认锁问题就在眼前。这时候要第一时间找到那个"老赖"事务,用它的线程 ID 去information_schema.innodb_trx里查它已经运行了多久、处于什么状态,确认是业务代码忘了提交还是确实在跑超长事务。5.2 死锁和长时间锁等待要怎么区分死锁和长时间锁等待症状类似,但处理方式完全不同。死锁是两方互相持有对方需要的锁,谁都不让谁。MySQL 的死锁检测机制会在检测到死锁时主动回滚其中一方,应用通常会收到Deadlock found when trying to get lock错误。这种情况虽然影响单个请求,但通常不会导致大面积故障。看到SHOW ENGINE INNODB STATUS里有LATEST DETECTED DEADLOCK段落时,重点看事务 SQL 和持有索引,调整事务的执行顺序或索引选择就可以规避。长时间锁等待则更隐蔽,它本质上是"锁合法持有者迟迟不放"。处理方式不是等待数据库自己处理,而是果断介入——找到持有锁的事务,确认业务情况后KILL掉对应线程,先把业务恢复,再从代码层排查为什么事务没及时提交。所以看到锁相关现象时,第一件事永远是:搞清楚谁持锁、持了多久、该不该 KILL。5.3 业务侧要怎么配合减少锁冲突锁问题往往不是数据库能单独解决的,更多要从业务侧设计来规避。我总结三条实际有用的建议:事务要短小精悍:一个事务里尽量只包含必要的 SQL,不要在事务里做远程调用、外部接口等待。事务时间越长,持锁时间越长,冲突概率越高。同一批数据按固定顺序更新:多个事务如果都按同样的顺序(比如先更新字典表再更新业务表)去操作,死锁概率会小很多。索引设计要覆盖访问路径:更新操作如果没走索引,会从前锁升级为锁更多行甚至锁表,这个问题优化索引往往比改代码更立竿见影。6. 第五站:主从复制与网络延迟,别忘了数据库外面的事6.1 主从延迟要怎么查、怎么定位很多场景下业务感受到的"数据库慢",其实是主从架构的延迟问题。主库写完数据要异步复制到从库,如果从库的复制赶不上主库的写入速度,从库提供的数据就会过期,或者读从库的请求在等待新数据时时灵时不灵。查看主从状态的核心指标是Seconds_Behind_Master(MySQL 5.7 及以下)或Seconds_Behind_Source(MySQL 8.0 及以上):SHOW SLAVE STATUS\G这个字段越大说明主从延迟越严重。但必须说一个坑:Seconds_Behind_Master只有在 SQL 线程正在执行时才更新,如果复制因为某种原因卡住,它反而可能显示为 0,误以为延迟不存在。所以不能只看这一个字段,还要配合看Relay_Log_File、Relay_Log_Pos、Slave_IO_Running、Slave_SQL_Running这几个值,确认 IO 线程和 SQL 线程都是Yes且中继日志在推进。主从延迟常见的根因有:大事务在主库执行太久(比如一次批量更新几十万行)、从库磁盘性能太差、从库上跑了重分析任务抢资源、DDL 执行造成复制阻塞。碰到延迟先看有没有明显的大事务,把大事务拆小或放到业务低谷执行,往往能解决一大半问题。6.2 网络抖动怎么和数据库自身问题区分最后这个检查点,也是容易被忽略的:数据库本身完全没问题,但应用就是觉得"数据库慢"。这种情况通常是网络链路抖动、连接被防火墙或负载均衡干扰、TCP 重传率上升导致的。判断方法其实不难。在应用服务器上执行:ping 数据库地址 mtr -n 数据库地址如果看到丢包率大于 0 或者延迟明显升高,那么问题就不在数据库而在网络。再更深一步,用ss -i查看 TCP 连接的重传率,retrans比例超过 0.2% 就要高度怀疑网络质量了。还有一种典型的场景是应用和数据库之间的连接没有做健康检查,网络闪断后 TCP 连接处于半开状态,应用不知道连接已失效,每次请求都等超时才报错——这种问题表面上是"数据库慢",实际上要从连接池的空闲连接检测参数去调。[!提醒] 排查到这里如果还是找不到原因,就大胆回头看看是不是真的"数据库"慢。有些慢是应用层的问题,比如序列化耗时、GC 停顿、代码写递归,根本和数据库无关。不要被"数据库慢"四个字套住了思路。7. 线上实战:一次“数据库慢”的完整排查记录7.1 现场情况还原某天晚上 9 点,某个电商模块的核心接口开始告警,P99 延迟从 200ms 涨到 3 秒,错误率飙升。第一波告警信息显示:数据库 CPU 90%+,活动连接数翻了三倍,主库负载明显上涨。接到告警后我没有急着去调配置,而是按上面的 5 个检查点逐步过了一遍。7.2 逐步排查过程第一步,先看连接数。Threads_connected从平时的 100 涨到 400,Threads_running一直在 60 以上,明显不对劲。400 个连接里大部分都在执行同一条语句模板,立刻意识到是某条 SQL 出了问题。第二步,翻慢查询日志。用pt-query-digest聚合最近 30 分钟的慢日志,排名第一的 SQL 是某个商品列表页的分页查询,单条执行 2 秒多,一小时执行了几万次,累计耗时巨大。这条 SQL 平时也不快,但数据量涨到一定程度后终于爆发了。第三步,看执行计划。EXPLAIN发现这条 SQL 里关联了商品主表和库存表,库存表的关联字段sku_id没走索引,rows预估扫了 30 万行,还带了Using temporary。难怪数据库 CPU 会打满——每次查询都在做全表关联和临时表排序,量一大直接压垮 CPU。第四步,把 CPU 飙高的"果"和这条 SQL 的"因"对上之后,先做应急处理:把这条 SQL 对应的接口开启了限流和降级,同时将慢查询时间阈值临时从 1 秒调到 2 秒,避免慢日志记录量过大加剧磁盘压力。然后让 DBA 给库存表的sku_id字段补上索引,并在 SQL 层面去掉不必要的排序字段。7.3 复盘与总结补了索引之后,这条 SQL 的执行时间从 2 秒降到 80ms,数据库 CPU 回落到 20%,接口延迟也恢复了。整个排查过程从接到告警到恢复业务用了大概 40 分钟。复盘的时候最想强调的一个点:这次故障如果一开始就盲目调大max_connections,或者直接重启数据库,结果是能短暂恢复,但下一次流量高峰还会以同样方式炸掉。真正解决问题的是把"打满连接的 SQL"挖出来并优化掉。另外这次还暴露出监控层面的问题——之前只截图了数据库 CPU 和连接数,没有对慢查询做聚合告警,导致 SQL 隐患长期潜伏。事后我推动把核心库的慢查询聚合指标接到了监控平台,设置按 SQL 模板维度的告警,后来类似的隐患提前暴露了不少次,这个投入非常值。8. 写在最后:几条血泪教训和排查建议8.1 我最容易误判的三种情况经验是踩坑换来的。我把自己最容易误判的三种情况列出来,希望大家少走弯路:一是连接数打满的时候只想到调参数。前面说过很多次,连接数是结果不是原因,真正要处理的是背后那批不健康的 SQL 或者连接泄漏。调参数只是止血,根因不除下次照炸。二是把锁等待当成慢 SQL。慢 SQL 是执行效率低,锁等待是资源被人占住。方向错了会浪费大量时间,而且处理方式完全不同。遇到数据库卡死第一时间看SHOW PROCESSLIST的State,别急着分析执行计划。三是只盯着数据库内部指标,忽略存储和网络。尤其到了云环境,磁盘性能、网络链路的问题占比更高。数据库各项指标都正常但业务就是慢的时候,大胆往外面看。8.2 给新手的几条建议如果你刚开始接触这类问题,我建议你有空就做三件事:一个是把上面 5 个检查点的常用命令整理成自己的排查手册,反复练习形成条件反射;二是把数据库的慢查询日志、连接数、CPU、锁等待、主从延迟这几个关键指标都接入监控并配上告警,做到问题一冒头你就知道;三是定期做一次慢 SQL 巡检,不要等故障来了才想起看慢日志。最后再分享一个我个人的习惯:每一次线上故障处理完,我都会把现象、排查过程、根因、修复措施写成一篇简短的复盘记录,存到团队的知识库里。下次再遇到类似问题,先查知识库,五分钟就能定位,远比从头开始猜快得多。这套方法坚持下来,处理数据库慢问题的速度和准确率会有非常明显的提升。