本帖最後由 wei-xh 于 2013-7-19 16:45 編輯
log buffer在Oracle的各個版本裡一共發生過2次重大的改變:
1)多log buffer(redo strands)的出現(9I)
2)private redo和in memory undo的出現(10g)
你可能會補充說,還應該包括12c的多lgwr,是的,這也是一次革命性的改變,但是目前還沒有對它有深刻的研究。我們暫時先把目光放回到11G之前。
ORACLE兩次log buffer的重大改變,都跟redo allocation latch有着直接性的聯系。本文以redo allocation latch切入點,描述log buffer的發展進化史。
由于題目寫的比較大,不可能面面俱到,沒有做太多細節性的描述。redo allocation latch存在的問題
既然說是redo allocation latch的問題,那麼你可能會問,redo allocation latch出了什麼問題,為什麼要對它進行改進?
我來給大家一個簡單的示例,一目了然的能知道一個小小的redo allocation latch存在什麼嚴重的性能問題:
下面的代碼裡有一張wxh_tbd的表,表上無索引,我們更新了3條記錄。下面的代碼中,你隻需要關注update的語句,其他的語句都是我采的快照,為了做分析用,你可以簡單的把它忽略。
execute snap_latch.start_snap
execute snap_my_stats.start_snap
update wxh_tbd set object_name='t' where rownum<4;
execute snap_latch.end_snap
execute snap_my_stats.end_snap
---------------------------------
Latch waits:- 17-Jul 08:51:59
Interval:- 0 seconds
---------------------------------
Latch Gets Misses Sp_Get Sleeps Im_Gets Im_Miss Holding Woken Time ms
----- ---- ------ ------ ------ ------- ------- ------- ----- -------
redo copy 0 0 0 0 3 0 0 0 .0
redo allocation 0 0 0 0 3 0 0 0 .0
Session stats - 17-Jul 09:11:39
Interval:- 0 seconds
---------------------------------
Name Value
---- -----
redo entries 3redo size 1,180
非常容易看到,僅僅是修改了3條記錄,ORACLE就申請了三次的redo copy latch,redo allocation latch。産生了3個redo entries,也就是3個redo record。
不難分析出每産生一個redo record,oracle就要申請一次redo copy latch+redo allocation latch。
那什麼時候會産生一個redo record呢?它由什麼組成的呢?
先回答第一個問題:每修改資料塊一次!就産生一個redo record。
一般情況下,一個redo recored由一個undo record(用來描述undo的修改)和一個redo change vector(用來描述資料塊的修改)組成,如下:
REDO RECORD - Thread:1 RBA: 0x00036f.00000005.008c LEN: 0x00f8 VLD: 0x01
SCN: 0x0000.03ee485a SUBSCN: 1 03/13/2011 17:43:01
CHANGE #1 TYP:0 CLS:36 AFN:2 DBA:0x0080009a SCN:0x0000.03ee485a SEQ: 4 OP:5.1 ---------undo record
…
CHANGE #2 TYP:0 CLS: 1 AFN:11 DBA:0x02c0018a SCN:0x0000.03ee485a SEQ: 2 OP:11.5--------redo change vector
…
上面的dump記錄裡,OP:5.1代表的是undo 塊的修改,OP:11.5代表對資料塊的修改。還有更多的OP代碼,大家可以去參照了解下。
我們的實驗裡,修改了三次資料塊,産生了三個redo record。如果object_name有索引的話,redo copy latch,redo allocation latch的數字至少是9.因為索引的每次update相當于是delete+insert,索引部分至少會增加6次redo copy latch,redo allocation latch的申請。
ORACLE裡對于redo copy latch的數量一般是比較多的,依據你的CPU數來決定,預設是CPU數的兩倍(通過隐含參數_log_simultaneous_copies控制)。是以造成競争的幾率很低。
但是對于redo allocation latch來說,在多redo strands出現之前,就隻有一把。在一個交易頻繁的系統裡,勢必會造成對redo allocation latch的競争。
Redo allocation latch有什麼用?
毋庸置疑,log buffer是個共享的地方,有很多會話需要在這個空間裡為自己的日志配置設定空間,必須要機制去保證保證會話間彼此的日志不互相覆寫。
ORACLE裡是通過redo allocation latch做到的這一點。
1.jpg (18.62 KB, 下載下傳次數: 247)
2013-7-17 13:32 上傳
看上面的圖,我們設定了log buffer 由9個log buffer block組成,典型的,一般一個log buffer block跟磁盤的扇區大小比對,512位元組。SSD之類的固态盤很多已經不是這個扇區大小了。
這裡面有2個指針需要特别指出來,一個是start of free space,一個是end of free space。這兩個指針的移動靠redo allocation latch去保護。
我們可以舉個例子來,這樣更容易了解:
session 1,session 2,session 3同時想要在log buffer裡配置設定空間,為了簡化我們的場景,我們假設每個SESSION計劃配置設定512位元組的空間,也就是剛好是一個log buffer block塊的大小。
redo allocation latch被session 1搶到,假如目前的start of free space的位置在塊1,那麼session 1持有redo allocation latch後,移動start of free space指針到2。然後釋放latch。
2.gif (18.13 KB, 下載下傳次數: 198)
2013-7-17 13:33 上傳
由于session 2,session 3沒有搶到唯一的一把redo allocation latch,它不能去log buffer 裡預留白間,隻能等待latch釋放。
session 1釋放latch後,假如session 2獲得了,它繼續移動start of free space到3,然後釋放latch。
3.jpg (17.43 KB, 下載下傳次數: 186)
2013-7-17 13:33 上傳
通過上面的機制,保證了多個session并發往log buffer配置設定空間的時候,不會導緻日志覆寫。
你可能會問:你怎麼沒有提到end of free space?
好,我們來說說這個end of free space什麼時候會移動。
假如session 2的記錄是一個commit record,那麼它會觸發lgwr寫,lgwr重新整理完日志後(我們的例子中,會把1,2兩個buffer刷到磁盤),移動end of free space到3。
這個時候end of free space和start of free space在位置上是重合的
6.jpg (19.38 KB, 下載下傳次數: 191)
2013-7-17 15:21 上傳
最後session 3獲得了redo allocation latch,移動start of free space到4.
7.jpg (18.35 KB, 下載下傳次數: 182)
2013-7-17 13:34 上傳
經過前面的描述,相信你基本上明白了redo allocatoin latch為什麼面臨性能問題:争用嚴重,以及它存在的價值:保護空間配置設定。
在我們真正去看9I出現的redo strands之前,我們可以再看一個稍微深入的實驗。這個實驗裡,僅僅跟我們第一次的實驗,多了一個commit
execute snap_latch.start_snap
update wxh_tbd set object_name='t' where rownum<4;
commit;
execute snap_latch.end_snap
---------------------------------
Latch waits:- 17-Jul 11:42:52
Interval:- 0 seconds
---------------------------------
Latch Gets Misses Sp_Get Sleeps Im_Gets Im_Miss Holding Woken Time ms
----- ---- ------ ------ ------ ------- ------- ------- ----- -------
redo copy 0 0 0 0 4 0 0 0 .0
redo allocation 3 0 0 0 4 0 0 0 .0
僅僅多了一個commit,redo allocation latch的申請的數量多了4次!!!!!
commit本身産生的redo record要申請一次redo allocation。+1
由于我的環境裡有2個redo strands,是以送出的時候,lgwr要獲得每個redo strand的redo allocation latch,lgwr此時擷取redo allocation latch是為了start of free space指針到目前buffer block的結尾(日志寫浪費就是這麼産生的) +2
寫完日志後,ORACLE要再次擷取redo allocation latch,移動end of free space指針到目前寫位置處。由于我隻有一個redo strand有日志要寫(另外一個是空的),是以申請一個redo strand的redo allocation latch。+1
9I 多log buffer的出現ORACLE從9I起,有了一個新特性,允許使用多個redo strands,多個redo strands組成一個log buffer。每一個redo strand由一個redo allocation latch保護。這樣的話,一定程度上增加的redo配置設定空間的擴充性。
但是多redo strands的出現,ORACLE需要解決一些棘手的問題,首先就是:事物日志的順序問題
我們假設現在系統裡存在2個redo strands。
1)首先考慮單個SESSION的情況
update xxx set name=1 where rowid=xxx;
update xxx set name=2 where rowid=xxx;
對于同樣的rowid,一個事物内修改了2次,最終的結果是name=2。
如果name=1的日志拷貝到了redo strand2,name=2的日志拷貝到了redo strand1。
lgwr重新整理日志後,真正寫到log裡的日志順序,可能是先更新name=2,再更新name=1。
這樣就會導緻最終的資料不正确。
如何解決?
ORACLE可以通過會話與固定的redo strand做綁定,保證一個會話産生的redo一定在一個redo strand裡就可以了,這點我相信不難做到。
2)我們再看看多個session的情況。
SESSION 1:
update xxx set name=1 where rowid=xxx;
SESSION 2:
update xxx set name=1 where rowid=xxx;
貌似依然會出現我們情況一裡的那種問題。但是,但是,真的不會出現。(後面我會又否認不會出現,慢慢看吧)
SESSION1更新成功後,如果不送出,SESSION 2的會話被等待鎖,不會産生日志。
如果session 2能夠更新成功了,說明SESSION 1一定送出了,送出一定代表日志已經在SESSION 2前持久化到磁盤了。是以,這種情況,SESSION 2的日志一定是落後于SESSION 1到LOG裡的。
是以有了ORACLE裡的鎖做保障,我們可以對這種情況高枕無憂了。
但是?我又要說但是了,ORACLE裡存在一些ACID的異常。相信很多人知道這個特性了,但是并不知道,它跟這裡說的日志順序有什麼關系。
如果你對于ORACLE的ACID已經非常了解了,下面的一塊内容,你直接跳過。
什麼是ACID異常?
首先我們需要了解,commit到底做了什麼?
事物開始的時候,會先在復原段段頭(undo segment header)的事物表内申請一個slot,并在這個slot内記錄下此事物的一些資訊:比如事物的狀态:活躍/送出,事物目前用的最後一個undo塊是哪個(復原的起點),一共使用了多少undo塊等等。
事物結束的時候,也還會在這個slot上登記這個事物已經送出了,并且産生一個commit recored,這個commit recored說白了就是用來描述復原段頭這個slot的改變,最最重要的是描述了事物已經從活躍變為了送出。
事物表的slot一旦标記為送出,也就意味着别的會話能夠看到此事物所作的所有修改了。
我們來列下commit後,觸發的操作:
1)産生一個redo record去描述對于事物表slot的修改
2)拷貝這個redo record到log buffer
3)應用這個redo recored到對應的undo segment header
4)通知lgwr去寫日志
我們要知道,我們在步驟3的時候,别的會話就認為這個事物已經送出了。但是這個時候,日志是還沒持久化到磁盤的,會話僅僅是通知了LGWR去寫。
步驟3和步驟4之間是有時間差的,雖然這個時間差很小!
OK,我們知道了什麼是ACID異常,我們繼續我們的問題:
我們再看看多個session的情況。
SESSION 1:
update xxx set name=1 where rowid=xxx;
SESSION 2:
update xxx set name=1 where rowid=xxx;
假如session 1送出了,通知lgwr去寫了,但是lgwr在沒真正開始工作之前,session 2已經看到session 1釋放了鎖,它可以産生它的事物日志往redo buffer裡拷貝了。
由于ORACLE的GROUP COMMIT特性,LGWR每次都會盡可能多的拷貝BUFFER裡的資料。很有可能session 2,session 1的LOG都同時重新整理到了磁盤裡,而且很可能是亂序的。
世界再一次變亂了,因為ORACLE的鎖機制遇到ACID異常後,失靈了!
是以我認為ORACLE在恢複階段,必須要對日志進行排序來規避這個問題。這裡我們并不去探讨這個問題。
我們接着聊我們的主題:redo allocation latch的問題(有時候路走的太遠了,就忘記了我們當初為什麼出發)。
雖然我們看到ORACLE引入多redo strands後,似乎緩解了redo allocation latch的争用問題。但是解決的不夠徹底。僅僅是對redo allocation latch做了非常有限的擴充。很容易看到,大多數的系統裡預設的這個strands的數量是2,僅僅是提升了2倍的擴充性。當然這個值你可以設定,但是并不是越大越好。原因很複雜,看情況,可以再寫一寫這個方面的部落格。
10G private redo的出現
如果我們能把一個事物産生的redo一次性的拷貝到log buffer(一次事務隻申請一次redo allocation latch),而不是之前的one change=one allocation latch,那麼就能極大程度的緩解了redo allocation latch競争的問題。
ORACLE 10G後引入了private redo和in memory undo,解決了這個問題,之是以說的是解決,而不是徹底解決,是因為新機制的限制比較多。
在你打開輔助日志、FLASHBACK DATABASE特性、事務過大(超過了pool的大小)、RAC,都将不能使用這一特性
private redo,in memory undo本質上就是在記憶體裡開辟出一塊區域用來臨時存放會話的私有日志,等送出的時候,一次性的刷入到公共的log buffer裡。
這些私有區域由許多的private redo pool,in memory undo pool組成,在64位系統上,每個pool都接近128K的大小,這些pool用來臨時存放會話的日志:資料塊的redo放入private redo pool中,undo塊的redo放入in memory undo pool中。
在private redo和in memory undo機制下,我們文章剛開始的一個update流程,可以用如下順序表示:
update wxh_tbd set object_name='t' where rownum<4;
1)首先擷取一對私有的memory pool:private redo pool,in memory undo pool
2)标記每一個修改的塊有private redo
3)把産生的undo change vector寫入in memory undo pool
4)把産生的redo change vector寫入private redo pool
5)事物送出的時候,把2個pool裡的change vector結合成一個大的redo record
6)拷貝這個record到公有的redo buffer裡,并且應用這個record到對應的資料塊和undo塊。
從上面的流程描述可以知道:
1)一個事物的所有的change vector在送出的時刻從私有memory結合成一個大的redo record拷貝到公有的redo buffer裡,隻需要申請一次redo allocation latch。
大大的降低了redo allocation latch的申請數。
2)資料塊、undo塊隻有在最後事務送出的時刻,才去應用日志改變。
其實在上面的描述中,為了簡化,故意漏掉了一些細節:
1)每個private redo pool是由私有的redo allocation latch來保護的,每次事務期間,會話隻需要申請一次私有的redo allocation latch就可以了,個人認為ORACLE将會話與私有redo pool做了綁定,比如在PGA裡留一個指針或者描述符來指向這個redo pool。會話獲得私有redo allocation latch後,标記一下這個私有pool已被使用,告訴别的會話不要再來使用這個pool。
2)每個in memory undo pool是由in memory undo latch來保護的,對于這個latch的申請要頻繁一些,每一條undo record都要申請一次in memory undo latch。貌似ORACLE僅僅是把競争從redo allocation latch變為了in memory undo latch的競争,但是事實并不是這樣,因為in memory undo latch的數量是很多的,每個pool都對應了一個in memory undo latch。
3)我的描述中說,直到事務送出,否則ORACLE不會去應用日志到任何的資料塊和UNDO塊。但是對于UNDO段頭的事物表是個特殊,每次事務開始前,都必須要修改這個slot。
我們可以重新梳理一下update流程:
1)首先擷取一對私有的memory pool:private redo pool,in memory undo pool。private redo pool的使用要擷取私有的redo allocation latch,擷取後,标記此pool被使用,釋放latch。in memory undo pool的使用要擷取in memory undo latch,每次有undo change vector産生都要持有in memory undo latch。
2)在undo段頭的slot中,記錄事務已經開始,以及相關資訊
3)标記每一個修改的塊有private redo(但是并不真正的修改資料塊)
3)把産生的undo change vector寫入in memory undo pool
4)把産生的redo change vector寫入private redo pool
5)事物送出的時候,把2個pool裡的change vector結合成一個大的redo record
6)拷貝這個record到公有的redo buffer裡,并且應用這個record到對應的資料塊和undo塊。
雖然文章很長,但是由于牽扯的知識面較廣,限于文章篇幅,有些細節并沒有做過多的描述。裡面很多的内容都可以單獨再拉出來讨論。