分类目录归档:Internal

oracle buffer cache

Granules

从10g开始引入了ASMM的功能,oracle会自动管理各个模块内存的使用,而granules则是oracle用于使用共享内存区域的最大内存单元。

在oracle的sga中,数据块都会读入到buffer cache这块内存区域当中,如果启用了ASSM,share pool中的部分区域会标记为KGH:NO ALLOC然后重新映射到buffer cache当中去,这是oracle会根据使用情况自动调整内存大小。

与数据缓存相关的主要有三部分:
最大的则是buffer阵列,主要用来存放从磁盘copy的数据块,其次为buffer header阵列,可以通过内存结构x$bh查看,其余则是少量的管理开销所需要的部分。

buffer header与buffer的联系非常紧密。buffer阵列的每一行与buffer header阵列中对应的每一行都有一个永久一对一的对应关系,buffer header与block header并不一样,buffer header包含一些关于block的信息,一些buffer状态的信息,和许多指向其他buffer header的指针。

Buffer池

每个granule都分配一个指定的buffer池,这表示任何一个granule都只包含同一个大小的buffer。

内存通常会被分成较小的一些块,这样会方便管理。将以块为单位的内存从一块区域(eg:data cache)移动到其他区域(eg:shared pool)是也会更容易。而管理这些块的机制最常见的就是LRU列表,但是这里要称之为工作数据集更加合适点。

每个不同的buffer池都可以分离出多个切片,主要为了减少需要当作一个整体进行维护的buffer数量。这些切片贯穿多个granule,所以如果一个buffer池由4个工作数据集(working data set)组成,每个切片则会包含granules当中1/4数量的buffer。每个buffer池都有相同数量的工作数据集,同时由于有8个buffer池,所以每个实例的工作数据集数量为8的倍数。

PARAMETER_NAME                                               TYPE        VALUE
------------------------------------------------------------ ----------- ---------------------------
db_16k_cache_size                                            big integer 0
db_2k_cache_size                                             big integer 0
db_32k_cache_size                                            big integer 0
db_4k_cache_size                                             big integer 0
db_8k_cache_size                                             big integer 0
db_cache_size                                                big integer 0
db_keep_cache_size                                           big integer 0
db_recycle_cache_size                                        big integer 0

工作数据集

工作数据集是一个非常重要的内存单元,是专门用来支持对buffer进行物理IO操作的部分。

SQL> @desc x$kcbwds
           Name                            Null?    Type
           ------------------------------- -------- ----------------------------
    7      DBWR_NUM                                 NUMBER
   10      CNUM_SET                                 NUMBER

   14      SET_LATCH                                RAW(8)
   15      NXT_REPL                                 RAW(8)
   16      PRV_REPL                                 RAW(8)
   17      NXT_REPLAX                               RAW(8)
   18      PRV_REPLAX                               RAW(8)
   19      CNUM_REPL                                NUMBER
   20      ANUM_REPL                                NUMBER
-- more

DBWR_NUM 每个工作数据集都有一个相关的dbwr进程,每个dbwr进程都会对应多个工作数据集。cnum_set表示数据集里含有的buffer数量,set_latch表示保护这个数据集的cache buffer lru chain的latch地址。剩下的几个字段其实就代表了replacement列表的地址,这里总共有两条列表,一个叫replacement列表(通常被称为LRU列表但不是完全精确),一个叫辅助replacement列表,从这里也可以看出都是双向列表,因为都是NXT和PRV成对出现。
我们知道一个工作数据集切片是覆盖多个granules,每个granule包含一组buffer headers,但是在x$kcbwds视图里可以看到一对链表的端点,如果检查x$bh可以看到另一对字段(nxt_repl和prv_repl)来知道链表是如何工作的。

上图中上面的buffer header链也就是通常被称为LRU列表数据集,nxt_repl字段指向列表中最多访问的部分,而prv_repl字段则是指向列表中最少访问的部分。
这里cnum_repl是两条链表上所有pin住的buffer header数量总和,anum_repl则单独表示辅助replacement列表上pin住的buffer header数量。

LRU/TCH算法

lru算法解释参考https://en.wikipedia.org/wiki/Cache_replacement_policies

所有的buffer header都是通过一个双向链连接起来,每个buffer header都会指向一个buffer,每个buffer都会持有一个数据块的副本。现在要做的就是如果读取一个新数据块到内存,哪个buffer是可以被覆盖的。而oracle用到的算法并不是LRU算法,而是在LRU算法基础上修改后的TCH算法(touch count)

SQL> @desc x$bh
           Name                            Null?    Type
           ------------------------------- -------- ----------------------------
    1      ADDR                                     RAW(8)
    2      INDX                                     NUMBER
    3      INST_ID                                  NUMBER
    4      CON_ID                                   NUMBER
    5      HLADDR                                   RAW(8)
    6      BLSIZ                                    NUMBER
    7      NXT_HASH                                 RAW(8)
    8      PRV_HASH                                 RAW(8)
    9      NXT_REPL                                 RAW(8)
   10      PRV_REPL                                 RAW(8)

   48      US_NXT                                   RAW(8)
   49      US_PRV                                   RAW(8)
   50      WA_NXT                                   RAW(8)
   51      WA_PRV                                   RAW(8)
   52      OQ_NXT                                   RAW(8)
   53      OQ_PRV                                   RAW(8)
   54      AQ_NXT                                   RAW(8)
   55      AQ_PRV                                   RAW(8)
   56      OBJ_FLAG                                 NUMBER
   57      TCH                                      NUMBER
   58      TIM                                      NUMBER

通过x$bh结构可以看到包含了大量的PRV NXT字段对,也说明了buffer header包含了大量的指针,并且是双向的

传统的LRU算法在一个对象被访问后,则将其移动到列表的顶端,但是对于像buffer cache这样的部分来说,有大量的对象被同时使用,将这些对象一直移动则会造成大量的资源消耗并同时带来很多latch争用。为了解决这些问题,则引入了touch count的概念,改进了算法,增加了上面标红的字段,TCH计数器和时间标志TIM。每次buffer被访问时,都会更新tch和时间戳——需要从上次更新到现在超过3s。现在不会移动这个buffer header。

读取块到内存

x$bh视图里也有tch字段,x$kcbwds视图有一个中间点指针cold_hd

加载数据块到buffer

如果要读取一个全新的数据块到内存,首先需要找一个可用的buffer然后复制进去。这时要看哪些块可用,则从replacement列表的LRU端开始去查询。假设在列表的末端找到一个tch为1的buffer header,表示这个buffer从加载到内存到现在并没有被其他会话访问过,那么就是属于很少访问的冷块。还需要检查这个buffer是否正被pin住,并且其不需要被写回磁盘。假设所有的检查都通过,则会以排它模式pin住buffer header,读取数据块到buffer,更新buffer header里记录的相关信息,从列表的末端移除buffer header,并将其重链到列表的中点(v$kcbwds.cold_hd),然后unpin buffer header。

重链buffer

由于需要重新读取新块到内存,所以需要将buffer从原先的cache buffer chain上分离并且连接到新的上,也就表示需要同时获取两个cache buffer chain latch。
大致过程如下:

  • 修改x$kcbwds.prv_repl指向列表中下一个buffer header
  • 修改列表中下一个buffer header指回x$kcbwds
  • 修改当前列表中点的两个buffer header指向我的buffer header而不是它们自己
  • 修改x$kcbwds.cold_hd指向我的buffer header
  • 将原先将buffer从原先的cache buffer chain上分离并且连接到新的上
  • 将buffer与旧的对象分离并关联新的

假设tch=1的buffer已经加载了一个新的数据块,则它会被移动到中点,所以tch=4的buffer就变成了列表的最末端。对于不同的tch值的buffer来说,要读取一个新块到这个buffer时处理逻辑都有所不同。

buffer包含一个活跃块:
buffer自从加载数据块以后被访问了多次,所以不需要急着将其清出内存。oracle将buffer从列表的最LRU(最近最少使用)端分离并将其重新链接到MRU(最近最多使用)端,并将tch值减半,然后继续检查列表中下一个数据块。所以一个活跃块只有在快要被移出缓存的LRU端的情况下才会移动到MRU端,而并不是每次被使用时就会移动。

辅助LRU

当会话想搜寻buffer来读入数据块时,并不是优先从主replacement列表的LRU端去查询的,而是先搜索的辅助replacement列表的LRU端。辅助replacement列表主要存放的是那些几乎可以立即被使用的buffer header,由于这个原因,会使得在搜寻可用buffer的效率提高,它不需要消耗资源来考虑哪些是脏buffer、被pin住的buffer和其他需要考虑的问题。

寻找数据

因为buffer就是用来储存数据块的副本,所以内存里会包含大量的数据块,有些可能是重复的,同时dbwr进程也会定期将这些脏buffer写回磁盘。这时我们访问一个特定的数据块时,要如何知道是否应该重新从磁盘读取到内存,并且要如何快速有效的判断这个块已经不在内存了呢?

其实是采用hash表的方式,将一小段链接起来的buffer header附在每个bucket上,然后将这些bucket分成小组,通过一个cache buffer chains latch来保护这些小组,下面这个图表示了4个cache buffer chains latch,16个hash buckets,和23个buffer headers。

当我们有了一些buffer headers的集合后,可以通过定义合适的指针去链接相关的buffer headers来施加各种模式到这个集合上。cache buffer chains只是通过指针产生的其中一种模式,完全独立于granules,buffer pools,工作数据集,replacement列表等。

下面是需要访问一个buffer的大致过程

  • 先将数据块的地址(DBAs:表空间、file_id、block_id的结合)进行hash算法计算,找到对应的hash bucket
  • 获取保护这个hash bucket的cache buffer chains latch
  • 如果获取latch成功,则读取buffer headers去查看指定版本的block是否已经在链表中,如果找到则直接访问buffer cache里的buffer,通过pin/unpin的动作来访问
    如果没有找到block,则在buffer cache中找一个可用buffer,从当前链移出,重新链到新hash链上,释放latch然后读取数据块到这个可用buffer
  • 如果获取latch失败,则一直spin到spin_count值的次数然后重新尝试获取latch,依旧失败则sleep然后继续尝试。