前言:突如其来的“系统瘫痪”

在日常的系统维护中,很多开发者最怕听到的就是“系统卡住了”。
近日,我们的生产平台突发严重卡顿,前端请求大量超时。紧急排查服务器资源发现了一个诡异的现象:服务器 CPU 使用率仅有 16% 左右,完全没有压力,但系统就是“点不动”了。

CPU 既然没满,那瓶颈到底在哪里?通过对 Oracle 数据库的抽丝剥茧,我们不仅揪出了几个“写得极烂”的业务 SQL,更揪出了一个隐藏极深的 Oracle 底层“内鬼”。本文将完整复盘这次排障的全过程,希望能给大家在处理千万/亿级大表性能问题时提供参考。


第一阶段:案发现场,挤爆的“收费站”

第一步,我们直接连入 Oracle 数据库,查看当前活动会话的状态。结果令人震惊:系统中存在数十个甚至上百个状态为 WAITING 的会话,而它们的等待事件(Wait Event)几乎全是指向同一个:read by other session

💡 核心知识点科普:什么是 read by other session

很多初学者看到这个事件,第一反应是“是不是被别人锁表了(行锁)?”
绝对不是! 这个事件与传统的事务锁毫无关系。它的本质是“热点块争用(Hot Block Contention)”。

打个比方:数据库的数据存在磁盘的“数据块(Block)”里。

  1. 会话 A 想要读取“块 X”,发现它不在内存里,于是去硬盘读(此时会话 A 等待 db file sequential read)。
  2. 会话 B、C、D… 同时并发也需要读取“块 X”。它们发现会话 A 已经在路上搬运这个块了,为了避免重复读盘,B、C、D 就会乖乖排队,等待会话 A 搬运完成。
  3. 此时,B、C、D 的等待事件就是 read by other session

结论: 系统不是被锁卡住了,而是极高并发的相同查询,全部堵在了极少数的几个数据块上。就像高速公路没堵,但所有车非要挤同一个收费口。


第二阶段:顺藤摸瓜,锁定业务层“毒瘤 SQL”

既然是在排队,我们就得找出大家到底在排队等哪个 SQL。通过联合查询 v$sessionv$sql 以及实时的 v$sessmetric (会话级资源消耗),我们锁定了两类典型的“性能杀手”:

  1. 高频引发热块争用的 SELECT 语句
  • 一条普通的条件查询,平均执行时间 2.35 秒,但由于缺少索引,且被并发调用了 47万次
  • 它不读硬盘,但每秒在内存中产生近 50万次逻辑读(Logical Reads),极度消耗资源。
  1. 灾难级的 DELETE 语句
  • 一条清理日志的 DELETE 语句,执行时间长达 32 分钟
  • 等待事件为 db file scattered read(全表扫描物理读),每秒物理读高达上万次,直接榨干了磁盘 I/O。

临时急救方案:
对于只读的 SELECT 和非核心的 DELETE 清理任务,我们可以直接在数据库端执行 ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE; 强制掐断源头。纯查询被 Kill 绝对安全,不需要回滚。 掐断后,I/O 压力瞬间释放,平台恢复了响应。

但这只是治标。必须把这两个 SQL 甩给研发同学加索引,否则几分钟后系统依然会崩。


第三阶段:惊天逆转,隐藏在底层的终极 BOSS

你以为排查到这里就结束了吗?并没有。
在刚才的排查中,除了业务 SQL,我还发现了一个极其诡异的会话,它来自 SYS 用户,跑了两个多小时,正在疯狂进行物理全表扫描,SQL 长这样:

select substrb(dump(val,16,0,64),1,240) ep, cnt from (
  select /*+ no_expand_table(t) index_rs(t) no_parallel(t) dbms_stats ... */ 
  "RECORDSTATE" val,count(*) cnt  
  from "ABC"."ABCD" t  
  where "RECORDSTATE" is not null  
  group by "RECORDSTATE"
) order by val

这绝对不是开发写的代码。这其实是 Oracle 的后台自动维护任务(AutoTask)在收集表统计信息(Gather Statistics)。它正在对 RECORDSTATE 这一列收集直方图(Histogram)来判断数据倾斜度。

🚨 灾难是如何发生的?

这张表是一张从 2019 年至今、每年拥有亿级数据、按年分区的超级大表。
由于近期有数据变动(超过了 10% 阈值),Oracle 认为统计信息旧了,于是触发了收集。

但为什么按年分区的表,会引发如此恐怖的 I/O 风暴?
查看执行计划,我们看到了绝望的一行:PARTITION RANGE ALL
在 Oracle 的默认机制下,即使它只收集了 2026 年新分区的统计信息,为了计算这张表的全局统计信息(Global Stats),它会极其粗暴地把 2019 年到 2026 年的所有十几个分区(几十亿条历史数据)全部重新扫描一遍!

这就是拖垮整个存储 I/O,导致业务卡死的真正元凶。


第四阶段:终极解决方案 —— 开启增量统计信息

面对这种十几亿级的历史流水表/日志表,必须改变 Oracle 收集统计信息的策略。千万不能让它再去扫描历史老分区了!

救命稻草就是:增量统计信息(Incremental Statistics)。

开启增量统计后,Oracle 在收集早期分区时,会生成一个极小的数学摘要(Synopsis)。当新分区有数据写入时,Oracle 只扫描新分区,然后拿新摘要和老摘要做一个简单的数学加法,瞬间完成全局统计信息的更新。耗时从几小时骤降到几秒!

实操步骤

如果你也遇到了大分区表的统计信息收集风暴,请立刻按照以下步骤处理:

1. 紧急止血(杀掉正在疯狂读盘的收集进程)
找到对应的 SID,直接 Kill。中断统计信息收集是安全的,Oracle 会丢弃一半的数据,不会破坏业务。

2. (可选)临时锁定统计信息,防复发
为了防止自动任务马上再次启动,白天可以先锁住这张表:

EXEC DBMS_STATS.LOCK_TABLE_STATS('你的用户名', '你的表名');

3. 彻底解决:开启增量统计开关
在合适的窗口期,执行以下 PL/SQL 块(如果你是用 DBeaver,记得加上 BEGIN...END;,否则会报无效 SQL):

BEGIN
  -- 如果之前锁了,先解锁
  -- DBMS_STATS.UNLOCK_TABLE_STATS('用户名', '大表名');
  
  -- 设置该表开启增量统计信息
  DBMS_STATS.SET_TABLE_PREFS('用户名', '大表名', 'INCREMENTAL', 'TRUE');
END;

4. 验证是否成功生效

SELECT DBMS_STATS.GET_PREFS('INCREMENTAL', '用户名', '大表名') AS IS_INCREMENTAL 
FROM DUAL;
-- 结果返回 TRUE 即代表配置成功。


总结

  1. 系统卡顿不一定是 CPU 不够。 当 CPU 利用率低而 DB Time 很高时,立刻去查 I/O 等待(v$sysmetric 视图)。
  2. 看到 read by other session 不要慌。 它不是死锁,而是热块争用。揪出源头那条疯狂读盘的 SQL 是关键。
  3. 大表分区了不代表万事大吉。 对于持续增长的亿级历史表,如果分区后不开启增量统计信息(INCREMENTAL),Oracle 自动收集全局信息时的全表扫会教你做人。

排障犹如破案,不要只停留在表面的“杀进程”,顺着执行计划和底层原理往下挖,才能药到病除!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐