周五晚高峰业务群里开始有人反馈下单超时紧接着监控大盘上数据库连接数拉满慢查询数从平时的几十条直接飙到几千条CPU使用率顶着99%不回头。你盯着屏幕脑子里只有一个问题为什么数据库一到高峰期就“扛不住”这篇文章想把这件事彻底聊明白。我会从过去处理过的数据库故障经验出发不绕弯子直接拆“连接、SQL、锁、系统资源”四个层面看看故障是怎么一步一步被放大的最后给你一张高峰期前后能直接照做的排查清单。不管你是写业务的开发还是全职DBA这套思路都能帮你少熬几个夜。1. 先弄清楚“扛不住”到底是什么信号1.1 三种最常见的故障表现先别急着改参数第一步是把“扛不住”这个含糊的说法拆成可观测的信号。我见过太多团队一出现故障就说“数据库不行了”结果重启之后第二天高峰又来一次。把现象拆细了排查方向才清晰。通常数据库在高峰期会呈现三种典型表现。第一种是连接层面应用日志里频繁出现“too many connections”或者获取连接超时说明连接数被耗尽了。第二种是响应时间层面同样一条SQL平时几毫秒高峰时变成几百毫秒甚至几秒应用侧平均RT明显抬高。第三种是锁和事务层面监控里的锁等待曲线持续上升偶尔还能看到死锁报错事务提交的时间越来越长。这三种表现经常同时出现但它们的根因并不一样后续排查的入口也不同。更关键的是别只看数据库自身指标要同步看应用侧的报错和业务指标。有一次我们排查一个订单系统数据库CPU确实很高但真正的问题是一台应用节点内存溢出导致频繁GCGC又把请求全压到另一台节点上数据库才被动打满。所以数据库“扛不住”经常是“结果”而不是“原因”只有把前后端指标放在一起看才能避免误判。1.2 从症状对应可能的原因下面这张表是我经常拿来帮团队快速收敛问题的现象到原因的对应关系不一定100%精确但能帮你在第一时间找到大方向。症状常见原因优先检查方向连接数打满连接池配置过大、慢SQL占用连接、阻塞事务不释放活跃会话数、innodb_trx响应时间变长缺索引、执行计划退化、锁等待慢查询日志、EXPLAINCPU飙升全表扫描、排序过多、连接数过多processlist、执行计划IO等待高缓冲池命中率低、临时表落盘、冷数据读取多命中率指标、磁盘IO锁等待增加长事务、事务中做外部调用、批量更新冲突innodb_lock_waits我自己排查时的习惯是先看活跃会话数再抓Top SQL最后才看硬件资源。因为SQL和会话是最接近业务行为的信号硬件指标往往是它们的结果。顺着这个顺序走大概率能在10分钟内锁定一个大致范围。2. 连接池爆满最常见的第一个瓶颈2.1 为什么连接数会先炸掉数据库连接不是免费的每一个连接背后都要分配内存和线程资源。以MySQL为例每个连接有会话内存、排序缓冲区、临时表空间等虽然很多是按需分配但几百上千个连接同时在线内存开销非常可观。连接数的上限由max_connections控制到了上限后新请求只能排队等待表现就是应用拿不到连接报错一片。更麻烦的是连接数打满之后会产生“反向放大”。所有应用线程都卡在获取连接上线程池被占满新的业务请求进不来健康检查也可能失败负载均衡就会把流量转发到其他节点其他节点也迅速被拖垮。这个连锁反应一旦启动单靠数据库本身已经无力回天必须在应用层先切断流量。所以要记住一句话连接数不是越大越好。如果你不知道合理值宁可保守一点也不要盲目调到几千。我见过一个系统把连接池最大连接数设成500数据库实例max_connections也是500平时没事一到高峰线程上下文切换就把CPU耗掉一大截反而是把连接数降到120之后CPU和响应时间双降。2.2 连接池大小到底怎么定连接池大小没有万能公式但可以从需求反推。假设你的数据库要支撑高峰期QPS为500平均每条SQL响应时间是80毫秒那么在某一时刻同时在数据库里执行的请求数约为500乘以0.08等于40。这40是理论并发压力再留30%到50%的缓冲连接池设在60左右是合理的。把逻辑写成公式合理连接数约等于QPS乘以平均RT秒再乘以1.3到1.5的缓冲系数。反过来也能用如果连接池一直处于打满状态说明要么QPS超过设计容量要么SQL变慢导致RT上升这时候加连接数是治标抓慢SQL才是治本。我见过太多团队在高峰前只会无脑调大连接池结果数据库连接满了、内存也满了问题依旧。另外要注意连接池的最小空闲连接和超时时间。最小空闲连接不要设成0否则流量一上来要现建连接建连过程在高并发下非常耗时但也不要设太大空置的连接浪费内存。获取连接超时时间建议根据业务容忍度设置一般3000毫秒左右比较常见太短容易误报太长会让请求堆积。2.3 和线程池怎么配合才合理应用侧的线程池和数据库连接池是一对“上下级”必须配合着看。如果应用线程池是200线程数据库连接池只有20那么大部分线程会阻塞在等连接上线程被白白占满如果反过来连接池很大但线程池很小连接再多也没人用白白浪费数据库资源。理解了这个关系你就知道为什么排查时要同时看两边的线程状态。我常用一个简单的匹配原则线程池大小可以略大于连接池比如线程数是连接数的1.5到2倍让线程有机会快速从连接池获取连接而不是长时间排队。同时数据库连接池等待获取连接的时长和应用线程池的拒绝策略要配置成合理的组合避免请求在应用层无限堆积。这个细节在高峰期最容易忽略等故障出现了才去对线程栈那就晚了。3. 慢SQL高峰期最危险的放大因子3.1 一条慢SQL如何拖垮整个库慢SQL是高峰期“扛不住”的头号放大器。平时数据库负载低一条执行500毫秒的SQL没什么感觉到了高峰期同样的SQL因为要扫描的数据页更多、CPU争抢更激烈可能变成5秒。这条SQL占住一个连接连接被占其他请求就在连接池排队排队又导致RT上升最终让整个库的吞吐量断崖式下降。最典型的案例是查询条件里只用了非索引列或者用了函数包裹索引列。某个电商订单表曾经有个查询筛选某个用户的待支付订单按创建时间倒序取最近20条。初期数据量小扫描全表也就几十毫秒数据涨到千万级后这条SQL在高峰期要扫几百万行然后把结果集排序单条SQL稳定超过2秒。这就是典型的“数据量上来了但索引没跟上”的坑。3.2 15分钟定位慢SQL的实操路径遇到疑似慢SQL按下面这个路径走基本不会跑偏。第一步是打开慢查询日志并设置合理的阈值。阈值建议生产环境先设1秒压力大的核心库可以设0.5秒。注意阈值不要设太高否则高峰期你什么都抓不到也别长期关闭采样否则事后根本无从复盘。第二步是从数据库的系统表里直接检索Top SQL。以MySQL为例可以查询performance_schema里的events_statements_summary_by_digest表按平均耗时排序几秒钟就能看到哪些SQL累计消耗最大。SELECT SCHEMA_NAME, DIGEST_TEXT, COUNT_STAR, AVG_TIMER_WAIT / 1e12 AS avg_ms FROM performance_schema.events_statements_summary_by_digest ORDER BY AVG_TIMER_WAIT DESC LIMIT 10;第三步是对疑点SQL执行EXPLAIN重点看type字段如果是ALL说明是全表扫描同时看rows估算行数和实际扫描行数对比能判断统计信息是否过期。就拿前面的订单查询来说优化前EXPLAIN显示typeALLrows约300万Extra里还有Using filesort增加一个(user_id, status, create_time)的联合索引后typerefrows降到几百条Extra里不再有filesort单条SQL从2.1秒降到了20毫秒左右。这个效果不是靠调数据库参数能换来的索引设计对慢查询的收益是数量级的。3.3 索引失效的高发场景速查索引不是建了就一定生效。以下几个场景我几乎每年都会遇到整理成速查表方便你对照判断。场景原因解决思路where phone 13800138000 而字段是varchar隐式类型转换应用侧传入字符串或建合适索引对索引列使用函数例如 DATE(create_time) ...函数让索引失效改写成范围条件LIKE %关键词前导模糊匹配考虑全文索引或搜索引擎联合索引没按最左前缀用不符合最左匹配原则调整查询列顺序或重建索引OR两侧只有一侧有索引优化器可能放弃索引拆成多个查询或走索引合并另外统计信息过期也很容易让优化器走错执行计划。对于变化剧烈的表定期执行ANALYZE TABLE或者开启自动统计信息更新能减少很多“没道理的全表扫描”。还有一点小经验不要看到慢SQL就加索引先用EXPLAIN确认是不是真缺索引建完之后再EXPLAIN对比一下rows变化确认有效再上线。4. 锁等待和隐式事务看不见的隐形雷区4.1 行锁和间隙锁在高峰期的表现并发一高锁的问题就藏不住了。InnoDB的行锁本质上是锁在索引记录上的当大量事务同时更新同一行或者同一批记录后到的事务只能等前一个事务提交或回滚。高峰期表现最明显的就是更新某条订单状态的接口平时1毫秒突然变成1秒而且连接数同步上升因为所有线程都挂在锁等待上。间隙锁和临键锁更麻烦。在RR隔离级别下为了防止幻读范围查询可能会锁住一个区间如果两个事务以不同顺序插入、更新相同间隙的数据就可能互相等待形成死锁。排查死锁时重点看错误日志里的死锁检测信息InnoDB会打印出两个事务各持有哪些锁、等待哪些锁把相关SQL揪出来后通常的解决方法是缩小事务范围或者让数据访问按统一顺序进行。4.2 一个真实事故事务里调用外部接口这是我最想强调的一个坑。某个积分系统的每日结算任务伪代码大致是开启事务更新一批积分账户调用外部通知接口再提交事务。外部接口平均耗时2秒高峰期甚至5秒。这个事务在这2到5秒里持有大量账户记录的行锁相当于把所有相关账户的更新入口全部堵死。很快业务侧另一个批量更新任务开始排队锁等待上升到几百秒应用线程耗尽连接池被打满整个系统接近不可用。排查时通过innodb_trx找到了那个长时间未提交的事务发现它在等外部响应这才定位到问题。修法也很直接把外部接口调用移出事务或者干脆在事务提交后再异步通知同时控制单个事务处理的行数从一次处理几万行改成几千行锁的粒度就小了很多。改完之后同场景的锁等待从几秒降到了几十毫秒。所以写事务代码时一定要有意识事务越短越好锁范围越小越好能放进事务的外部调用越少越好。4.3 锁排查常用的几个查询当锁等待已经出现你可以直接查询这些视图快速找到“元凶”。-- 查看所有未提交事务 SELECT trx_id, trx_state, trx_started, trx_rows_locked FROM information_schema.innodb_trx\G -- 查看当前谁在等锁、谁占着锁 SELECT * FROM sys.innodb_lock_waits\Gsys.innodb_lock_waits这个视图比较好用它把事务和锁等待的信息做了关联直接显示阻塞方和被阻塞方的线程ID以及SQL文本。常见操作是先找到阻塞源的trx_id再回innodb_trx找到对应连接kill连接或者让它自己回滚。但kill操作要谨慎如果被kill的是一个已经跑了很久的大事务回滚也需要时间期间可能还会加重负载。所以不到万不得已优先考虑用应用配置和事务设计来消除锁等待而不是依靠应急kill。5. 系统资源和配置的隐藏瓶颈5.1 CPU飙升时先看这三个方向CPU打满并不一定都是慢SQL。我见过的CPU飙升至少有三种方向。第一种是逻辑读过高也就是SQL扫描的数据块太多即使全部命中内存SQL层也要花大量CPU去逐行判断这时候EXPLAIN里的rows和扫描行数会给出答案。第二种是排序和临时表过多比如大面积使用ORDER BY、GROUP BY、DISTINCT而且排序数据超过内存阈值还要落到磁盘CPU和IO都会涨。第三种是线程调度开销连接数过大导致频繁上下文切换这种通常是连接池配置引发的看线程数曲线就能识别。遇到CPU飙升我的操作顺序是先show processlist抓当前正在跑的SQL再看CPU是持续高还是波动高最后结合EXPLAIN判断是哪种类型。很多时候一个本来只跑200毫秒的SQL在CPU紧张时会放大到2秒所以CPU飙升会连带制造一批“假慢SQL”——它们不是自身慢而是在抢不到CPU资源。5.2 磁盘IO和缓冲池命中率怎么判断数据库不可能把所有数据都放在内存里磁盘IO是绕不开的。当大量查询反复读取未缓存的冷数据或者排序、临时表落盘磁盘的IO等待就会明显升高。先看缓冲池命中率如果长期低于99%说明内存利用率不够SQL经常在扫磁盘页。提高命中率一方面可以调整缓冲区大小另一方面要思考为什么频繁读取冷数据是不是扫描太宽或缓存淘汰太严重。临时表落盘是高并发下的隐形杀手。用EXPLAIN看Extra如果出现Using temporary就要警惕排序或去重是否真的需要必要时可以在临时表容量配置上做调整但更好的办法是优化SQL避免大数据量的临时结果集。磁盘IO还有一个容易忽略的点备份和批量任务经常在整点时启动正好和业务高峰撞车。调度任务时要做错峰数据库日常维护和备份尽量安排在业务低谷不然数据库再强壮也扛不住内外夹击。5.3 超时与重试为什么会让雪崩更早到来应用层的超时配置和重试策略决定了数据库故障会不会迅速升级成雪崩。设想高峰期数据库RT已经升高到200毫秒而应用设置的读超时是100毫秒那么大量请求会判定超时然后发起重试。重试又带来更多数据库请求RT进一步恶化超时更多重试更多——这个循环一旦形成数据库系统会以比预期快得多的速度崩盘。我在实践中见过一个典型例子某服务的数据库连接池没有熔断超时后统一重试3次结果故障期间数据库收到的QPS从2000涨到6000最终把所有实例拖死。后来把重试改成指数退避、限制最大重试次数并增加了针对数据库的快速失败开关系统才能在故障时保住存活的服务器。记住高峰期保护数据库最有效的手段不一定是在数据库内部而是应用层的限流、降级和熔断宁可让部分请求快速失败也不要让所有请求同时卡死。6. 高峰期前后可以照做的排查清单6.1 故障发生时的应急顺序真出了问题别慌按顺序来。第一看监控大盘确认是数据库自身问题还是上游流量突然涨了把QPS、CPU、连接数、活跃会话数四张图拉出来。第二抓Top SQL直接定位是哪些查询占了大量执行时间或扫描行数。第三查锁等待看有没有长事务霸占行锁。第四如果连接数打满优先处理阻塞事务和慢SQL而不是无限扩容连接如果确认是流量打满容量再考虑限流或扩容。最后所有动作都要记录时间线和操作内容方便事后复盘。有一点很重要不要一上来就重启数据库或应用。有时重启只是把问题延后而且重启大事务回滚可能会引发更严重的故障。处理过太多“凌晨重启导致第二天高峰更惨”的案例数据库不是不能重启而是要在明确了故障原因之后再决定要不要重启。6.2 日常可以建立的三道防线第一道防线是监控和日志。慢查询日志长期开启阈值可以结合实际设置监控大盘上至少涵盖连接数、活跃会话、锁等待、CPU、磁盘IO、缓冲池命中率。第二道防线是压测和容量规划。每逢大促前用压测工具模拟高峰期流量提前找出最容易触顶的指标并估算需要的连接数和实例规格。第三道防线是代码和数据访问规范。事务代码评审时重点看有没有外部调用、有没有不必要的锁范围SQL评审重点看执行计划和索引使用避免上线后带病运行。我自己习惯在活动前跑一次检查单连接池参数、慢查询阈值、锁监控、备份任务调度、应用重试策略、熔断开关。这些看起来琐碎但每到高峰期都在默默决定系统能不能扛住。与其在故障发生时开几小时的会不如在日常花半小时把检查单过一遍。6.3 复盘时注意的三个视角故障恢复之后复盘不要只问“数据库为什么慢了”要多问三个视角的问题。容量视角流量是否超过设计值有没有提前扩容空间。代码视角大量慢SQL和长事务是不是某次发布引入的能不能快速回滚。协同视角应用、缓存、队列、数据库之间有没有形成级联放大的路径。这三个视角往往能拼出完整的故障链条避免下次换个场景再踩一次类似的坑。干数据库维护这些年我最大的感受是绝大多数“一到高峰期就扛不住”的问题都不是数据库单点问题而是流量、代码、配置和运维相互叠加后的结果。高峰期暴露出的也不只是数据库的瓶颈更是整个系统的弹性。每次处理完一个故障我都会让团队把检查和优化更新到文档里变成下一次高峰前就可以排除的雷。这套做法听起来简单但长期坚持真的能让你少熬很多夜。