ITPub博客

首页 > 数据库 > Oracle > 搞懂buffer cache

搞懂buffer cache

原创 Oracle 作者:静以致远√团团 时间:2014-07-16 08:49:08 3 删除 编辑

搞懂buffer cache

    虽然随着Oracle版本的推进,内存管理变的不在那么复杂,9i的PGA自动管理(pga_aggregate_target),10g的SGA自动管理(sga_target),到11g把PGA和SGA都纳入自动管理的范畴(memory_target),但是Oracle内存的内部运行机制很值得去探索,能够清晰的认识Oracle每个操作的运行方式,从基层去解决表象的问题,才是正道。


SGA中的数据高速缓存区(buffer cache)

Oracle修改块的规则并不是直接去修改数据文件中的数据块,而是将数据块缓存到一个区域内,在这个区域内对数据块进行操作后再将这些数据块写入数据文件。这个区域就是buffer cache。当然,在Oracle具体进行数据块操作时并不是这么简单,每当操作一个数据块,Oralce都会先去buffer cache中按照一定规则检索该数据是否存在,如果存在,就会直接从buffer cache中对数据进行操作,如果不存在,再去数据文件中查找数据,将数据块缓存到buffer cache中。


LRU List 和 Dirty List

LRU(Least Recently Used)链表用来维护内存中的buffer(buffer指的是缓存到buffer 中的数据块),在数据库初始化时,所有的buffer就已经被Hash(Hash算法指的是按照一定的算法规则,把一个buffer按照算法规则放置到相应位置,这样下次检索该buffer时,通过同样的算法即可快速定位)到LRU List上进行管理。Dirty List(或者叫LRUW Least Recently Used Write)。

当LRU上的buffer进行修改以后,数据的状态就会变为Dirty,相应的buffer将被移动Dirty List上,等待DBWr写入到数据文件。个buffer header要么挂在LRU上,要么挂在LRUW上,不能同时挂在这两个链表上。

需要注意的是,从Oracle 8i开始,又在LUR List和DIrty List上增加了辅助List,此处计为LUR_AUX List和Dirty_AUX List,这样以来,当数据库初始化时,buffer首先存在LRU_AUX List上,当被使用后再移动到LRU list,这样当用户搜索数据时使用LRU_AUX List,而当DBWR 搜索Dirty buffer时可以从LRU List上进行,从而提高了效率。


Cache Buffers lru chain(lru latch)

随着硬件技术的发展,电脑的内存越来越大。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。


SQL> select x.ksppinm name, y.ksppstvl value, x.ksppdesc describ

  2  from sys.x$ksppi x,sys.x$ksppcv y

  3  where x.indx = y.indx and x.ksppinm = '_db_block_lru_latches'

  4  /


NAME                      VALUE      DESCRIB

------------------------- ---------- ------------------------------

_db_block_lru_latches     8          number of lru latches


据库在操作数据时具体是如何在buffer cache中查找指定buffer的呢? 


引入几个名词:

Buffer header、hash chain、Hash bucket 


每一个数据块在被读入buffer cache时,都会先在buffer cache中构造一个buffer header,buffer header与数据块一一对应。buffer header类似于buffer的身份证,上面记录着buffer的详细信息。这样只需要通过检查buffer header就可以确定检索的buffer是否存在于buffer cache中。

    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的进程列表(waiter list)和正在使用该buffer header的进程列表(user list)

7)  该buffer header被访问(touch)的次数。

8)  各种CR区域,诸如redo rba,为实现多种操作


hash bucket 是buffer header里记录的数据块地址和数据块类型运用hash算法以后,得到的组号。

hash chain就是属于同一个hash bucket的所有buffer header所串起来的链表。实际上,hash bucket只是一个逻辑上的概念。每个hash bucket都是通过不同的hash chain而体现出来的。每个hash chain都会由一个cache buffers chains latch来管理其并发操作。


转存出buffer cache研究一下

SQL> create table buffer_cache_test(id varchar2(10));

Table created.


SQL> insert into buffer_cache_test values('000001');

1 row created.


SQL> commit;

Commit complete.


SQL> select object_id from dba_objects where object_name='BUFFER_CACHE_TEST';


 OBJECT_ID

----------

     57338

SQL> select * from buffer_cache_test;


SQL> alter session set events 'immediate trace name buffers level 1';

Session altered.

SQL> oradebug setmypid;

Statement processed.

SQL> oradebug tracefile_name;

/u01/app/oracle/admin/orcl_dup/udump/orcl_dup_ora_8220.trc


    000001占了6个block,加上表结构自身占的2个block,dump出的trace文件中应该有8个block信息,此处仅列出其中的一个块上的信息


BH (0x20ffa69c) file#: 4 rdba: 0x01000185 (4/389) class: 1 ba: 0x20fb6000

  set: 3 blksize: 8192 bsi: 0 set-flg: 0 pwbcnt: 0

  dbwrid: 0 obj: 57338 objn: 57338 tsn: 4 afn: 4

  hash: [21ff1fdc,290e80f8] lru: [20ffa7a0,22bf5730]

  ckptq: [NULL] fileq: [NULL] objq: [NULL]

  st: CR md: NULL tch: 1

  cr: [scn: 0x0.23b440],[xid: 0xc.29.d6],[uba: 0x2000037.cc.27],[cls: 0x0.23b441],[sfl: 0x0]

  flags: only_sequential_access

BH (0x21ff1fdc) file#: 4 rdba: 0x01000185 (4/389) class: 1 ba: 0x21e2e000

  set: 3 blksize: 8192 bsi: 0 set-flg: 0 pwbcnt: 0

  dbwrid: 0 obj: 57338 objn: 57338 tsn: 4 afn: 4

  hash: [290e80f8,20ffa69c] lru: [20bf3f20,21ff1950]

  lru-flags: hot_buffer

  ckptq: [291529c8,20ffacf4] fileq: [29152a18,20ffacfc] objq: [20ffad74,27bf5010]

  st: XCURRENT md: NULL tch: 4


可以看到

第一个BH (0x20ffa69c)的 hash: [21ff1fdc,290e80f8]

第二个BH (0x21ff1fdc)的 hash: [290e80f8,20ffa69c]  

注意这里第二个BH应该是在chain的首部

这里hash记录的就是指向前一个buffer header和后一个buffer header的指针。

这里,我们看到第二个BH所指向的后一个buffer header的指针是20ffa69c,而第一个BH所指向的前一个buffer header的指针也是20ffa69c,说明这两个buffer header是在同一个hash chain上。如果发现hash值相同的情况,说明该chain上只有一个BH信息,同样的,我们还可以看到类似结构的lru、ckptq、fileq,这些都是管理buffer header的一些链表结构。

class 表示buffer header所对应的数据块的类型


st: CR md: NULL tch: 1

st: XCURRENT md: NULL tch: 4


st用来表示buffer cache所指向的数据块的状态

FREE(0)=可以被重用的数据块;

XCURRENT(1)=实例以排他方式获取的当前模式数据块;

SCURRENT(2)=可以与其他实例共享的当前模式数据块;

CR(3)=作为一致性读镜像的数据块,永远不会被写入磁盘;

READING(4)=正在从磁盘读出的数据块;

MRECOVERY(5)=正在进行介质恢复的数据块;

IRECOVERY(6)=正在进行实例恢复的数据块。


整体buffer cache结构如图:



现在应该明白了,Oralce在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 比较地址的时候往里读取的)


如果在hash chain上没有找到所要的buffer header时,oracle会发出I/O调用,到磁盘上的数据文件中获取数据块,并将该数据块的内容拷贝一份到buffer cache中的内存数据块里。这个时候,如果buffer cache有空闲空间,将按照上面所说的机制,对该数据库进行hash运算后,将数据库块存放到指定的bucket中,再将buffer header挂到LRU List上。  而此时没有足够的buffer cache空间呢?

前面已经说过,自Orale8i以后,Oracle引进了辅助List。在引进该链表的同时,Oracle还对buffer cache增加了一个属性:touch,touch用来记录buffer header曾经被访问的次数,方便对LRU List的管理。假设buffer cache只能容纳4个数据块,同时只有一个hash chain和一个LRU List(因为有辅助链表,该LRU list暂且被分为分为LRU 和LRU_AUX List),读入第一个数据块A时,该数据块对应的buffer header会挂到LRU辅助链表,的最末端,同时touch数量为1。读取第二个不同的数据块B时,数据块B对应的buffer header会挂到A 的buffer header后面,位于LRU辅助链表的最末端,同样touch为1。这样依次读入四块数据块。此时如果再读入第五个数据块E,会发现buffer cache 中已经没用足够空的buffer,于是从LRU_AUX List的尾端开始扫描,扫描过程中会按照如下规则来确定哪个buffer被牺牲:


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数量减掉一半。

比如BH 9的touch是4,_db_aging_hot_criteria是2,_db_aging_hot_criteria 是0 ,该buffer header此次不会被牺牲,但是 count < criteria,那么该BH 的count = touch = 3;这样一来下次再扫描该块的时候,touch = 4,criteria=2,count=3,虽然仍然不会使用该块,但是此时count > criteria ,touch = touch/2 = 2。 也就是说,在touch 超出_db_aging_hot_criteria的情况,第一次扫描不会牺牲该BH,但会记录该数据库的_db_aging_stay_count,以后每次再访问该数据块时touch的次数都会减半,直到有数据块的touch < _db_aging_hot_criteria,牺牲该数据块。


SQL> select x.ksppinm name, y.ksppstvl value, x.ksppdesc describ

  2  from sys.x$ksppi x,sys.x$ksppcv y

  3  where x.indx = y.indx and x.ksppinm = '_db_aging_hot_criteria'

  4  /


NAME                      VALUE      DESCRIB

------------------------- ---------- ------------------------------

_db_aging_hot_criteria    2          Touch count which sends a buff

                                  er to head of replacement list


SQL> select x.ksppinm name, y.ksppstvl value, x.ksppdesc describ

  2  from sys.x$ksppi x,sys.x$ksppcv y

  3  where x.indx = y.indx and x.ksppinm = '_db_aging_stay_count'

  4  /


NAME                      VALUE      DESCRIB

------------------------- ---------- ------------------------------

_db_aging_stay_count      0          Touch count set when buffer mo

                                    ved to head of replacement list



  按照上述的逻辑,这时将选出A作为牺牲者(因为A的touch数量为1,小于_db_aging_hot_criteria)并将其对应的内存数据块的内容清空,同时将当前第五个数据块的内容拷贝进去。

但是这里要注意,这个时候该A在LRU链表上的位置并不会发生任何的变化(这里是插入了新的数据块的内容,所以touch的数量没有变化)。

执行第六句和第七句SQL,分别要返回与第五句和第四句SQL一样的数据块,


也就是要返回当前的D和E。这个时候,oracle会增加D和E的touch数量,同时将该D和E从辅助LRU链表上摘下,转移到主LRU链表的中间位置。



执行第八句SQL,要求返回与第三句SQL相同的数据块,也就是当前的C,则这时C会插入主LRU链表上的D和E中间,注意每次向主LRU列表插入buffer header时都是向中间位置插入。如果发来了第九句SQL要求返回B,则我们可以知道,B会转移到主LRU链表的中间。这个时候,辅助LRU链表就空了,没有buffer header了。



    这时,如果又发来第十句SQL,要求返回一个新的、buffer cache中不存在所需内容的数据块时。oracle会先扫描辅助LRU链表,发现上面没有任何的buffer header时,则必须扫描主LRU链表。从尾部开始扫描,采用前面说到的与扫描辅助LRU链表相同的规则挑选牺牲者。挑出的可以被替代的buffer header将从主LRU链表上摘下,放入辅助LRU链表。


从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自己来计算。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/29320885/viewspace-1219943/,如需转载,请注明出处,否则将追究法律责任。

下一篇: 学会 Shared Pool
请登录后发表评论 登录
全部评论
每个人都有梦想,去实现吧!

注册时间:2013-11-14

  • 博文量
    164
  • 访问量
    2103381