: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**了。咱們到時候接著拆。今天話題就聊到這歡迎留言交流。覺得內容有用別忘了點贊轉發給有需要的朋友回見