:commit 之后,數(shù)據(jù)塊里的“戰(zhàn)場“誰來打掃?)
前言上一篇我們聊 DDL 的時候一起 dump 過數(shù)據(jù)塊看過 ITL 里的 Xid 和 Uba也看過 undo 段頭里那張事務(wù)表。當(dāng)時有個細(xì)節(jié)我刻意沒展開你 commit 之后再 dump 一次那個塊會發(fā)現(xiàn) ITL 條目還躺在那里行鎖的標(biāo)記也還在——事務(wù)明明結(jié)束了塊里怎么還留著一地戰(zhàn)斗痕跡這就引出了一個很多 DBA 都迷糊過的問題commit 不是事務(wù)的終點嗎為什么提交了數(shù)據(jù)塊里的事務(wù)信息還在更讓人想不通的是有時候一個全表掃明明數(shù)據(jù)沒怎么變卻跑得忽快忽慢有時候 commit 返回成功了后面第一個來查這張表的人卻莫名其妙多干了一堆活。摸爬滾打這么多年我可以負(fù)責(zé)任地說這類詭異現(xiàn)場十有八九都跟今天要聊的這個機制有關(guān)——塊清除Block Cleanout。一句話概括commit 只是宣布戰(zhàn)爭結(jié)束而打掃戰(zhàn)場這件事Oracle 是分批、甚至是甩鍋給別人做的。這篇文章我們就把這件事講透塊里到底留了什么、誰來打掃、什么時候打掃、打掃不動了怎么辦以及怎么親手做實驗把這些過程看得清清楚楚。一、先搞清楚塊里到底留了什么痕跡上一篇我們講過Oracle 改一行數(shù)據(jù)不是上去就改而是要先在塊里辦一堆手續(xù)。這些手續(xù)就是事務(wù)結(jié)束后需要清理的東西第一類行鎖row lock。塊里每一行都有一個鎖定位lock byte指向鎖住它的事務(wù)在 ITL 里的條目。行本身不知道我的事務(wù)提交了沒有它只記著一個 ITL 條目號。第二類ITL 條目。Interested Transaction List塊頭里的事務(wù)登記表。里面有 Xid指向 undo 段頭事務(wù)表的槽位、Uba指向這塊的 undo 記錄、還有我們今天的兩位主角——commit flag和commit SCN。第三類free space credit。事務(wù)刪行騰出來的空間在事務(wù)沒提交之前別的事務(wù)是不能隨便用的——萬一回滾呢這塊空間是記賬記著的提交后要確認(rèn)回收。一個事務(wù)可能動幾百上千個塊。commit 那一瞬間要把所有這些塊的行鎖解掉、ITL 標(biāo)上已提交提交時間、空間記賬結(jié)清——如果全部當(dāng)場做完大事務(wù)的 commit 會慢得讓你懷疑人生。Oracle 的解法很務(wù)實commit 只做最重要的一件事——在 undo 段頭的事務(wù)表里把槽位標(biāo)記為已提交并寫下 commit SCN。這個動作做完commit 就算成功返回了。數(shù)據(jù)塊那邊能順手收拾就收拾收拾不了就先欠著。能順手收拾的叫Fast Block Cleanout先欠著的叫Deferred Block Cleanout。下面我們分開講。二、Fast Block Cleanout趁熱打鐵順手收拾Fast cleanout 的前提是commit 的時候事務(wù)得記得自己碰過哪些塊。不然 buffer cache 那么大上哪兒找去所以事務(wù)在運行過程中會把改過的塊記小本本上。這個小本本在內(nèi)核里叫Block List State ObjectBL State Object狀態(tài)對象的一種。它的結(jié)構(gòu)很緊湊每個 BL State Object 最多記 20 個塊條目每個條目里存三樣?xùn)|西該塊在當(dāng)前事務(wù)里的 savepoint 號回滾到保存點時要靠它定位范圍、該塊用的 ITL 索引、指向 buffer cache 里 塊頭的指針同一個塊在清單里只出現(xiàn)一次改一百遍也只記一條清單總量有上限——大約是 buffer cache 緩沖區(qū)數(shù)量的 10%而且是動態(tài)的按 20 的倍數(shù)取整。11.2 的源碼里可以在 ktl.h / ktl.c 里找到這套結(jié)構(gòu)。超過上限的塊、被逐出內(nèi)存的塊、或者 commit 那一刻正被別的進程 pin 住的塊統(tǒng)統(tǒng)進不了 fast cleanout 的流程只能留給 deferred cleanout。commit 之后事務(wù)銷毀這些 BL State Object 的時候內(nèi)核函數(shù) ktldbl會對清單里的每個塊調(diào)一次 kcbnlc()嘗試做no-logging cleanout——注意這個名字不產(chǎn)生日志的清除這是它的靈魂待會兒細(xì)說。每調(diào)一次統(tǒng)計項 commit cleanouts 加一做成功了commit cleanouts successfully completed 加一。這兩個數(shù)的比率就是 fast cleanout 的成功率。順手說一句如果某個 BL State Object 上的塊連續(xù)失敗 3 次Oracle 就不在這本賬上浪費時間了直接跳到事務(wù)的下一個 SO 接著試——很現(xiàn)實的止損策略。真正干活的是 ktbdbc()。它拿到塊之后做幾件檢查ITL 條目是不是還指向我們這個事務(wù)會不會已經(jīng)被別人動過ITL 是不是已經(jīng)被標(biāo)成 committed / upperbound會不會已經(jīng)被清理過了;塊的 cleanout wrap 跟當(dāng)前事務(wù)的 commit SCN wrap 對不對得上。檢查通過就把 ITL 標(biāo)記為 upperbound把 commit SCN 的 base 部分拷進 ITL。這里有個特別反直覺、也特別重要的設(shè)計fast cleanout 并不真正清掉行鎖也不回收 free space credit。它只是告訴后來者這個事務(wù)在 SCN 多少多少之前就提交了——是個上限不是精確值所以 ITL 里連 commit SCN 的 wrap 部分都不存。行鎖空間記賬留給 deferred cleanout 或者下次真正要改這個塊的人去收拾。上一篇說過的那句Oracle 的設(shè)計哲學(xué)是夠用就好在這里體現(xiàn)得淋漓盡致。清除做完后塊會被打上 KTBFUPB 標(biāo)志delayed-logging cleanout塊 SCN 更新為 commit SCN如果一樣就 seq 加一塊標(biāo)臟設(shè)置 block high RBA——但全程不產(chǎn)生 redo 記錄。為什么不產(chǎn)生 redo你想想cleanout 只是補記事務(wù)早就提交了這個事實這個事實本身在 undo 段頭里已經(jīng)有記錄了即使 instance 崩潰、這個塊恢復(fù)到舊版本大不了下次再清一遍丟了也不影響數(shù)據(jù)正確性。既然丟了無所謂干嘛花 redo 的代價去保護它這就是 no-logging 設(shè)計哲學(xué)的底層邏輯——只給丟了會出事的變化記 redo。三、為什么失敗六個統(tǒng)計項就是排障清單fast cleanout 是個盡力而為的操作失敗是常態(tài)。Oracle 把失敗原因直接做成了統(tǒng)計項這在排障時就是現(xiàn)成的清單統(tǒng)計項含義commit cleanout failures: block lost塊已經(jīng)不在 cache 里了 / 被做成 CR 塊 / 臨時塊 / 正在克隆commit cleanout failures: cannot pin想 pin 這個塊但 pin 不住被別的進程占著commit cleanout failures: write disabled實例狀態(tài)不允許寫比如只讀commit cleanout failures: hot backup in progress文件在熱備中且塊還沒 loggedcommit cleanout failures: buffer being writtenDBWR 正在寫這個 buffercommit cleanout failures: callback failuresktbdbc 報告沒做成這里面最常見的就是第一條block lost——大事務(wù)改了大量塊commit 的時候早期的塊早就被擠出 buffer cache 了。這也是為什么大事務(wù) commit 很快、但之后第一個掃到這些塊的查詢會變慢——那些沒打掃的戰(zhàn)場全留給它了。在 v$sesstat / v$sysstat 里盯著這幾個值看再配合 commit cleanouts 的比率你基本就能判斷一個系統(tǒng)里 deferred cleanout 的壓力大不大。后面實驗部分我會給出具體查法。四、Deferred Block Cleanout誰讀到誰打掃fast cleanout 沒收拾的塊最終由下一個讀到它的人清理——這就是 deferred block cleanout。形象點說fast 是誰污染誰治理deferred 是下一個進屋的人順手把地掃了。最典型的場景是 CURCURRENT模式訪問——你要修改這個塊必須先拿到它的最新版本拿到之后就躲不開這上面的舊事務(wù)到底提交沒有這個問題。判斷的依據(jù)是 ITL 描述符里的C/U 標(biāo)志C 和 U 都沒有 → 這個 ITL 條目對應(yīng)的事務(wù)看起來還活著有其中一個 → 已提交C 表示精確 commit SCNU 表示 upperbound 上限。看起來活著不等于真活著——commit 只更新了 undo 段頭塊這邊沒人通知。所以要確認(rèn)就得順著 Xid 去 undo 段頭查戶口。Xid 三段信息usnundo 段號、slot事務(wù)表槽位號、seq/wrap#槽位復(fù)用的序列號。查的過程會把這些信息收集到一個叫cleanout info area的地方。然后就會遇到三種情況一種比一種曲折Case 1undo 段已經(jīng)沒了。比如 undo 表空間重建過或者那個回滾段被 drop 了。別擔(dān)心UNDO$ 里的行不會物理刪除只是 STATUS$ 置成 1SCNBAS/SCNWRP 記下 drop 之前最近的 commit SCN。Oracle 拿著這個值給 ITL 條目打上 CU 標(biāo)志——意思是這個事務(wù)在這個 SCN 之前肯定提交了。是個近似值但夠用反正它要證明的只是這事務(wù)死透了。Case 2槽位被復(fù)用了。Xid 里的 seq 跟事務(wù)表槽位當(dāng)前的 wrap# 對不上說明這個槽位已經(jīng)換過主人原事務(wù)肯定提交了。但精確 commit SCN 呢事務(wù)表本身也被后來不斷復(fù)用修改過——想找回原來的樣子得對事務(wù)表本身做一致性讀一層層應(yīng)用 undo 記錄回滾事務(wù)表的修改統(tǒng)計項transaction tables consistent reads - undo records applied記的就是這個。如果 undo 鏈走到頭了還沒找到只能退而求其次用一個近似值——KTUXCSCN也就是 lowtime。Case 3槽位沒被動過。最省心的情況精確的 commit SCN 就躺在事務(wù)表里直接抄走。五、Lowtime、Hitime 和有效清除 SCN上面提到的 lowtime 值得展開講講它是理解這套機制的鑰匙。undo 段頭的控制結(jié)構(gòu) KTUXC 里有個字段KTUXCSCNlowtime它給出的保證是所有槽位已被復(fù)用的已提交事務(wù)它們的 commit SCN 都 ≤ lowtime。事務(wù)表里的已提交槽位是按 SCN 排成一條鏈的ktuxcchd 是鏈頭最老的已提交事務(wù)ktuxcctl 是鏈尾最新的。新事務(wù)來找空閑槽位時從鏈頭開始收——所以最老的槽位最先被復(fù)用lowtime 才能隨之前移。很精巧的 LRU 式安排。對應(yīng)的還有hitime等于這個 buffer 的 CR_SCN。于是事務(wù)表的每一個版本都覆蓋一個確定的 SCN 區(qū)間[lowtime, hitime]——做一致性讀找事務(wù)歷史的時候就是靠這個區(qū)間定位哪個版本的事務(wù)表能看到我要的那個事務(wù)。最后說有效清除 SCNKTBBHCSC記在塊的事務(wù)頭里含義是這個塊上次的清除工作對哪個 SCN 是有效的。清除的本質(zhì)是一次全塊事務(wù)狀態(tài)盤點掃一遍所有 ITL驗證帶 C/U 標(biāo)志的都確實提交了其余當(dāng)時確實活躍。盤點的結(jié)論需要一個時間戳背書這個時間戳就是有效清除 SCN。它的取值有個微妙的坑如果清除過程中恰好有事務(wù)提交了怎么辦比如清除開始于 SCN 50干到一半 T3 在 SCN 51 提交了——那有效清除 SCN 得取 51因為盤點結(jié)論已經(jīng)反映了 51 時刻的世界。反過來如果塊里還有活躍事務(wù)沒提交有效清除 SCN 不能直接取已提交事務(wù)里的最大 SCN而要取各事務(wù)表 CR_SCN 的最小值且不能小于清除開始時的 SCN。一句話這個 SCN 必須同時對所有已驗證的事務(wù)成立寧可保守不能冒進。六、追現(xiàn)場redo 與 event 10203deferred cleanout 跟 fast cleanout 不一樣它是產(chǎn)生 redo 的——因為它真正改了 ITL、清了行鎖這些變化丟了會出問題。它的 redo 記錄屬于layer 4transaction blockopcode 1block cleanout。記錄內(nèi)容包括有效清除 SCNKTBBHCSC以及每個被修改的 ITL 條目的 itli、flg 和 commit SCN。其中 flg 取值很有信息量1 SCN 是上限近似2 精確 commit SCN3 下限近似——前面講的三種 case 帶來的不確定性全濃縮在這一個小小的標(biāo)志位里。想親眼看到 cleanout 的過程Oracle 留了個專門的追蹤事件event 10203ALTER SESSION SET EVENTS 10203 trace name context forever, level 2;level 1只記錄 cleanout 的信息level 2附加塊頭和 ITL 的 dumplevel 3 及以上連整個塊的內(nèi)容一起 dump 出來。排查delayed block cleanout 引發(fā)的性能抖動這類問題時10203 加上前面那組統(tǒng)計項基本就夠了。七、動手實驗親手制造一次 block lost光說不練假把式。下面這套實驗我自己玩過很多次每一步都能在上面講的理論里找到對應(yīng)。第一步造一個受害塊。終端 1CONNECT scott/tigerDROP TABLE ts;CREATE TABLE ts (a number, b number);CONNECT scott/tiger -- 重連一次把會話統(tǒng)計清零INSERT INTO ts VALUES (1,1);SELECT 1 FROM dual; -- 確認(rèn)已執(zhí)行第二步記錄 commit 前的 cleanout 統(tǒng)計。終端 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 執(zhí)行COMMIT;然后回終端 2 再查一次統(tǒng)計。你會看到 commit cleanouts 加了一但 commit cleanout failures: block lost 也跟著加了一——塊不在了fast cleanout 想做也做不了。這就是教科書級的 deferred cleanout 伏筆。第五步看現(xiàn)場。先 dump 數(shù)據(jù)塊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 段頭塊你會看到事務(wù)表里那個槽位已經(jīng)標(biāo)成 committedcommit SCN 白紙黑字寫著。事務(wù)提交了這個事實只在 undo 段頭里數(shù)據(jù)塊還蒙在鼓里——這就是 flush 之后的世界。第六步讓下一個讀者來打掃。換個會話對 TS 做一次 update 或 insert觸發(fā) CUR 模式訪問然后重新 dump 數(shù)據(jù)塊Xid 被清了ITL 打上了標(biāo)志cleanout SCN 出現(xiàn)在塊頭里。再回頭看 redo dump能找到 layer 4 opcode 1 的記錄。undo 段頭 dump 里的 txn control 部分引用的 UBA也可以順藤摸瓜看看事務(wù)表自己的 undo——就是 Case 2 里回滾事務(wù)表用的那串鏈。走完這一遍fast/deferred 的分工、統(tǒng)計項的含義、ITL 標(biāo)志的變化就都不是紙面概念了。總結(jié)???????把整件事串起來commit 的正事只有一件——改 undo 段頭事務(wù)表寫 commit SCN數(shù)據(jù)塊的清理分兩手在內(nèi)存里、記得住、pin 得到的塊commit 時順手做 fast cleanoutno-logging、只標(biāo) upperbound、不碰行鎖其余的全部 deferred等下一個 CUR 模式的訪問者來按 Xid 查案undo 段沒了用近似值槽位復(fù)用了對事務(wù)表做一致性讀實在不行還有 lowtime 兜底整個過程留了充足的觀測窗口一組commit cleanout%統(tǒng)計項、layer 4 opcode 1 的 redo、還有 event 10203。回到開頭那個問題commit 之后事務(wù)信息為什么還在塊里現(xiàn)在答案很清楚——那不是 bug是 Oracle 精打細(xì)算后的留白。它用最小的代價保證了提交這個事實的持久性把昂貴的清掃工作攤銷給了時間。但這個留白也引出了下一個問題fast cleanout 留下的那個upperbound SCN說事務(wù)在這個點之前提交了卻不說具體哪個點——那當(dāng)一致性讀要重建某個歷史版本時到底憑什么判斷這行數(shù)據(jù)我該看哪個版本近似 SCN、ITL 標(biāo)志、undo 段頭里的精確記錄它們是怎么配合著把過去的某一瞬間精確還原出來的這就是下一篇要聊的**一致性讀Consistent Read**了。咱們到時候接著拆。今天話題就聊到這歡迎留言交流。覺得內(nèi)容有用別忘了點贊轉(zhuǎn)發(fā)給有需要的朋友回見