Oracle 块清除(Block Cleanout):commit 之后,数据块里的“战场“谁来打扫?
前言上一篇我们聊 DDL 的时候一起 dump 过数据块看过 ITL 里的 Xid 和 Uba也看过 undo 段头里那张事务表。当时有个细节我刻意没展开你 commit 之后再 dump 一次那个块会发现 ITL 条目还躺在那里行锁的标记也还在——事务明明结束了块里怎么还留着一地战斗痕迹这就引出了一个很多 DBA 都迷糊过的问题commit 不是事务的终点吗为什么提交了数据块里的事务信息还在更让人想不通的是有时候一个全表扫明明数据没怎么变却跑得忽快忽慢有时候 commit 返回成功了后面第一个来查这张表的人却莫名其妙多干了一堆活。摸爬滚打这么多年我可以负责任地说这类诡异现场十有八九都跟今天要聊的这个机制有关——块清除Block Cleanout。一句话概括commit 只是宣布战争结束而打扫战场这件事Oracle 是分批、甚至是甩锅给别人做的。这篇文章我们就把这件事讲透块里到底留了什么、谁来打扫、什么时候打扫、打扫不动了怎么办以及怎么亲手做实验把这些过程看得清清楚楚。一、先搞清楚块里到底留了什么痕迹上一篇我们讲过Oracle 改一行数据不是上去就改而是要先在块里办一堆手续。这些手续就是事务结束后需要清理的东西第一类行锁row lock。块里每一行都有一个锁定位lock byte指向锁住它的事务在 ITL 里的条目。行本身不知道我的事务提交了没有它只记着一个 ITL 条目号。第二类ITL 条目。Interested Transaction List块头里的事务登记表。里面有 Xid指向 undo 段头事务表的槽位、Uba指向这块的 undo 记录、还有我们今天的两位主角——commit flag和commit SCN。第三类free space credit。事务删行腾出来的空间在事务没提交之前别的事务是不能随便用的——万一回滚呢这块空间是记账记着的提交后要确认回收。一个事务可能动几百上千个块。commit 那一瞬间要把所有这些块的行锁解掉、ITL 标上已提交提交时间、空间记账结清——如果全部当场做完大事务的 commit 会慢得让你怀疑人生。Oracle 的解法很务实commit 只做最重要的一件事——在 undo 段头的事务表里把槽位标记为已提交并写下 commit SCN。这个动作做完commit 就算成功返回了。数据块那边能顺手收拾就收拾收拾不了就先欠着。能顺手收拾的叫Fast Block Cleanout先欠着的叫Deferred Block Cleanout。下面我们分开讲。二、Fast Block Cleanout趁热打铁顺手收拾Fast cleanout 的前提是commit 的时候事务得记得自己碰过哪些块。不然 buffer cache 那么大上哪儿找去所以事务在运行过程中会把改过的块记小本本上。这个小本本在内核里叫Block List State ObjectBL State Object状态对象的一种。它的结构很紧凑每个 BL State Object 最多记 20 个块条目每个条目里存三样东西该块在当前事务里的 savepoint 号回滚到保存点时要靠它定位范围、该块用的 ITL 索引、指向 buffer cache 里 块头的指针同一个块在清单里只出现一次改一百遍也只记一条清单总量有上限——大约是 buffer cache 缓冲区数量的 10%而且是动态的按 20 的倍数取整。11.2 的源码里可以在 ktl.h / ktl.c 里找到这套结构。超过上限的块、被逐出内存的块、或者 commit 那一刻正被别的进程 pin 住的块统统进不了 fast cleanout 的流程只能留给 deferred cleanout。commit 之后事务销毁这些 BL State Object 的时候内核函数 ktldbl会对清单里的每个块调一次 kcbnlc()尝试做no-logging cleanout——注意这个名字不产生日志的清除这是它的灵魂待会儿细说。每调一次统计项 commit cleanouts 加一做成功了commit cleanouts successfully completed 加一。这两个数的比率就是 fast cleanout 的成功率。顺手说一句如果某个 BL State Object 上的块连续失败 3 次Oracle 就不在这本账上浪费时间了直接跳到事务的下一个 SO 接着试——很现实的止损策略。真正干活的是 ktbdbc()。它拿到块之后做几件检查ITL 条目是不是还指向我们这个事务会不会已经被别人动过ITL 是不是已经被标成 committed / upperbound会不会已经被清理过了;块的 cleanout wrap 跟当前事务的 commit SCN wrap 对不对得上。检查通过就把 ITL 标记为 upperbound把 commit SCN 的 base 部分拷进 ITL。这里有个特别反直觉、也特别重要的设计fast cleanout 并不真正清掉行锁也不回收 free space credit。它只是告诉后来者这个事务在 SCN 多少多少之前就提交了——是个上限不是精确值所以 ITL 里连 commit SCN 的 wrap 部分都不存。行锁空间记账留给 deferred cleanout 或者下次真正要改这个块的人去收拾。上一篇说过的那句Oracle 的设计哲学是够用就好在这里体现得淋漓尽致。清除做完后块会被打上 KTBFUPB 标志delayed-logging cleanout块 SCN 更新为 commit SCN如果一样就 seq 加一块标脏设置 block high RBA——但全程不产生 redo 记录。为什么不产生 redo你想想cleanout 只是补记事务早就提交了这个事实这个事实本身在 undo 段头里已经有记录了即使 instance 崩溃、这个块恢复到旧版本大不了下次再清一遍丢了也不影响数据正确性。既然丢了无所谓干嘛花 redo 的代价去保护它这就是 no-logging 设计哲学的底层逻辑——只给丢了会出事的变化记 redo。三、为什么失败六个统计项就是排障清单fast cleanout 是个尽力而为的操作失败是常态。Oracle 把失败原因直接做成了统计项这在排障时就是现成的清单统计项含义commit cleanout failures: block lost块已经不在 cache 里了 / 被做成 CR 块 / 临时块 / 正在克隆commit cleanout failures: cannot pin想 pin 这个块但 pin 不住被别的进程占着commit cleanout failures: write disabled实例状态不允许写比如只读commit cleanout failures: hot backup in progress文件在热备中且块还没 loggedcommit cleanout failures: buffer being writtenDBWR 正在写这个 buffercommit cleanout failures: callback failuresktbdbc 报告没做成这里面最常见的就是第一条block lost——大事务改了大量块commit 的时候早期的块早就被挤出 buffer cache 了。这也是为什么大事务 commit 很快、但之后第一个扫到这些块的查询会变慢——那些没打扫的战场全留给它了。在 v$sesstat / v$sysstat 里盯着这几个值看再配合 commit cleanouts 的比率你基本就能判断一个系统里 deferred cleanout 的压力大不大。后面实验部分我会给出具体查法。四、Deferred Block Cleanout谁读到谁打扫fast cleanout 没收拾的块最终由下一个读到它的人清理——这就是 deferred block cleanout。形象点说fast 是谁污染谁治理deferred 是下一个进屋的人顺手把地扫了。最典型的场景是 CURCURRENT模式访问——你要修改这个块必须先拿到它的最新版本拿到之后就躲不开这上面的旧事务到底提交没有这个问题。判断的依据是 ITL 描述符里的C/U 标志C 和 U 都没有 → 这个 ITL 条目对应的事务看起来还活着有其中一个 → 已提交C 表示精确 commit SCNU 表示 upperbound 上限。看起来活着不等于真活着——commit 只更新了 undo 段头块这边没人通知。所以要确认就得顺着 Xid 去 undo 段头查户口。Xid 三段信息usnundo 段号、slot事务表槽位号、seq/wrap#槽位复用的序列号。查的过程会把这些信息收集到一个叫cleanout info area的地方。然后就会遇到三种情况一种比一种曲折Case 1undo 段已经没了。比如 undo 表空间重建过或者那个回滚段被 drop 了。别担心UNDO$ 里的行不会物理删除只是 STATUS$ 置成 1SCNBAS/SCNWRP 记下 drop 之前最近的 commit SCN。Oracle 拿着这个值给 ITL 条目打上 CU 标志——意思是这个事务在这个 SCN 之前肯定提交了。是个近似值但够用反正它要证明的只是这事务死透了。Case 2槽位被复用了。Xid 里的 seq 跟事务表槽位当前的 wrap# 对不上说明这个槽位已经换过主人原事务肯定提交了。但精确 commit SCN 呢事务表本身也被后来不断复用修改过——想找回原来的样子得对事务表本身做一致性读一层层应用 undo 记录回滚事务表的修改统计项transaction tables consistent reads - undo records applied记的就是这个。如果 undo 链走到头了还没找到只能退而求其次用一个近似值——KTUXCSCN也就是 lowtime。Case 3槽位没被动过。最省心的情况精确的 commit SCN 就躺在事务表里直接抄走。五、Lowtime、Hitime 和有效清除 SCN上面提到的 lowtime 值得展开讲讲它是理解这套机制的钥匙。undo 段头的控制结构 KTUXC 里有个字段KTUXCSCNlowtime它给出的保证是所有槽位已被复用的已提交事务它们的 commit SCN 都 ≤ lowtime。事务表里的已提交槽位是按 SCN 排成一条链的ktuxcchd 是链头最老的已提交事务ktuxcctl 是链尾最新的。新事务来找空闲槽位时从链头开始收——所以最老的槽位最先被复用lowtime 才能随之前移。很精巧的 LRU 式安排。对应的还有hitime等于这个 buffer 的 CR_SCN。于是事务表的每一个版本都覆盖一个确定的 SCN 区间[lowtime, hitime]——做一致性读找事务历史的时候就是靠这个区间定位哪个版本的事务表能看到我要的那个事务。最后说有效清除 SCNKTBBHCSC记在块的事务头里含义是这个块上次的清除工作对哪个 SCN 是有效的。清除的本质是一次全块事务状态盘点扫一遍所有 ITL验证带 C/U 标志的都确实提交了其余当时确实活跃。盘点的结论需要一个时间戳背书这个时间戳就是有效清除 SCN。它的取值有个微妙的坑如果清除过程中恰好有事务提交了怎么办比如清除开始于 SCN 50干到一半 T3 在 SCN 51 提交了——那有效清除 SCN 得取 51因为盘点结论已经反映了 51 时刻的世界。反过来如果块里还有活跃事务没提交有效清除 SCN 不能直接取已提交事务里的最大 SCN而要取各事务表 CR_SCN 的最小值且不能小于清除开始时的 SCN。一句话这个 SCN 必须同时对所有已验证的事务成立宁可保守不能冒进。六、追现场redo 与 event 10203deferred cleanout 跟 fast cleanout 不一样它是产生 redo 的——因为它真正改了 ITL、清了行锁这些变化丢了会出问题。它的 redo 记录属于layer 4transaction blockopcode 1block cleanout。记录内容包括有效清除 SCNKTBBHCSC以及每个被修改的 ITL 条目的 itli、flg 和 commit SCN。其中 flg 取值很有信息量1 SCN 是上限近似2 精确 commit SCN3 下限近似——前面讲的三种 case 带来的不确定性全浓缩在这一个小小的标志位里。想亲眼看到 cleanout 的过程Oracle 留了个专门的追踪事件event 10203ALTER SESSION SET EVENTS 10203 trace name context forever, level 2;level 1只记录 cleanout 的信息level 2附加块头和 ITL 的 dumplevel 3 及以上连整个块的内容一起 dump 出来。排查delayed block cleanout 引发的性能抖动这类问题时10203 加上前面那组统计项基本就够了。七、动手实验亲手制造一次 block lost光说不练假把式。下面这套实验我自己玩过很多次每一步都能在上面讲的理论里找到对应。第一步造一个受害块。终端 1CONNECT scott/tigerDROP TABLE ts;CREATE TABLE ts (a number, b number);CONNECT scott/tiger -- 重连一次把会话统计清零INSERT INTO ts VALUES (1,1);SELECT 1 FROM dual; -- 确认已执行第二步记录 commit 前的 cleanout 统计。终端 2sysdbaSELECT sid FROM v$session WHERE username SCOTT;SELECT sn.name, ss.valueFROM v$sesstat ss, v$statname snWHERE ss.sid SIDAND sn.name LIKE %cleanout%AND ss.statistic# sn.statistic#;第三步把块冲出 buffer cache断 fast cleanout 的后路ALTER SYSTEM FLUSH BUFFER_CACHE;第四步回终端 1 执行COMMIT;然后回终端 2 再查一次统计。你会看到 commit cleanouts 加了一但 commit cleanout failures: block lost 也跟着加了一——块不在了fast cleanout 想做也做不了。这就是教科书级的 deferred cleanout 伏笔。第五步看现场。先 dump 数据块file# 和 block# 可以从 dba_extents 或上一篇介绍的方法拿到ALTER SYSTEM DUMP DATAFILE BLOCK ;用 oradebug setmypid oradebug tracefile_name 找到 trace 文件看 ITL——flag 位还是空的Xid 还在行锁还在。再顺着 Xid 的第一段去查 undo 段头的位置SELECT file#, block# FROM undo$ WHERE us# ;dump 这个 undo 段头块你会看到事务表里那个槽位已经标成 committedcommit SCN 白纸黑字写着。事务提交了这个事实只在 undo 段头里数据块还蒙在鼓里——这就是 flush 之后的世界。第六步让下一个读者来打扫。换个会话对 TS 做一次 update 或 insert触发 CUR 模式访问然后重新 dump 数据块Xid 被清了ITL 打上了标志cleanout SCN 出现在块头里。再回头看 redo dump能找到 layer 4 opcode 1 的记录。undo 段头 dump 里的 txn control 部分引用的 UBA也可以顺藤摸瓜看看事务表自己的 undo——就是 Case 2 里回滚事务表用的那串链。走完这一遍fast/deferred 的分工、统计项的含义、ITL 标志的变化就都不是纸面概念了。总结把整件事串起来commit 的正事只有一件——改 undo 段头事务表写 commit SCN数据块的清理分两手在内存里、记得住、pin 得到的块commit 时顺手做 fast cleanoutno-logging、只标 upperbound、不碰行锁其余的全部 deferred等下一个 CUR 模式的访问者来按 Xid 查案undo 段没了用近似值槽位复用了对事务表做一致性读实在不行还有 lowtime 兜底整个过程留了充足的观测窗口一组commit cleanout%统计项、layer 4 opcode 1 的 redo、还有 event 10203。回到开头那个问题commit 之后事务信息为什么还在块里现在答案很清楚——那不是 bug是 Oracle 精打细算后的留白。它用最小的代价保证了提交这个事实的持久性把昂贵的清扫工作摊销给了时间。但这个留白也引出了下一个问题fast cleanout 留下的那个upperbound SCN说事务在这个点之前提交了却不说具体哪个点——那当一致性读要重建某个历史版本时到底凭什么判断这行数据我该看哪个版本近似 SCN、ITL 标志、undo 段头里的精确记录它们是怎么配合着把过去的某一瞬间精确还原出来的这就是下一篇要聊的**一致性读Consistent Read**了。咱们到时候接着拆。今天话题就聊到这欢迎留言交流。觉得内容有用别忘了点赞转发给有需要的朋友回见