buffer cache 深度解析

 http://blog.csdn.net/cymm_liu/article/details/7477695


   本文首先詳細介紹了Oracle中buffer cache的概念以及所包含的內存結構。然後結合各個後臺進程(包括DBWRn、CKPT、LGWR等)深入介紹了oracle對於buffer cache的管理機制,並詳細解釋了oracle爲什麼會採用現在的管理機制,是爲了解決什麼問題。比如爲何會引入touch次數、爲何會引入增量檢查點等等。最後全面介紹了有關buffer cache監控以及調優的實用方法。

1. buffer cache的概念
  用最簡單的語言來描述oracle數據庫的本質,其實就是能夠用磁盤上的一堆文件來存儲數據,並提供了各種各樣的手段對這些數據進行管理。作爲管理數據的最基本要求就是能夠保存和讀取磁盤上的文件中的數據。衆所周知,讀取磁盤的速度相對來說是非常慢的,而內存相對速度則要快的多。因此爲了能夠加快處理數據的速度,oracle必須將讀取過的數據緩存在內存裏。而oracle對這些緩存在內存裏的數據起了個名字:數據高速緩存區(db buffer cache),通常就叫做buffer cache。按照oracle官方的說法,buffer cache就是一塊含有許多數據塊的內存區域,而這些數據塊主要都是數據文件裏的數據塊內容的拷貝。通過初始化參數:buffer_cache_size來指定buffer cache的大小。oracle實例一旦啓動,該區域大小就被分配好了。

buffer cache所能提供的功能主要包括:
1) 通過緩存數據塊,從而減少I/O。
2) 通過構造CR塊,從而提供讀一致性功能。
3) 通過提供各種lock、latch機制,從而提供多個進程併發訪問同一個數據塊的功能。


2.buffer cache的內存結構

2.1 buffer cache概述

oracle內部在實現其管理的過程中,有兩個非常有名的名詞:鏈表和hash算法
鏈表是一種數據結構,通過將對象串連在一起,從而構成鏈表結構。這樣,如果要修改、刪除、查找某個對象的話,都可以先到鏈表中去查找,而不必實際的訪問物理介質(這樣可以直接修改刪除查找某個塊對應的buffer header)。oracle中最有名的鏈表大概就是LRU鏈表了,我們後面會介紹它。

  而hash算法則是爲了能夠進行快速查找定位所使用一種技術。所謂hash算法,就是根據要查找的值,對該值進行一定的hash算法後得出該值所在的索引號,然後進入到該值應該存在的一列數值列表(可以理解爲一個二維數組)裏,通過該索引號去找它應該屬於哪一個列表。然後再進入所確定的列表裏,對其中所含有的值,進行一個一個的比較,從而找到該值。這樣就避免了對整個數值列表進行掃描才能找到該值,這種全掃描的方式顯然要比hash查找方式低效很多。其中,每個索引號對應的數值列在oracle裏都叫做一個hash bucket。

  我們來列舉一個最簡單的hash算法。假設我們的數值列表最多可以有10個元素,也就是有10個hash buckets,每個元素最多可以包含20個數值。則對應的二維數組就是t[10][20]。我們可以定義hash算法爲n MOD 10。通過這種算法,可以將所有進入的數據均勻放在10個hash bucket裏面,hash bucket編號從0到9。比如,我們把1到100都通過這個hash函數均勻放到這10個hash bucket裏,當查找32在哪裏時,只要將32 MOD 10等於2,這樣就知道可以到2號hash bucket裏去找,也就是到t[2][20]裏去找,2號hash bucket裏有10個數值,逐個比較2號hash bucket裏是否存在32就可以了。

  buffer cache就是使用多個hash bucket來管理的,其hash算法當然比我們前面列舉的要複雜多了。
我們先來看下面這個圖一。這副圖從邏輯上說明了整個buffer cache的結構是怎麼樣的。這副圖的右
上角列出了三個名詞:hash bucket、buffer header和hash chain。

  這裏的hash bucket就是我們前面說明hash算法中提到的二維數組的第一維。它是通過對buffer header
 裏記錄的數據塊地址和數據塊類型運用hash算法以後,得到的組號。
這裏的hash chain就是屬於同一個hash bucket的所有buffer header所串起來的鏈表。實際上,hash 
bucket只是一個邏輯上的概念每個hash bucket都是通過不同的hash chain而體現出來的。每個hash chain都會由一個cache buffers chains latch來管理其併發操作。

  而對於buffer header來說,每一個數據塊在被讀入buffer cache時,都會先在buffer cache中構造一個buffer header,buffer header與數據塊一一對應。buffer header包含的主要信息有:
1) 該數據塊在buffer cache中實際的內存地址。就是上圖中的虛線箭頭所表示的意思。
2) 該數據塊的類型,包括data、segment header、undo header、undo block等等。
3) 該buffer header所在的hash chain,是通過在buffer header裏保存指向前一個buffer header的指針和指向後一個buffer header的指針的方式實現的。
4) 該buffer header所在的LRU、LRUW、CKPTQ等鏈表(這些鏈表我們後面都會詳細說明)。也是通過記錄前後buffer header指針的方式實現。
5) 當前該buffer header所對應的數據塊的狀態以及標記
6) 該buffer header被訪問(touch)的次數
7) 正在等待該buffer header的進程列表(waiter list)和正在使用該buffer header的進程列表(user list)。(難道正在使用該buffer header和正在等待該buffer header的進程表會皴法在當前buffer header中?對,正在使用的進程存在裏面,可以理解,爲什麼正在等待的進程也在裏面呢?)
    
   buffer cache中,缺省的hash bucket的數量或者說缺省有多少條hash chain鏈表,是由一個隱藏參數:
_db_block_hash_buckets決定的。至於該參數的取值,在我的測試中,8i下,該參數缺省爲db_block_buffers×2;但是到了9i以後,該參數似乎取的是小於且最接近於db_block_buffers×2的素數。

2.2 轉儲buffer cache
就象實例中的其他內存結構一樣,oracle提供了可以將buffer cache轉儲到跟蹤文件的方法。方法如下:

ALTER SESSION SET EVENTS 'immediate trace name buffers level level';

  這裏的level有很多值,分別可以轉儲buffer cache中的不同的內容。level的可選值包括:
1 只轉儲buffer header
2 在level 1的基礎上再轉儲數據塊頭
3 在level 2的基礎上再轉儲數據塊內容
4 轉儲buffer header和hash chain
5 在level 1的基礎上再轉儲數據塊頭和hash chain
6 在level 2的基礎上再轉儲數據塊內容和hash chain
8 轉儲buffer header和hash chain以及users/waiters鏈表
9 在level 1的基礎上再轉儲數據塊頭、hash chain以及users/waiters鏈表
10 在level 2的基礎上再轉儲數據塊內容、hash chain以及users/waiters鏈表

我們創建一個簡單的測試表,然後看看轉儲出來的buffer header是什麼樣子的。

SQL> create table buffer_test(id number);SQL> select object_id from dba_objects where object_name='BUFFER_TEST'; OBJECT_ID----------      7087SQL> insert into buffer_test values(1);SQL> commit;

這時我們知道buffer_test表的object_id是7987,同時,該表中只有2個block具有數據。1個是segment header,另一個就是實際存放了1這個值的數據塊。接着我們把buffer header轉儲出來:

SQL> ALTER SESSION SET EVENTS 'immediate trace name buffers level 1';

到user_dump_dest所定義的目錄下,找到跟蹤文件並打開,可以看到類似下面的信息,這裏我們列出前兩個buffer header以及我們建立的object_id爲7087的buffer_test表所對應的buffer header的內容:


BH (0x637F0720) file#: 1 rdba: 0x004011ed (1/4589) class 1 ba: 0x63570000…………………………………  hash: [64be8000,65a5eab4] lru: [637f06ac,637f0824]  LRU flags: moved_to_tail  ckptq: [NULL] fileq: [NULL]…………………………………BH (0x64BE8000) file#: 0 rdba: 0x00000000 (0/0) class 0 ba: 0x64800000…………………………………  hash: [65a5eab4,637f0720] lru: [64be8104,65aa3f0c]…………………………………BH (0x63BEC0A0) file#: 6 rdba: 0x0180b00a (6/45066) class 1 ba: 0x638B0000  set: 3 dbwrid: 0 obj: 7087 objn: 7087  hash: [65a9ccd4,65a9ccd4] lru: [63bec1a4,63bec02c]  ckptq: [65abceb4,63bec66c] fileq: [65abcfbc,63becd10]  st: XCURRENT md: NULL rsop: 0x00000000 tch: 1  flags: buffer_dirty gotten_in_current_mode redo_since_read  LRBA: [0xe9.229.0] HSCN: [0x0000.00122967] HSUB: [1] RRBA: [0x0.0.0]BH (0x63BECAE8) file#: 6 rdba: 0x0180b009 (6/45065) class 4 ba: 0x638CC000  set: 3 dbwrid: 0 obj: 7087 objn: 7087  hash: [65a9cbcc,65a9cbcc] lru: [63becbec,63beca74]  ckptq: [637fc250,63becdc4] fileq: [65ab8ad0,63becdcc]  st: XCURRENT md: NULL rsop: 0x00000000 tch: 2  flags: buffer_dirty gotten_in_current_mode redo_since_read  LRBA: [0xe9.21b.0] HSCN: [0x0000.00122965] HSUB: [1] RRBA: [0x0.0.0]…………………………………


    我們可以看到第一個BH (0x637F0720)的hash: [64be8000,65a5eab4]和第二個BH (0x64BE8000)的hash:[65a5eab4,637f0720]。這裏記錄的就是指向前一個buffer header和後一個buffer header的指針。這裏,我們看到第一個BH所指向的後一個buffer header的指針是65a5eab4,而第二個BH所指向的前一個buffer header的指針也是65a5eab4,說明這兩個buffer header是在同一個hash chain上。同樣的,我們還可以看到類似結構的lru、ckptq、fileq,這些都是管理buffer header的一些鏈表結構

  然後,我們來看我們創建的buffer_test表所對應的buffer header。  首先,我們看到class,表示該buffer header所對應的數據塊的類型,具體的值與含義的對應爲:

1=data block;

2=sort block;

3=save undo block;

4=segment header;

5=save undo header;

6=free list;

7=extent map;

8=1st level bmb;

9=2nd level bmb;

10=3rd level bmb;

11=bitmap block;

12=bitmap index block;

13=unused;

14=undo header;

15=undo block。

我們可以看到與buffer_test表相關buffer header有兩個:一個是4(segment header),另一個是1(data block)。
 
  然後,我們看到rdba,這表示buffer header所對應的數據塊在磁盤數據文件上的地址。我們可以看到class爲1的buffer header的rdba爲0x0180b00a (6/45066)。十六進制數。說明該數據塊的位置是6號文件的45066號block裏。018表示數據文件號乘以4,而b00a表示數據塊的號。

上面一句算法是錯的,正確的算法是這樣的:

這個RDBA由,rfile# + block#組成的
相對文件號10位 block號22位

0x0180b00a解碼以後就是:

0000 0001 1000 0000 1011 0000 0000 1010前10位代表rfile#
也就是

0110 = 6
00 0000 1011 0000 0000 1010 = 45066

SQL> select to_number('018','xxx')/4 as file#,to_number('b00a','xxxx') 
as block# from dual;     FILE#     BLOCK#---------- ----------         6      45066

我們看到,該buffer header指向的就是6號文件裏的45066號數據塊。我們可以再來看看錶buffer_test
裏的rowid所告訴我們的文件號以及數據塊號,從下面可以看到,結果是一樣的。

SQL> select id,dbms_rowid.rowid_relative_fno(rowid) as file#,  2  dbms_rowid.rowid_block_number(rowid) as block# from cost.buffer_test;        ID      FILE#     BLOCK#---------- ---------- ----------         1          6      45066

關於ROWID 和 rdba 參考:http://blog.csdn.net/changyanmanman/article/details/7486103
  我們可以來看一下st,這表示buffer cache所指向的數據塊的狀態。一共有六種狀態:

FREE(0)=可以被重用的數據塊;

XCURRENT(1)=實例以排他方式獲取的當前模式數據塊;

SCURRENT(2)=可以與其他實例共享的當前模式數據塊;

CR(3)=作爲一致性讀鏡像的數據塊,永遠不會被寫入磁盤;

READING(4)=正在從磁盤讀出的數據塊;

MRECOVERY(5)=正在進行介質恢復的數據塊;

IRECOVERY(6)=正在進行實例恢復的數據塊。

從狀態說明中我們可以看到,現在表buffer_test的數據塊都是當前模式的數據塊。我們可以來構造一個CR狀態的數據塊。
1、分別建立兩個session,在一個session中,執行:

SQL> update buffer_test set id=2 where id=1;

2、不要提交,然後在另外一個session中,執行:


SQL> select * from buffer_test;        ID----------         1


 3、 然後我們轉儲buffer header後,到跟蹤文件中找到obj爲7087的記錄,可以看到類似如下的內容。可以看到該buffer header的狀態就是CR。

…………………………………BH (0x63FFBBC8) file#: 6 rdba: 0x0180b00a (6/45066) class 1 ba: 0x63F5C000…………………………………  ckptq: [NULL] fileq: [NULL]  st: CR md: NULL rsop: 0x00000000 tch: 0…………………………………


  另外,我們還可以看到tch,就是表示該數據塊被掃描的次數。 以上這些是轉儲出來的內容。Oracle還提供了視圖來顯示buffer header的內容,這就是X$BH這個視圖就是把轉儲到平面文件以後所看到的諸如hash、st、tch等的值以列的方式呈現出來。這裏就不做過多的介紹了,有興趣的話,可以將該視圖取出的結果與轉儲出來的文件進行比較,就可以知道每一列的含義。

3.buffer cache的內部管理機制
3.1 在buffer cache中獲取所需要的數據塊的過程
    當前臺進程發出SELECT或者其他DML語句時,oracle根據SQL語句的執行計劃所找到的數據塊(根據查詢索引或者全表掃描,找到要查詢的數據條目所在的塊,一般根據ROWID等信息,舉個列子:一個簡單的select語句,條件是empno=7788,根據索引,oracle會知道7788這個條目的ROWID,然後就能知道數據塊的位置,到底是在那個表空間,那個對象,那個數據文件中),會構造一個名爲數據塊描述(buffer descriptor)的內存結構。該buffer descriptor位於session的PGA中,所包含的內容主要是數據塊所在的物理地址(根據ROWID信息的第33-64bit構造出rbda)、數據塊的類型、數據塊所屬對象的object id等信息。
    隨後,oracle會把對數據塊請求的鎖定模式以及所構造出來的buffer descriptor傳入專門搜索數據塊的函數中。在該函數中,oracle根據buffer descriptor所記錄的信息,應用hash算法以後,得到要找的數據塊所處的hash bucket,也就是確定該數據塊在哪條hash chain上。然後,oracle進入該hash chain,從上面所掛的第一個buffer header開始搜索,一直搜索到最後一個buffer header。

 在hash chain上搜索的邏輯如下:
  1) 比較buffer header上所記錄的數據塊的地址(rdba),如果不符合,則跳過該buffer header。
  2) 跳過狀態爲CR的buffer header。(說明有別的進程正在進行一致性讀,所以才構造了這個cr塊,如果我也要找這個塊的原塊,我需要自己再重新構造一個新的cr塊,不會使用這個舊的cr塊,如果我不是找這個塊的原塊,那我不需要構造,所以這兩種情況下都是跳過cr塊)
  3) 如果遇到狀態爲READING(正在從磁盤上讀出的數據塊)的buffer header,則等待,一直等到該buffer header的狀態改變以後再比較所記錄的數據塊的地址是否符合。(說不定是之前的查詢,有可能就是這條sql語句,也有可能是之前的(自己用戶或者其他用戶的sql)語句,正好也需要讀這個塊內的數據,正在往內存裏讀,這下我就可以直接用前輩的努力就可以了
  4) 如果發現數據塊地址符合的buffer header,則查看該buffer header是否位於正在使用的列表上,如果是位於正在使用的列表上,則判斷已存在的鎖定模式與當前所要求的鎖定模式是否兼容,如果是兼容的,則返回該buffer header所記錄的數據塊地址,並將當前進程號放入該buffer header所處的正在使用的列表上(在開始介紹buffer header的時候有這一項,還有等待該buffer header的進程列表)。
  5) 如果發現鎖定模式不兼容,則根據找到的buffer header所指向的數據塊的內容,構建一個新的、內容一樣的、數據塊狀態爲XCURRENT(實例以排他方式獲取的當前模式數據塊)的複製數據塊,並且構造一個狀態爲CR的buffer header,同時該buffer header指向所新建立的複製數據塊。然後,返回該複製數據塊的地址,並將當前進程號放入該buffer header所處的正在使用的列表上。
  6) 如果比較完整個hash chain以後還沒發現所要找的buffer header,則從磁盤上讀取數據文件。並將讀取到的數據塊所對應的buffer header掛到hash chain上。(之前我有個疑問,到底是在生成執行計劃的時候就開始往內存裏讀取數據塊?還是在用buffer descriptor 比較地址的時候讀?現在知道了,是在用buffer descriptor 比較地址的時候往裏讀取的)

3.2 LRU和LRUW鏈表結構及其管理機制
  3.2.1 LRU和LRUW鏈表結構概述
    在前面,我們已經知道了oracle是如何在hash chain中搜索要找的數據塊所對應的buffer header的過程,我們也知道如果在hash chain上沒有找到所要的buffer header時,oracle會發出I/O調用,到磁盤上的數據文件中獲取數據塊,並將該數據塊的內容拷貝一份到buffer cache中的內存數據塊裏(順帶提一句,內存數據塊通常叫做buffer,而數據文件裏的數據塊通常叫做block,二者是一個意思)。這個時候,假如buffer cache是空的,比較好辦,直接拿一個空的內存數據塊來用即可。但是如果buffer cache中的內存數據塊全都被用掉了,沒有空的內存數據塊了,怎麼辦?應該重新使用哪一個內存數據塊?當然我們可以一個一個的比較內存數據塊與其對應在數據文件中的數據塊的內容是否一致,如果一致則可以將該數據塊拿來,將其內容清空,然後拷貝上當前數據塊的內容;如果不一致,則跳過,再找下一個。毫無疑問,這種方式效率低下。爲了高效的管理buffer cache中的內存數據塊,oracle引入了LRU和LRUW等鏈表等結構。

   在buffer cache中,最耳熟能詳的鏈表可能就是LRU鏈表了。在前面描述buffer cache結構的圖上,也可以看到有兩個鏈表:LRU和LRUW。在介紹LRU和LRUW前,先說明幾個概念。
    1) 髒數據塊(dirty buffer):buffer cache中的內存數據塊的內容與數據文件中的數據塊的內容不一致。
    2) 可用數據塊(free buffer):buffer cache中的內存數據塊爲空或者其內容與數據文件中的一致。注意,可用數據塊不一定是空的。
    3) 釘住的數據塊(ping buffer):當前正在更新的內存數據塊。
    4) 數據庫寫進程(DBWR):這是一個很底層的數據庫後臺進程。既然是後臺進程,就表示該進程是不能被用戶調用的。由oracle內置的一些事件根據需要啓動該進程,該進程用來將髒數據塊寫入磁盤上的數據文件。
    LRU表示Least Recently Used,也就是指最近最少使用的buffer header鏈表。LRU鏈表串連起來的buffer header都指向可用數據塊。而LRUW則表示Least Recently Used Write,也叫做dirty list,也就是髒數據塊鏈表,LRUW串起來的都是修改過但是還沒有寫入數據文件的內存數據塊所對應的buffer header。某個buffer header要麼掛在LRU上,要麼掛在LRUW上,不能同時掛在這兩個鏈表上。
     隨着硬件技術的發展,電腦的內存越來越大。buffer cache也是越來越大,只用一條LRU和一條LRUW來管理buffer header已經不夠用了。同時oracle還引入了多個DBWR後臺進程來幫助將buffer cache中的髒數據塊寫入數據文件,顯然,多個DBWR後臺進程都去掃描相同的LRUW鏈表會引起爭用。爲此oracle引入了working set(工作集)的概念。每個working set都具有它自己的一組LRU和LRUW鏈表。每個working set都由一個名爲“cache buffers lru chain”的latch(也叫做lru latch)來管理,所以從這個意義上說,每一個lru latch就是一個working set。而每個被加載到buffer cache的buffer header都以輪詢的方式掛到working set上去。也就是說,當buffer cache加載一個新的數據塊時,其對應的buffer header會去找一個可用的lru latch(找這個工作集中的lru列表,將新加載進來的數據塊掛到LRU列表上),如果沒有找到,則再找下一個lru latch,直到找到爲止。如果輪詢完所有的lru latch也沒能找到可用的lru latch,該進程只有等待latch free等待事件,同時出現在v$session_wait中,並增加“latch misses”。如果啓用了多個DBWR後臺進程的話,每個DBWR進程都會對應一個不同的working set,而且每個DBWR只會處理分配給它的working set,不會處理其他的working set。

    我們已經知道一個lru latch就是一個working set,那麼working set的數量也就是lru latch的數量。而lru latch的數量是由一個隱藏參數:_db_block_lru_latches決定的。該參數缺省值爲DBWR進程的數量×8
 該參數最小必須爲8,如果強行設置比8小的數值,oracle將忽略你設置的值,而使用8作爲該參數值。

1SQL> alter system set "_db_block_lru_latches"=1 scope=spfile;
2SQL> startup force
3SQL> show parameter _db_block
4NAME                                 TYPE        VALUE
5------------------------------------ ----------- ------------------------------
6_db_block_lru_latches                integer     8

3.2.2 深入LRU鏈表
  我們已經知道LRU鏈表是用來查找可以重用的內存數據塊的,那麼oracle是怎麼使用LRU鏈表的呢?這裏需要分爲8i之前和8i以後兩種情況。
   在8i之前,我們舉一個例子。假設buffer cache只能容納4個數據塊,同時只有一個hash chain和一個LRU。當數據庫剛剛啓動,buffer cache是空的。這時前臺進程發出SELECT語句獲取數據塊時,oracle找一個空的內存數據塊,並將其對應的buffer header掛到hash chain上。同時,oracle還會把該buffer header掛到LRU的最尾端。隨後前臺進程又發出SELECT語句,這時所找到的buffer header在LRU上會掛到前一個buffer header的後面,也就是說第二次SELECT語句所找到的buffer header現在變成了LRU的最尾端了。假設發出4句SELECT以後找到了4個buffer header,從而用完了所有的buffer cache空間。這個時候的LRU可以用下圖二來表示。
 


   這個時候,發來了第五句SELECT語句。這時的buffer cache裏已經沒有空的內存數據塊了。但是既然需要容納下第五個數據塊,就必然需要找一個可以被替換(後面會看到類似犧牲、重用的字樣,它們和替換都是一個意思)的內存數據塊。這個內存數據塊會到LRU上去找。按照oracle設定的最近最少使用的原則,位於LRU最尾端的BH1將成爲犧牲者,oracle會把該BH1對應的內存數據塊的內容清空,並將當前第五句SQL所獲得的數據塊的內容拷貝進去。這個時候,BH1就成了LRU的首端,而BH2則成爲了LRU的尾端。如下圖三所示。在這種方式下,經常被訪問的數據塊可以一直靠近LRU的首端,也就保證了這些數據塊可以儘可能的不被替換掉,從而保證了訪問的效率。


                    圖三
  到了8i以後,oracle引入了一種更加複雜的機制來管理LRU上的數據塊。8i以後,LRU和LRUW鏈表都具有兩個子鏈表,分別叫做輔助鏈表和主鏈表。同時還對buffer header增加了一個屬性:touch數量,也就是每個buffer header曾經被訪問過的次數,來對LRU鏈表進行管理。oracle每訪問一次buffer header,就會將該buffer header上的touch數量增加1,因此,touch數量“近似”的體現了某個內存數據塊總共被訪問的次數。注意,這只是近似,並不精確。因爲touch的增加並沒有使用latch來管理併發性。這只是一個大概值,表示趨勢的,不用百分百的精確。
還是用上面的這個例子來說明。還是假設buffer cache只能容納4個數據塊,同時只有一個hash chain和一個LRU(確切的說應該是一對LRU主鏈表和輔助鏈表)。讀入第一個數據塊時,該數據塊對應的buffer header會掛到LRU輔助鏈表(注意,這裏是輔助鏈表,而不是主鏈表)的最末端,同時touch數量爲1。讀取第二個不同的數據塊時,該數據塊對應的buffer header會掛到前一個buffer header的後面,從而位於LRU輔助鏈表的最末端,同樣touch爲1。假設4個數據塊全都用完以後的LRU鏈表可以用下圖四描述。每個buffer header的touch數量都爲1。



從上圖中我們可以看到輔助LRU鏈表都掛滿了,而主LRU鏈表還是空的。這個時候,前臺發出第五句SQL語句,要求返回指定的數據塊。這時,oracle發現buffer cache裏已經沒有空的內存數據塊了,於是從輔助LRU鏈表的尾部開始掃描,也就是從BH1開始掃描,以查找可以被替代的數據塊。掃描的過程中按照下面的邏輯來選擇被犧牲的(也就是可以被替代的)數據塊:


1) 如果被掃描到的buffer header的touch數量小於隱藏參數_db_aging_hot_criteria(該參數缺省爲2)的值,則選中該buffer header作爲犧牲者,並立即返回該buffer header所含有的數據塊的地址。
2) 如果當前buffer header的touch數量大於_db_aging_hot_criteria的值,則不會使用該buffer header。但是如果當前的_db_aging_stay_count的值小於_db_aging_hot_criteria的值,則會將當前該buffer header的touch值賦值給_db_aging_stay_count;否則將當前buffer header的touch數量減掉一半。

  按照上述的邏輯,這時將選出BH1作爲犧牲者(因爲BH1的touch數量爲1,小於_db_aging_hot_criteria
的值),並將其對應的內存數據塊的內容清空,同時將當前第五個數據塊的內容拷貝進去。但是這裏要注意,這個時候該BH1在LRU鏈表上的位置並不會發生任何的變化(這裏是插入了新的數據塊的內容,所以touc的數量沒有變化,下面是返回已經有的數據塊,所以touch的數量加1了,這樣就保證了touch爲1的數據塊即不常用的數據塊一直在輔助鏈表,而不會跑到主lru鏈表上)。而不會像8i之前的那樣,BH1變成LRU鏈表的首端。
接下來,前臺發來了第六句和第七句SQL,分別要返回與第五句和第四句SQL一樣的數據塊,也就是要返回當前的BH1和BH4。這個時候,oracle會增加BH1和BH4的touch數量,同時將該BH1和BH4從輔助LRU鏈表上摘下,轉移到主LRU鏈表的中間位置。可以用下圖五描述。
 


圖五


   這個時候,如果發來了第八句SQL,要求返回與第三句SQL相同的數據塊,也就是當前的BH3,則這時該BH3會插入主LRU鏈表上的BH1和BH4中間,注意每次向主LRU列表插入buffer header時都是向中間位置插入。如果發來了第九句SQL要求返回BH2,則我們可以知道,BH2會轉移到主LRU鏈表的中間。這個時候,輔助LRU鏈表就空了,沒有buffer header了。

    這時,如果又發來第十句SQL,要求返回一個新的、buffer cache中不存在所需內容的數據塊時。oracle會先掃描輔助LRU鏈表,發現上面沒有任何的buffer header時,則必須掃描主LRU鏈表。從尾部開始掃描,採用前面說到的與掃描輔助LRU鏈表相同的規則挑選犧牲者。挑出的可以被替代的buffer header將從主LRU鏈表上摘下,放入輔助LRU鏈表。

    從上面所描述的buffer header在輔助LRU鏈表和主LRU鏈表之間交替的過程中,我們可以看出,oracle改進LRU鏈表的管理方式的目的,就是想千方百計的能夠將多次被訪問的數據塊保留在內存裏,同時又要平衡有限的內存資源。這種方式相比較8i之前而言,無疑是進步很多的。在8i之前中,某個數據塊可能只會被訪問一次,但是就這麼一次的訪問就將該數據塊放到了LRU的首端,從而可能就擠掉了一個LRU上不是那麼經常被訪問,但是也會多次訪問的數據塊。而8i以後,將訪問一次的數據塊和訪問一次以上的數據塊徹底分開,而且查找可用數據塊時,始終都是從輔助LRU鏈表開始掃描。實際上也就使得越傾向於只訪問一次的數據塊越快的從內存中清理出去。

3.2.3 LRUW鏈表管理

   
從前面我們已經知道SELECT語句讀取數據塊到buffer cache的過程。那麼我們必然會產生另外一個疑問,就是當使用DML等語句修改了buffer cache裏的內存數據塊以後的過程是怎樣的?實際上,爲了能夠最有效、安全的完成將內存數據塊寫入數據文件的過程,oracle提供了比讀取數據塊更爲複雜的機制。

  我們已經知道LRUW表示髒數據塊鏈表,該鏈表上的buffer header指向的都是已經從LRU鏈表上摘下來、其對應的內存數據塊裏的內容已經被修改、但是還沒有被寫入數據文件的內存數據塊。在這些髒數據塊在能夠被重用之前,它們必須要被DBWR寫入磁盤。從8i以後,LRUW鏈表同樣包含兩個子鏈表:輔助LRUW鏈表和主LRUW鏈表。那麼LRUW鏈表是如何產生buffer header的呢?oracle又是如何對其進行管理的呢?

  我們還是接着上面圖五所示的例子來說明。假設這個時候,前臺用戶發出DML語句,要求修改BH2所指向的內存數據塊。這時,按順序發生下面的動作:
1) oracle會將BH2從輔助LRU鏈表上摘下,同時插入主LRU鏈表的中間,也就是插入BH1和BH4中間,同時增加BH2的touch的數量。(與selectBH2的效果一樣, 都會使要查找的塊從輔助鏈表上摘下,放入主LRU鏈表)
2) 將該BH2的標記設置爲釘住(ping)。(這個在select語句中沒有說明,不知道有沒有,應該是沒有的)
3) 更新BH2對應的內存數據塊的內容。
4) 更新完以後,取消釘住的標記(在LRU列表上進行第一次更新)。
5) 將BH2從主LRU鏈表轉移到主LRUW鏈表上
6) 如果這個時候又有進程發出更新BH2所對應的內存數據塊的內容,則BH2再次被釘住,更新,取消釘住(可以在主LRUW列表上繼續更新)。
7) DBWR啓動以後,在掃描主LRUW鏈表時會將BH2轉移到輔助LRUW鏈表上(必須轉移到輔助LURW列表才能寫入到磁盤)。
8) DBWR將輔助LRUW鏈表上的BH2對應的數據塊寫入數據文件。
9) 確認成功寫入數據文件以後,將BH2從輔助LRUW鏈表上轉移到輔助LRU鏈表上(返回到輔助LUR列表)。

  從上面的描述中,我們可以看到,主LRUW鏈表上包含的buffer header要麼是已經更新完了的數據塊,要麼是被釘住正在更新的數據塊。而當DBWR進程啓動以後,它會掃描主LRUW鏈表,並跳過正在被釘住更新的buffer header,而將已經更新完了的buffer header從主LRUW鏈表上摘除,並轉移到輔助LRUW鏈表上去。

掃描完主LRUW鏈表,或掃描的buffer header的個數達到一定限度時,DBWR會轉到輔助LRUW上,將輔助LRUW上面的buffer header所對應的數據塊寫入數據文件。所以說,對於輔助鏈表上的buffer header來說,要麼是正在等待被寫入的;要麼就是已經發出寫入請求,正在寫入而還沒寫完的這裏要注意的是,buffer header進入LRUW鏈表,是從尾端進入;而DBWR掃描LRUW鏈表時,則是從首端開始。


  順帶提一句,這裏將主LRUW鏈表和輔助LRUW鏈表分開,主要就是爲了提高DBWR在主LRUW鏈表上掃描的效率。如果只有主LRUW鏈表而沒有輔助LRUW鏈表的話,勢必造成三種類型buffer header交織在LRUW鏈表上:

1)正在被釘住更新的buffer header;

2)已經更新完,而正在等待被寫入數據文件的buffer header;

3)已經發出寫請求,正在寫而尚未寫完的buffer header。

在這種情況下,必然造成DBWR爲了找到第二種類型的buffer header而需要掃描不該掃描的第三種類型的buffer header。(把第三種已經發出寫請求,但是還沒有寫完的BH放到了輔助LRUW列表裏,避免了掃描第二種已經更新完成,等待被寫入的的BH)

3.2.4 DBWR進程
  我們已經知道DBWR進程負責將髒數據塊寫入磁盤。它是一個非常重要的進程,在後臺進程中的sid爲2,在PMON進程啓動以後隨即啓動。
 

SQL> select c.sid,a.name,a.description
  2  from v$bgprocess a ,v$process b , v$session c
  3  where a.paddr=b.addr
  4    and b.addr = c.paddr;
       SID NAME  DESCRIPTION
---------- ----- -------------------------------------------
         1 PMON  process cleanup
         2 DBW0  db writer process 0
         3 LGWR  Redo etc.
         4 CKPT  checkpoint
………………………………………………………………………………


  隨着內存的不斷增加,1個DBWR進程可能不夠用了。所以從8i起,我們可以爲系統配置多個DBWR進程。初始化參數:db_writer_processe決定了啓動多少個DBWR進程。每個DBWR進程都會分配一個lru latch,也就是說每個DBWR進程對應一個working set。因此oracle建議配置的DBWR進程的數量應該等於lru latch的數量,同時應該小於CPU的數量。系統啓動時,就確定好了working set與DBWR進程的對應關係,每個DBWR進程只會將分配給自己的working set上的髒數據塊寫入數據文件。
  DBWR作爲一個後臺進程,只有在某些條件滿足了纔會觸發。這些條件包括:

1) 當進程在輔助LRU鏈表和主LRU鏈表上掃描以查找可以覆蓋的buffer header時,如果已經掃描的buffer header的數量到達一定的限度(由隱藏參數:_db_block_max_scan_pct決定)時,觸發DBWR進程。_db_block_max_scan_pct表示已經掃描的buffer header的個數佔整個LRU鏈表上buffer header總數的百分比。這時,搜索可用buffer header的進程掛起,在v$session_wait中表現爲等待“free buffer wait”事件,同時增加v$sysstat中的“dirty buffers inspected”的值。

2) 當DBWR在主LRUW鏈表上查找已經更新完而正在等待被寫入數據文件的buffer header時,如果找到的buffer header的數量超過一定限度(由隱藏參數:_db_writer_scan_depth_pct決定)時,DBWR就不再繼續往下掃描了,而轉到輔助LRUW鏈表上將其上的髒數據塊寫入數據文件。_db_writer_scan_depth_pct表示已經掃描的髒數據塊的個數佔整個主LRUW鏈表上buffer header總數的百分比。

3) 如果主LRUW鏈表和輔助LRUW鏈表上的髒數據塊的總數超過一定限度,也將觸發DBWR進程。該限度由隱藏參數:_db_large_dirty_queue決定。
4) 發生增量檢查點(incremental checkpoint)或完全檢查點(complete checkpoint)時觸發DBWR。
5) 每隔三秒鐘啓動一次DBWR。
6) 將表空間設置爲離線(offline)狀態時觸發DBWR。
7) 發出命令:alter tablespace … begin backup,從而將表空間設置爲熱備份狀態時觸發DBWR。
8) 將表空間設置爲只讀狀態時,觸發DBWR。
9) 刪除對象時(比如刪除某個表)會觸發DBWR。

  當DBWR要寫髒數據塊時,並不是說立即將所有的髒數據塊都同時寫入磁盤。爲了儘量減少物理的
I/O的次數,DBWR會將要寫的髒數據塊所對應的buffer header拷貝到一個名爲
批量寫(write batch)的結構中。每個working set所對應的DBWR進程都可以向該結構裏拷貝buffer header。當write batch的buffer header的個數達到一定限額時,纔會發生實際的I/O,從而將髒數據塊寫入磁盤。這個限額爲硬件平臺所能支持的同時併發的異步I/O的最大數量。8i之前是可以用隱藏參數(_db_block_write_batch)來控制這個限額的。但是8i以後,取消了該參數,而由oracle自己來計算。

3.2.5 DBWR、CKPT、LGWR進程之間的合作
  將內存數據塊寫入數據文件實在是一個相當複雜的過程,在這個過程中,首先要保證安全。所謂安全,就是在寫的過程中,一旦發生實例崩潰,要有一套完整的機制能夠保證用戶已經提交的數據不會丟失;其次,在保證安全的基礎上,要儘可能的提高效率。衆所周知,I/O操作是最昂貴的操作,所以應該儘可能的將髒數據塊收集到一定程度以後,再批量寫入磁盤中。
 直觀上最簡單的解決方法就是,每當用戶提交的時候就將所改變的內存數據塊交給DBWR,由其寫入數據文件。這樣的話,一定能夠保證提交的數據不會丟失。但是這種方式效率最爲低下,在高併發環境中,一定會引起I/O方面的爭用。oracle當然不會採用這種沒有擴展性的方式。oracle引入了CKPT和LGWR這兩個後臺進程,這兩個進程與DBWR進程互相合作,提供了既安全又高效的寫髒數據塊的解決方法。

 用戶進程每次修改內存數據塊時,都會在日誌緩衝區(redo buffer)中構造一個相應的重做條目(redo entry),該重做條目描述了被修改的數據塊在修改之前和修改之後的值。而LGWR進程則負責將這些重做條目寫入聯機日誌文件。只要重做條目進入了聯機日誌文件,那麼數據的安全就有保障了,否則這些數據都是有安全隱患的。LGWR 是一個必須和前臺用戶進程通信的進程。LGWR 承擔了維護系統數據完整性的任務,它保證了數據在任何情況下都不會丟失。

  LGWR將重做條目寫入聯機日誌文件的情況分兩種:後臺寫(background write)同步寫(sync write)。觸發後臺寫的條件有四個:

1)每隔三秒鐘,LGWR啓動一次;

2)在DBWR啓動時,如果發現髒數據塊所對應的重做條目還沒有寫入聯機日誌文件,則DBWR觸發LGWR進程並等待LRWR寫完以後纔會繼續;

3)重做條目的數量達到整個日誌緩衝區的1/3時,觸發LGWR;

4)重做條目的數量達到1MB時,觸發LGWR。

而觸發同步寫的條件就一個:當用戶提交(commit)時,觸發LGWR。


  假如DBWR在寫髒數據塊的過程中,突然發生實例崩潰。我們已經知道,用戶提交時,oracle是不一定會把提交的數據塊寫入數據文件的。那麼實例崩潰時,必然會有一些已經提交但是還沒有被寫入數據文件的內存數據塊丟失了。當實例再次啓動時,oracle需要利用日誌文件中記錄的重做條目在buffer cache中重新構造出被丟失的數據塊,從而完成前滾和回滾的工作,並將丟失的數據塊找回來。於是這裏就存在一個問題,就是oracle在日誌文件中找重做條目時,到底應該找哪些重做條目?換句話說,應該在日誌文件中從哪個起點開始往後應用重做條目?注意,這裏所指的日誌文件可能不止一個日誌文件。

  因爲oracle需要隨時預防可能的實例崩潰現象,所以oracle在數據庫的正常運行過程中,會不斷的定位這個起點,以便在不可預期的實例崩潰中能夠最有效的保護並恢復數據。同時,這個起點的選擇非常有講究。首先,這個起點不能太靠前,太靠前意味着要處理很多的重做條目,這樣會導致實例再次啓動時所進行的恢復的時間太長;其次,這個起點也不能太靠後,太靠後說明只有很少的髒數據塊沒有被寫入數據文件,也就是說前面已經有很多髒數據塊被寫入了數據文件,那也就意味着只有在DBWR啓動的很頻繁的情況下,才能使得buffer cache中所殘留的髒數據塊的數量很少。但很明顯,DBWR啓動的越頻繁,那麼所佔用的寫數據文件的I/O就越嚴重,那麼留給其他操作(比如讀取buffer cache中不存在的數據塊等)的I/O資源就越少。這顯然也是不合理的。

  從這裏也可以看出,這個起點實際上說明了,在日誌文件中位於這個起點之前的重做條目所對應的在buffer cache中的髒數據塊已經被寫入了數據文件,從而在實例崩潰以後的恢復中不需要去考慮。而這個起點以後的重做條目所對應的髒數據塊實際還沒有被寫入數據文件,如果在實例崩潰以後的恢復中,需要從這個起點開始往後,依次取出日誌文件中的重做條目進行恢復。考慮到目前的內存容量越來越大,buffer cache也越來越大,buffer cache中包含幾百萬個內存數據塊也是很正常的現象的前提下,如何才能最有效的來定位這個起點呢?

 爲了能夠最佳的確定這個起點,oracle引入了名爲CKPT的後臺進程,通常也叫作檢查點進程(checkpoint process)。這個進程與DBWR共同合作,從而確定這個起點。同時,這個起點也有一個專門的名字,叫做檢查點位置(checkpoint position)。
  oracle爲了在檢查點的算法上更加的具有可擴展性(也就是爲了能夠在巨大的buffer cache下依然有效工作),引入了檢查點隊列(checkpoint queue),該隊列上串起來的都是髒數據塊所對應的buffer header。

而DBWR每次寫髒數據塊時,也是從檢查點隊列上掃描髒數據塊,並將這些髒數據塊實際寫入數據文件的。當寫完以後,DBWR會將這些已經寫入數據文件的髒數據塊從檢查點隊列上摘下來。這樣即便是在巨大的buffer cache下工作,CKPT也能夠快速的確定哪些髒數據塊已經被寫入了數據文件,而哪些還沒有寫入數據文件,顯然,只要在檢查點隊列上的數據塊都是還沒有寫入數據文件的髒數據塊。

而且,爲了更加有效的處理單實例和多實例(RAC)環境下的表空間的檢查點處理,比如將表空間設置爲離線狀態或者爲熱備份狀態等,oracle還專門引入了文件隊列(file queue)文件隊列的原理與檢查點隊列是一樣的,只不過每個數據文件會有一個文件隊列,該數據文件所對應的髒數據塊會被串在同一個文件隊列上;

同時爲了能夠儘量減少實例崩潰後恢復的時間,oracle還引入了增量檢查點(incremental checkpoint),從而增加了檢查點啓動的次數。如果每次檢查點啓動的間隔時間過長的話,再加上內存很大,可能會使得恢復的時間過長。因爲前一次檢查點啓動以後,標識出了這個起點。然後在第二次檢查點啓動的過程中,DBWR可能已經將很多髒數據塊已經寫入了數據文件,而假如在第二次檢查點啓動之前發生實例崩潰,導致在日誌文件中,所標識的起點仍然是上一次檢查點啓動時所標識的,導致oracle不知道這個起點以後的很多重做條目所對應的髒數據塊實際上已經寫入了數據文件,(我的理解:前滾恢復到日誌的最後一條重做條目,發現沒有檢查點,接着再回滾,回到上一個檢查點對應的日誌條目)從而使得oracle在實例恢復時再次重複的處理一遍,效率低下,浪費時間。

  上面說到了有關CKPT的兩個重要的概念:檢查點隊列(包括文件隊列)增量檢查點

檢查點隊列在我們上面轉儲出來的buffer header裏可以看到,就是類似ckptq: [65abceb4,63bec66c]和fileq: [65abcfbc,63becd10]的結構,記錄的同樣都是指向前一個buffer header和指向後一個buffer header的指針。這個隊列上面掛的也是髒數據塊對應的buffer header鏈表,但是它與LRUW鏈表不同。檢查點隊列上的buffer header是按照數據塊第一次被修改的時間的先後順序來排列的。越早修改的數據塊的buffer header排在越前面,同時如果一個數據塊被修改了多次的話,在該鏈表上也只出現一次。而且,檢查點隊列上的buffer header還記錄了髒數據塊在第一次被修改時,所對應的重做條目在重做日誌文件中的地址,也就是RBA(Redo Block Address)。同樣在轉儲出來的buffer header中可以看到類似LRBA: [0xe9.229.0]的結構,這就是RBA,L表示Low,也就是第一次被修改的時候的RBA。但是注意,在檢查點隊列上的buffer header,並不表示一定會有一個對應的RBA,比如控制文件重做(controlfile redo)就不會有相應的RBA。對於沒有對應RBA的buffer header來說,在檢查點隊列上始終處於最尾端,其優先級永遠比有RBA的髒數據塊的buffer header要低。8i以前,每個working set都有一個檢查點隊列以及多個文件隊列(因爲一個數據文件對應一個文件隊列);而從8i開始,每個working set都有兩個檢查點隊列,每個檢查點都會由checkpoint queue latch來保護。

   而增量檢查點是從8i開始出現的,是相對於8i之前的完全檢查點(complete checkpoint)而言的。完全檢查點啓動時,會標識出buffer cache中所有的髒數據塊,然後啓動DBWR進程將這些髒數據塊寫入數據文件。8i之前,日誌切換的時候會觸發完全檢查點。而到了8i及以後,完全檢查點只有在兩種情況下才會被觸發:

1)發出命令:alter system checkpoint;

2)除了shutdown abort以外的正常關閉數據庫。

注意,這個時候,日誌切換不會觸發完全檢查點,而是觸發增量檢查點。8i所引入的增量檢查點每隔三秒鐘或發生日誌切換時啓動。它啓動時只做一件事情:找出當前檢查點隊列上的第一個buffer header,並將該buffer header中所記錄的LRBA(這個LRBA也就是checkpoint position了)記錄到控制文件中去。如果是由日誌切換所引起的增量檢查點,則還會將checkpoint position記錄到每個數據文件頭中。也就是說,如果這個時候發生實例崩潰,oracle在下次啓動時,就會到控制文件中找到這個checkpoint position作爲在日誌文件中的起點,然後從這個起點開始向後,依次取出每個重做條目進行處理。
上面所描述的概念,用一句話來概括,其實就是DBWR負責寫檢查點隊列上的髒數據塊,而CKPT負責記錄當前檢查點隊列的第一個數據塊所對應的的重做條目在日誌文件中的地址。

從這個意義上說,檢查點隊列比LRUW還要重要,LRUW主要就是區分出哪些數據塊是髒的,不可以被重用的。而到底應該寫哪些髒數據塊,寫多少髒數據塊,則還是要到檢查點隊列上才能確定的。
我們用一個簡單的例子來描述這個過程。假設系統中發生了一系列的事務,導致日誌文件如下所示:

事務號    數據文件號    block號      行號    列    值        RBA
T1        8            25             10      1     10        101
T1        7            623            12      2     a         102
T3        8            80             56      3     b         103
T3        9            98            124      7     e         104
T5        7            623            13      3     abc       105
Commit SCN#        timestamp                                  106
T123     8            876            322      10    89        107

這時,對應的檢查點隊列則類似如下圖六所示。我們可以看到,T1事務最先發生,所以位於檢查點
 

圖六


  隊列的首端,而事務T123最後發生,所以位於靠近尾端的地方。同時,可以看到事務T1和T5都更新了7號數據文件的623號數據塊。而在檢查點隊列上只會記錄該數據塊的第一次被更新時的RBA,也就是事務T1對應的RBA102,而事務T5對應的RBA105並不會被記錄。因爲根本就不需要在檢查點隊列上記錄。當DBWR寫數據塊的時候,在寫RBA102時,自然就把RBA105所修改的內容寫入數據文件了。日誌文件中所記錄的提交標記也不會體現在檢查點隊列上,因爲提交本身只是一個標記而已,不會涉及到修改數據塊。

   這時,假設發生三秒鐘超時,於是增量檢查點啓動。增量檢查點會將檢查點隊列的第一個髒數據塊所對應的RBA記錄到控制文件中去。在這裏,也就是RBA101會作爲checkpoint position記錄到控制文件中。
 然後,DBWR後臺進程被某種條件觸發而啓動。DBWR根據一系列參數及規則,計算出應該寫的髒數據塊的數量,從而將RBA101到RBA107之間的這5個髒數據塊寫入數據文件,並在寫完以後將這5個髒數據塊從檢查點隊列上摘除,而留下了4個髒數據塊在檢查點隊列上。如果在寫這5個髒數據塊的過程中發生實例崩潰,則下次實例啓動時,oracle會從RBA101開始應用日誌文件中的重做條目。

 


圖七



  而在9i以後,在DBWR寫完這5個髒數據塊以後,還會在日誌文件中記錄所寫的髒數據塊的塊號。如下圖所示。這主要是爲了在恢復時加快恢復的速度。
 



  這時,又發生三秒鐘超時,於是增量檢查點啓動。這時它發現checkpoint position爲RBA109,於是將RBA109寫入控制文件。如果接着發生實例崩潰,則oracle在下次啓動時,就會從RBA109開始應用日誌。(待續..........)

4. buffer cache的優化

4.1 buffer cache的設置優化


   buffer cache的設置隨着oracle版本的升級而不斷變化。8i下使用db_block_buffers來設置,該參數表示buffer cache中所能夠包含的內存數據塊的個數;9i以後使用db_cache_size來設置,該參數表示buffer cache的總共的容量,可以用字節、K、M爲單位來進行設置。而到了10g以後則更加簡單,甚至可以不用去單獨設置buffer cache的大小。因爲10g引入了ASMM(Automatic Shared Memory Management)這樣一個可以進行自我調整的組件,該組件可以自動調整shared pool size、db cache size等SGA中的組件。只需要設置sga_target參數,則其他組件就能夠根據系統的負載和歷史信息自動的調整各個部分的大小。要啓動ASMM,只需要設置statistics_level爲typical或all

   oracle8.0以前只能設置一種buffer cache,而從8.0以後,oracle提供了三種類型的buffer cache,分別是default、keep、recyle。keep和recycle是可選的,default必須存在。8i以前使用db_block_buffer設置default、buffer_pool_keep設置keep、buffer_pool_recycle設置recyle。

而8i以後使用db_cache_size設置default

db_keep_cache_size設置keep、

db_recycle_cache_size設置recycle。

10g不能自動設置db_keep_cache_size和db_recycle_cache_size,必須手工設置。
同時,8i以前,這三種buffer cache是獨立指定的,互不制約。而8i以後,這三種buffer cache是有相互制約關係的。如果指定了keep和recycle的buffer cache,則default類型的buffer cache的大小就是db_cache_size - buffer_pool_keep - buffer_pool_recycle。

    通常將經常訪問的對象放入keep類型的buffer cache裏,而將不常訪問的大表放入recycle類型的buffer cache裏。其他沒有指定buffer cache類型的對象都將進入default類型的buffer cache裏。爲對象指定buffer cache類型的方法如下:

SQL> create table test (n number) storage (buffer_pool keep);
SQL> alter table test storage (buffer_pool recycle);

  如果沒有指定buffer_pool短語,則表示該對象進入default類型的buffer cache。

  這裏要說明的是,從名字上看,很容易讓人誤以爲這三種buffer cache提供了三種不同的管理內存數據塊的機制。但事實上,它們之間在管理和內部機制上沒有任何的區別。它們僅僅是爲DBA們提供了一個選擇,就是能夠將數據庫對象分成“非常熱的”、“比較熱的”和“不熱的”這三種類型。因爲數據庫中總會存在一些“非常熱”的對象,它們頻繁的被訪問。而如果某個時候系統偶爾做了一次大表的全表掃描,就有可能將這些對象清除出內存。爲了防止這種情況的發生,我們可以設置keep類型的buffer cache,並將這種對象都移入keep buffer cache中。同樣的,數據庫中也總會有一些很大的表,可能每天爲了生成一張報表,而只需要訪問一次就可以了。但有可能就是這麼一次訪問,就將大部分的內存數據塊清除出了buffer cache。爲了避免這種情況的發生,可以設置recycle類型的buffer cache,並將這種偶爾訪問的大表移入recycle buffer cache。

  毫無疑問,如果你要設置這三種類型的buffer cache,你需要自己研究並等於你的數據庫中的對象進行分類,並計算這些對象的大小,從而才能夠正確的把它們放入不同的buffer cache。但是,不管怎麼說,設置這三種類型的buffer cache只能算是最低層次的優化,也就是說在你沒有任何辦法的情況下,可以考慮設置他們。但是如果你能夠優化某條buffer gets非常高SQL使其buffer gets降低50%的話,就已經比設置多個buffer cache要好很多了

    9i以後還提供了可以設置多種數據塊尺寸(2、4、8、16 或 32k)的buffer cache,以便存放不同數據塊尺寸的表空間中的對象。使用初始化參數:db_Nk_cache_size來指定不同數據塊尺寸的buffer cache,這裏的N就是2、4、8、16 或 32。創建數據庫時,使用初始化參數:db_block_size所指定缺省的數據塊尺寸用於system表空間。然後可以指定最多4個不同數據塊尺寸的表空間,每種數據塊尺寸的表空間必須對應一種不同尺寸的buffer cache,否則不能創建不同數據塊尺寸的表空間。

SQL> create tablespace tbs_test_16k
  2  datafile 'C:\oracle\oradata\ora92\tbs_test_16k.dbf' size 10M
  3  blocksize 16k;
create tablespace tbs_test_16k
*
ERROR 位於第 1 行:
ORA-29339: 表空間塊大小 16384 與配置的塊大小不匹配
SQL> show parameter db_16k_cache_size
NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
db_16k_cache_size                    big integer 0

  我們可以看到,由於16k數據塊所對應的buffer cache沒有指定,所以創建16k數據塊的表空間會失
敗。於是我們先設置db_16k_cache_size,然後再試着創建16k數據塊的表空間。

SQL> alter system set db_16k_cache_size=10M;
系統已更改。
SQL> create tablespace tbs_test_16k
  2  datafile 'C:\oracle\oradata\ora92\tbs_test_16k.dbf' size 10M
  3  blocksize 16k;
表空間已創建。

  不同尺寸數據塊的buffer cache的管理和內部機制與缺省數據塊的buffer cache沒有任何的分別。它最大的好處是,當使用可傳輸的表空間從其他數據庫中將不同於當前缺省數據塊尺寸的表空間傳輸過來的時候,可以不做很多處理的直接導入到當前數據庫,只需要設置對應的數據塊尺寸的buffer cache即可。同時,它對於調優OLTP和OLAP混合的數據庫也有一定的用處。OLTP環境下,傾向於使用較小的數據塊,而OLAP環境下,由於基本都是執行全表掃描,因此傾向於使用較大的數據塊。這時,可以將OLAP的錶轉移到使用大數據塊(比如32k)的表空間裏去。而將OLTP的表放在中等大小的數據塊(比如8k)的表空間裏。
對於應該設置buffer cache爲多大,oracle從9i開始通過設置初始化參數:db_cache_advice,從而提供了可以參照的建議值。oracle會監控default類型、keep類型和recycle類型的buffer cache的使用,以及其他五種不同數據庫尺寸(2、4、8、16 或 32k)的buffer cache的使用。在典型負荷的時候,啓用該參數,從而收集數據幫助用戶確定最佳的db_cache_size的大小。該參數有三個值:
   1) off:不收集數據。
   2) on:開始分配內存收集數據,有可能引發CPU和內存的負擔,可能引起4031錯。
   3) ready:不收集數據,但是收集數據的內存已經預先分配好了。通過把該參數值從off設置爲ready,然後再設置爲on,就可以避免出現4031錯。


    oracle會根據當前所監控到的物理讀的速率,從而估算出在不同大小尺寸的buffer cache下,所產生的可能的物理讀的數量。oracle會將這些收集到的信息放入視圖:v$db_cache_advice中。每種類型的buffer cache都會有相應的若干條記錄來表示所建議的buffer cache的大小。比如下面,我們顯示對於缺省類型的、缺省數據塊尺寸的buffer cache的建議大小應該是多少。

SQL> SELECT size_for_estimate, buffers_for_estimate,
  2         estd_physical_read_factor,estd_physical_reads
  3  FROM v$db_cache_advice
  4  WHERE NAME = 'DEFAULT'
  5  AND block_size = (SELECT VALUE
  6  FROM v$parameter
  7  WHERE NAME = 'db_block_size')
  8  /
SIZE_FOR_ESTIMATE BUFFERS_FOR_ESTIMATE ESTD_PHYSICAL_READ_FACTOR ESTD_PHYSICAL_READS
----------------- -------------------- ------------------------- -------------------
                4                  500                    1.3869               40154
                8                 1000                    1.3848               40093
               12                 1500                    1.1861               34339
               16                 2000                    1.1397               32996
               20                 2500                         1               28952
               24                 3000                         1               28952
               28                 3500                         1               28952
               32                 4000                         1               28952
               36                 4500                    0.8671               25104
               40                 5000                    0.8671               25104
               44                 5500                    0.8671               25104
               48                 6000                    0.7422               21488
               52                 6500                    0.7422               21488
               56                 7000                    0.7422               21488
               60                 7500                     0.554               16040
               64                 8000                     0.554               16040
               68                 8500                     0.554               16040
               72                 9000                     0.554               16040
               76                 9500                     0.554               16040
               80                10000                     0.554               16040

    這裏的字段estd_physical_read_factor表示在相應的buffer cache尺寸(由字段size_for_estimate表示)
 下,估計從硬盤裏讀取數據的次數除以在內存裏讀取數據的次數。如果沒有發生物理讀則該比值爲空。在
 內存足夠的前提下,這個比值應該是越低越好的。從上面的輸出中,我們可以看到,如果將buffer cache
設置爲60M,可以獲得較好的性能,物理讀也將會有一個顯著的下降。但是設置爲大於60M的話(比如
64M或68M),則不會降低物理讀,反而浪費內存空間。所以從上面的查詢結果中,我們可以知道,設置
爲60M是比較合適的。

4.2 buffer cache的統計信息

   爲了對buffer cache進行性能的診斷,oracle提供了很多有關buffer cache的統計信息。這些統計信息大致可以分成三類:

1)有關用戶發出的對內存數據塊的請求相關的統計信息;

2)有關DBWR後臺進程對內存數據塊處理相關的統計信息;

3)RAC相關的統計信息。

      我們在診斷buffer cache時,不需要關注所有的統計信息。這裏主要介紹幾個重要的統計信息,其他的統計信息都可以到《Oracle9i Database Reference: Appendix C》中找到。如下所示:

SQL> SELECT name, value FROM v$sysstat  WHERE name in (
  2      'session logical reads',
  3      'physical reads',
  4      'physical reads direct',
  5      'physical reads direct (lob) ',
  6      'consistent gets',
  7      'db block gets',
  8      'free buffer inspected')
  9  /
NAME                                                                  VALUE
---------------------------------------------------------------- ----------
session logical reads                                                 73797
db block gets                                                           498
consistent gets                                                       73299
physical reads                                                        29017
free buffer inspected                                                     0
physical reads direct                                                    40

  這裏做些簡單的解釋。
     1) session logical reads:所有的邏輯讀的數據塊的數量。注意,其中包括先從硬盤上讀數據塊到內存裏,再從內存裏讀數據塊。
    2) consistent gets:在一致性(consistent read)讀模式下讀取的內存裏的數據塊數量。包括從rollback segment裏讀取的數據塊數量以及從data block buffer裏讀取的數據塊數量。主要是通過select產生的。Update/delete也能產生很少量的此類數據塊。注意:如果oracle的運行時間過長,由於oracle的bug導致consistent gets大大超過實際的數量。因此建議使用‘no work - consistent read gets’, ‘cleanouts only - consistent read gets’,‘rollbacks only - consistent read gets’, ‘cleanouts and rollbacks - consistent read gets’之和來代替consistent gets的值。
    3) db block gets:在當前(current)模式下讀取的內存裏的數據塊的數量。不是讀取過去某個時點的數據塊,而必須是當前最新的數據塊。主要是通過update/delete/insert來產生的,因爲DML需要當前最新的數據塊才能對之進行改變。在字典管理表空間下,一些獲得當前可用擴展空間的select語句也會產生此類數據塊,因爲必須得到當前最新的空間使用信息才能擴展。邏輯上,session logical reads = consistent gets + db block gets。
    4) physical reads:從硬盤裏讀取的數據塊的數量。注意,這個數量大於實際從硬盤裏讀取的數量,因爲這部分block也包括了從操作系統緩存裏讀取的數據塊數量。
    5) physical reads direct:有些數據塊不會先從硬盤讀入內存再從內存讀入PGA再傳給用戶,而是繞過SGA直接從硬盤讀入PGA。比如並行查詢以及從臨時表空間讀取數據。這部分數據塊由於不緩存使得hit ratio不會被提高。
    6) physical reads direct (lob):與physical reads direct一樣。
    7) free buffer inspected:這個值表示爲了找到可用數據塊而跳過的數據塊的數量。這些被跳過的數據塊就是髒的或被鎖定的數據塊。明顯,這個值如果持續增長或很高,就需要增加buffer cache的大小了。

   在獲得了這些統計信息以後,我們可以計算buffer cache的命中率:

1Hit Ratio = 1 – (physical reads – physical reads direct
- physical reads direct (lob) ) / session logical reads
2Miss ratio = (physical reads – physical reads direct-
physical reads direct (lob) ) / session logical reads



   通常在OLTP下,hit ratio應該高於0.9。否則如果低於0.9則需要增加buffer cache的大小。在考慮
 調整buffer cache hit ratio時,需要注意以下幾點。
    1) 如果上次增加buffer cache的大小以後,沒有對提高hit ratio產生很大效果的話,不要盲目增加buffer cache的大小以提高性能。因爲對於排序操作或並行讀,oracle是繞過buffer cache進行的。
    2) 在調整buffer cache時,儘量避免增加很多的內存而只是提高少量hit ratio的情況出現。
我們還可以查詢每種buffer cache的統計信息,主要關注的還是consistent_gets和db_block_gets以及
 physical_reads的值。

SQL> SELECT name, block_size,physical_reads, db_block_gets,consistent_gets
  2  FROM v$buffer_pool_statistics;
NAME                 BLOCK_SIZE PHYSICAL_READS DB_BLOCK_GETS CONSISTENT_GETS
-------------------- ---------- -------------- ------------- ---------------
DEFAULT                    8192          28978           719           77591
DEFAULT                   16384              2            80              11

      v$sysstat中名稱以DBWR開頭的都是有關DBWR後臺進程相關的統計信息。當DBWR進程寫完髒數據塊以後或者掃描完LRU鏈表以後更新這些統計信息。DBWR會基於被觸發的頻率以及所處理的內存數據塊的數量與總內存數據塊的數量的比例,來進行自我調整。我們可以通過這些統計信息得到一些對當前DBWR運行情況的認識。

4.3 buffer cache的等待事件
    與buffer cache相關的等待事件包括:latch freebuffer busy waitsfree buffer waits。曾經發生過的等
待事件可以從v$system_event(一個等待事件對應一行記錄)和v$session_event(一個session一個等待事件對應一行記錄)中看到。而當前系統正在經歷的等待事件可以從v$session_wait看到。
4.3.1 latch free等待

     等待事件“latch free”中與buffer cache有關的有兩類:cache buffers chains latch和cache buffers lru chain latch。在理解了上面所描述的有關buffer cache的內部管理機制以後,就應該很容易理解這兩個latch產生的原因。

    對於buffer cache中的每個hash chain鏈表來說,都會有一個名爲cache buffers chains latch的latch來保護對hash chain的併發操作,這種latch通常也叫作hash latchCBC latch

數據庫中會有很多的cache buffers chains latch,每個latch都叫做child cache buffers chains latch。一個child cache buffers chains latch會管理多個hash chain。前面我們知道,hash chain的數量由一個隱藏參數:_db_block_hash_buckets決定。同樣也有一個隱藏參數:_db_block_hash_latches來決定有多少個cache buffers chains latch來管理這些hash chain。該參數的缺省值由buffer cache中所含有的內存數據塊的多少決定,當內存數據塊的數量
     少於2052個時,_db_block_hash_latches = power(2,trunc(log(2, 內存塊數量 - 4) - 1))
    多於131075個時,_db_block_hash_latches = power(2,trunc(log(2, db_block_buffers - 4) - 6))
    位於2052與131075 buffers之間,_db_block_hash_latches = 1024
可以使用下面的SQL語句來確定當前系統的cache buffers chains latch的數量。

SQL> select count(distinct(hladdr)) from x$bh;
COUNT(DISTINCT(HLADDR))
-----------------------
                   1024
SQL> select count(*) from v$latch_children where name='cache buffers chains';
  COUNT(*)
----------
      1024

     在知道了cache buffers chains latch的數量以後,我們只需要用hash chain的數量除以latch的數量以後,就可以算出每個latch管理多少個hash chain

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章