Java

数据库 RT 抬高但慢 SQL 不明显,该从锁、线程还是 I/O 起手?

数据库 RT 明显抬高,慢日志却不扎眼时,现场最容易卡在“SQL 看起来都还行”。这时真正要拆的往往不是某一条语句,而是 RT 里混进了多少锁等待、事务停留、连接排队和 I/O 波动。

  • MySQL
  • 数据库RT
  • 锁等待
  • 长事务
  • 性能排查
18 分钟阅读

线上经常会碰到一种很别扭的现场:应用侧数据库调用 RT 明显抬高,接口 RT、超时率、连接池压力也在变差,但慢日志里又看不到特别扎眼的大慢 SQL。难点往往不在于找不到 SQL,而在于 RT 里混进了多少锁等待、事务停留、连接排队和 I/O 波动。数据库 RT 变高,并不等于 SQL 执行本身一定变慢。

我更习惯先把 RT 拆开,看主要是哪一段被拉长了,再决定后面该往锁和事务、线程和连接,还是 I/O 继续查。

RT 先卡在哪一段

这个阶段最重要的,不是马上翻磁盘图或急着排掉数据库,而是判断 RT 里到底哪一段被拉长了。

你现在看到的现象当前更该落哪一层为什么
数据库调用 RT 抬高,但慢日志并不扎眼把 RT 拆开来看这正是“数据库 RT 在升,但慢点还没收窄”的现场
已经看到热点行、锁等待或长事务很明显直接转去锁等待或长事务那两篇这时没必要继续停在 RT 这一层
获取连接先变慢,连接池已经开始变紧回头看连接池等待和连接耗尽先分数据库慢和应用持连接过久
你想沉淀值班固定看板,而不是定位一次事故去看监控面板那篇那篇更偏治理和长期建设

把 RT 里的时间拆开

如果你已经确认数据库调用 RT 在升,但慢 SQL 还不足以解释现场,重点就不是再追一批“更慢的 SQL”,而是把问题拆成 执行时间、等待时间和排队时间 三段,再决定后面该落到锁等待、长事务、连接池,还是 I/O。

现场已经确认问题落在数据库这一层,但慢点还没收窄时,就按这个顺序拆。

先抓这 5 个判断点

  1. 把数据库调用 RT、接口 RT、获取连接耗时放到同一时间窗里对齐。
  2. 分清数据库 RT 里主要抬高的是执行、等待,还是排队时间。
  3. 如果写接口、高峰期、热点业务键更敏感,优先怀疑锁等待和长事务。
  4. 如果慢日志不突出,但连接池和线程池一起变差,再把应用排队时间一起拉进来。
  5. 最后才决定要不要把 I/O 放到第一优先级,不要一看到 RT 高就先翻磁盘图。

一、别把数据库 RT 直接理解成“SQL 执行时间”

很多监控里的数据库 RT,实际混着下面几段时间:

  • 获取连接前的等待
  • SQL 在数据库里的排队与锁等待
  • SQL 真正执行时间
  • 结果返回与应用处理时间

所以看到数据库 RT 抬高,第一步不是直接下结论“SQL 变慢了”,而是把它拆开。

1. 执行时间真的在变慢时,现场通常会这样

通常会看到:

  • 某几类 SQL 持续慢,平峰也慢
  • 慢日志里能稳定看到同一批语句
  • explain、rows、排序、回表都有比较明确的问题
  • 数据量增长和 RT 变差高度一致

2. 等待时间在拉长时,现场通常会这样

通常会看到:

  • 同一条 SQL 有时几十毫秒,有时几秒
  • 高峰期才明显变差
  • 写接口或混合读写接口受影响更重
  • 数据库 CPU 没完全打满,但接口和连接池已经很难受
  • 慢日志数量不算夸张,数据库 RT 却整体抬高

如果更像第二类,就别先死磕慢 SQL 文本了。

二、把影响面看清:是数据库整体慢,还是某一段等待先变长

排这类问题,第一步建议先回答下面几个问题:

  • 是所有依赖数据库的接口都慢,还是只有写接口慢
  • 是单实例更明显,还是整组服务一起慢
  • 是数据库调用 RT 先抬高,还是连接池等待先抬高
  • 是数据库 CPU、I/O、活跃事务一起变差,还是只有 RT 曲线先难看

1. 写接口明显更差

更要优先怀疑:

  • 锁等待
  • 热点行竞争
  • 长事务不提交
  • 批任务与在线事务撞车

2. 读写都一起变差

更要优先怀疑:

  • 实例资源争抢
  • 缓存回源把数据库整体压高
  • 连接池等待和线程排队已经放大到全链路

3. 数据库 CPU 不高,但应用普遍觉得数据库慢

这类场景特别典型,往往说明:

  • 不是数据库“算不过来”
  • 而是数据库或应用一侧的等待已经足够长

为什么我通常先查锁和事务,再看磁盘

在“数据库 RT 高但慢 SQL 不明显”的现场里,我通常会把锁等待和长事务放到 I/O 前面查。

原因很简单:锁和事务最容易制造“数据库慢了,但慢日志没几条特别扎眼”的错觉。

1. 锁等待为什么容易被低估

锁等待场景里,慢的通常不是 SQL 计划本身,而是:

  • 在等别的事务提交
  • 在等热点行释放锁
  • 在等批量更新结束

这时你会看到:

  • explain 看起来还行
  • 数据库 CPU 没离谱地高
  • 但事务时间、接口 RT、连接池等待都在变差

2. 长事务为什么是放大器

长事务会同时放大两件事:

  • 锁持有时间
  • 连接持有时间

于是后面的请求会逐渐表现成:

  • 写 SQL 开始排队
  • 获取连接变慢
  • 接口 RT 继续升高

这也是为什么这类问题经常要和《事务执行时间过长,真正拖慢系统的往往不只是数据库》一起看。

四、再查线程和连接池:很多“数据库 RT 高”其实已经混入应用排队时间

有一类很常见的误判是:

  • 只要监控上写着“数据库 RT”
  • 就默认这全是数据库里的时间

但真实线上里,应用经常会把下面这些等待一起感知成“数据库阶段变慢”:

  • 获取连接时等待
  • 线程排队后才真正开始执行数据库调用
  • 事务里被下游调用拖长

1. 更像连接池等待的信号

  • 活跃连接长期接近上限
  • 空闲连接接近 0
  • 获取连接耗时和数据库 RT 一起上升
  • 应用里开始报 timeout waiting for connection

如果这些信号明显,应该继续看《数据库连接池打满时,根因通常不是连接数太小》。因为这时监控里看到的“数据库慢”,很多已经掺了“连接拿得晚、归还得慢”的时间。

2. 更像线程堆积的信号

  • 线程池活跃线程持续高位
  • 队列上涨
  • 应用吞吐下降,但数据库资源曲线不算夸张
  • 上游请求线程也开始排队

这时要意识到:数据库 RT 高,可能已经是应用线程在等待数据库、数据库连接在等待释放,两边一起拖长的结果。

五、I/O 什么时候该提到前面查

不是说 I/O 不重要,而是下面几种信号一旦明显,就该把它提到前面查:

  • 数据库实例读写 IOPS、await、util 同时上升
  • Buffer Pool 命中率变差
  • 临时表、排序落盘、批量导入导出任务正在跑
  • 报表、备份、归档或扫描型任务和在线流量撞车

1. I/O 抖动更像什么现场

通常会看到:

  • 读写都一起慢
  • 某些平时很稳的查询突然整体变差
  • 并不是少数热点行问题,而是整批查询都受影响
  • 数据库层面的 RT 和磁盘等待曲线对齐

2. 为什么 I/O 不该默认排第一

因为在很多互联网业务现场里,锁等待和事务边界问题的发生频率,往往高于“纯磁盘突然不行”。

如果一开始就把注意力全放在 I/O,很容易错过真正更高频的等待源:

  • 热点写冲突
  • 长事务
  • 连接池堆积
  • 线程排队

六、排查顺序通常是:锁和事务 -> 线程和连接 -> I/O

遇到“数据库 RT 高,但慢日志不显眼”的现场,我更建议按这个顺序排。

第 1 步:把数据库 RT 里混着的时间拆开

重点看:

  • 获取连接耗时
  • SQL 执行耗时
  • 事务总耗时
  • 接口总 RT

目标是先判断:

  • 慢在数据库执行
  • 还是慢在连接等待、事务等待、应用排队

第 2 步:重点核对锁等待、活跃事务和长事务

重点看:

  • 锁等待数量和耗时
  • 活跃事务数
  • 长事务数量
  • 某些热点表、热点行是不是在高峰期冲突明显

第 3 步:再查连接池和线程池

重点看:

  • 活跃连接数、空闲连接数、等待线程数
  • 获取连接耗时
  • 工作线程和业务线程池是否持续高位
  • 是否有请求排队把数据库阶段整体拖长

第 4 步:再看实例资源和 I/O

重点看:

  • CPU、IOPS、await、util
  • 同时间窗口是否有报表、备份、批处理
  • Buffer Pool、临时表、落盘排序是否异常

第 5 步:最后再回看 SQL 本身是不是还有静态问题

比如:

  • explain 正常但 rows 仍然偏大
  • 深分页、排序、回表成本在数据量上来后开始变脆
  • 单条 SQL 虽不进慢日志,但总体次数过多

这一步可以再和《explain 看起来没问题,SQL 还是很慢,接下来该查什么?》串起来看。

七、一个典型案例:数据库没打满,为什么 RT 还是被整体抬高

假设某个订单写链路高峰期开始变慢。

现象

  • 应用里数据库调用 RT 从 40ms 抬到 400ms 以上
  • 慢日志并不夸张
  • 数据库 CPU 在 60% 左右
  • 连接池开始紧张

第一眼看数据库

很多人会困惑:

  • CPU 没打满
  • 慢日志也不多
  • 为什么大家都觉得数据库慢

再往里拆

发现:

  • 订单状态表上有热点行更新
  • 某个事务里还夹了下游券服务校验
  • 前面的事务提交变慢,导致后面的写请求逐渐排队
  • 连接池归还速度下降,获取连接耗时被一起放大

结论

根因不是纯 I/O,也不是某条巨慢 SQL,而是:

热点写冲突 + 事务边界过大 -> 锁和连接都被拿得更久 -> 应用统一感知成数据库 RT 抬高

这个案例很能说明一件事:数据库 RT 升高,很多时候不是“数据库跑得慢”,而是“数据库等待链被拉长了”。

八、关键误判:这类问题最容易在哪些地方走偏

误判 1:慢日志不明显,就排除数据库方向

错。

锁等待、长事务、连接等待,都可能让数据库 RT 很难看,但慢日志并不一定特别显眼。

误判 2:数据库 CPU 不高,就说明瓶颈不在数据库

错。

等待型瓶颈经常发生在 CPU 还没满的时候。

误判 3:数据库 RT 高,就一定先查磁盘 I/O

不一定。

很多时候更先该查的是锁、事务、连接和线程。

误判 4:只看 SQL 执行,不看连接获取和事务总时长

这会把“等待混在数据库 RT 里”的问题直接漏掉。

误判 5:一看到连接池紧张,就直接扩连接数

如果根因是锁等待或长事务,扩池子只会让更多请求更快撞进同一段堵点。

九、现场最常被问到的几个问题

1. 这类问题更常从锁还是 I/O 开始?

默认更建议把锁等待和长事务放在前面,再看连接池、线程池,最后再判断 I/O 是否是第一现场。因为锁和事务更高频,也更容易制造“RT 高但慢日志不明显”的现场。

2. 数据库 RT 高,但 CPU 和慢日志都正常,最该怀疑什么?

优先怀疑:

  • 锁等待
  • 长事务
  • 连接池等待
  • 应用线程排队

3. 连接池等待会不会被误看成数据库慢?

会,而且很常见。应用往往把“拿连接慢”和“SQL 变慢”一起感知成数据库调用 RT 上升。

4. I/O 抖动有什么比较明显的信号?

通常会伴随:

  • 多类查询一起变慢
  • await、util、IOPS 异常
  • 落盘排序、临时表、批量任务同时明显
  • 数据库层曲线和磁盘层曲线高度对齐

证据已经收窄时,直接接对应专题

证据已经开始收窄时,直接顺着下面对应专题往下查会更省时间。

同一组数据库线索

如果现场还牵着应用侧

我会怎么往下收

  1. 写接口、高峰期、热点业务键更明显,就去看 锁等待、热点行、事务竞争把接口拖慢时,应该先查什么?
  2. 已经看到事务时间拉长,就去看 事务执行时间过长,真正拖慢系统的往往不只是数据库
  3. 应用开始报拿连接慢,再接 数据库连接池打满时,根因通常不是连接数太小
  4. 锁和事务都不突出,再去看 explain 看起来没问题,SQL 还是很慢,接下来该查什么? 和实例资源、I/O 方向。

回到最重要的判断:是哪一段等待先变长

这类问题最容易把人带偏的地方就在于:

  • 看到数据库 RT 高,就直接去翻慢日志
  • 慢日志不明显,又立刻去怀疑磁盘或网络
  • 结果锁等待、长事务、连接池排队和线程堆积都被拖了一圈才看见

我自己会按这条线往下收:

先把数据库 RT 里的时间拆开,再查锁和事务,再查连接池和线程,最后再决定 I/O 是不是主因。

把“是哪一段等待变长了”问清以后,很多看起来像“数据库整体变慢了”的模糊问题,最后都会落到一段更具体、更可验证的瓶颈上。