Oracle Least Recently Used Chains

LRU Chains(or LRU lists)有它们相关的算法在过去已经修改过多次。尽管算法已经修改过,但LRU chain的功能仍然相同:为了帮助被频繁访问的buffer内置在cache中和帮助服务器进程快速地找到可被替换的buffers。任何时候单个列表都要努力地完成这两个任务,这将可能出现一些妥协。LRU chain也不例外,正如你将要发现的一样,Oracle当前的LRU算法实现的非常好,支持buffer caches超过100G的大小来满足电信与政府系统的高事务处理的要求。

在Oracle 6中,只有单个LRU chain被单个LRU chain latch保护着。在大型的OLTP系统中,DBA将与LRU chainlatch竞争进行斗争。但从Oracle 7开始,Oracle通过将单个LRU chain分割成多个小的LRU chains,每个都有一个相关的LRU chain latch来缓解这种问题。每个cache buffer存放在CBC结构中并存放在一个LRU chain或一个写列表(也叫脏列表)中。buffers不会同时存放在一个写列表与一个LRU列表中。LRU chains要比CBCs长太多。

脏buffers存放在一个LRU chain中不是问题。事实上,如果脏buffers不能存放在一个LRU chain上将会影响性能。LRU chains的一目标就是将被频繁访问的buffers保留在cache中,并且许多脏buffers也会被频繁地访问。当在数据库检查点期间,每个脏buffer将被写入磁盘并再次变为free buffer。

隐含参数_db_block_lru_latches显示实例正在使用的的LRU chains的数量。与CBCs一样,每个LRU chain latch控制着一组LRU chains的序列化。
SQL> select x.ksppinm NAME,y.ksppstvl value,x.ksppdesc describ
2 from x$ksppi x, x$ksppcv y
3 where x.inst_id=USERENV(‘Instance’)
4 and y.inst_id=USERENV(‘Instance’)
5 and x.indx=y.indx
6 and x.ksppinm like ‘%&par%’;
Enter value for par: _db_block_lru_latches
old 6: and x.ksppinm like ‘%&par%’
new 6: and x.ksppinm like ‘%_db_block_lru_latches%’

NAME VALUE DESCRIB
———————- —— ——————————–
_db_block_lru_latches 640 number of lru latches

LRU Chain随着时间的推移而变化
当前的LRU chain算法被叫做touch-count算法,它使用计算频率方案在每个buffer header上设置一个数字。但是Oracle花了很多年才实现这个算法。理解Oracle的LRU算法的发展更能了解LRU chains是如何工作的,它的缺点是什么以及如何确保它们按需执行。

当LRU chains出现性能问题时,大量的LRU chain latch竞争将会出现。从Oracle算法角度来说,latch问题通常会造成服务器进程在搜索一个free buffer时持有一个lRU chain latch的时间太长。这里存在许多相互关联的原因,其解决方案也是一样。

Standard LRU Algorithm(标准LRU算法)
不管Oracle的LRU算法如何,每个Oracle LRU chain有一个最近最少使用(LRU)端,也有一个最近频繁使用(MRU)端。笼统地说,被频繁访问的buffer header将存放在靠近MRU端,并且不被频繁访问的buffer将存放在靠近LRU端。

标准LRU算法是非常简单的。当一个buffer被放入cache中或被访问时(查询或DML操作),buffer将被存放在会话相关的LRU chain(每个会话与一个LRU chain相关)的MRU端。这种想法是一个被频繁访问的buffer将被重复touched并且会被重复移动到LRU chain中的MRU端。buffer移动到LRU chain的MRU端通常叫做buffer promotion。如果一个buffer不被频繁访问,那么其它的buffer将被promoted或插入到LRU chain中,不被频繁访问的buffer将被移动到LRU chain的LRU端。

靠近每个LRU chain的LRU端可能潜伏着一个服务器进程用来查找一个可用的不被频繁访问的buffer好让刚刚从磁盘中读取来的块替换掉它。假设LRU chain是8个buffer header那么长,全表扫描会扫描8个数据块,并且每个数据块将读入Oracle cache中并且buffer headers会被放入LRU chain中。当标准LRU算法使用时,只有一个LRU chain,因此整个LRU链将被全表扫描所访问的数据块所替换。随着时间的推移包含被频繁访问的buffer已经被替换了。用户肯定会注意到性能的变化,并且IO子系统也将受到打击。当数据库大小继续增长的时候,Oracle显然不得不进行改进,所以修改了LRU算法。

Modified LRU Algorithm(修改后的LRU算法)
Oracle著名的LRU算法修改是在Oracle 6中。它是一次重大成就并且Oracle开发者确实应该对他们的高级buffer cache算法感到自豪。在这之后,它确实解决了标准LRU算法的关键问题。

修改后的LRU算法与标准LRU算法仅有的区别是对LRU chain的LRU端的几个buffer创建了一个窗口(用来存放被频繁访问的buffers)。这个窗口的大小只有几个buffers(例如,4个)并且可以通过隐含参数_small_table_threshold来进行修改。这可以确保不管对多大的表进行全表扫描都将不会对cache产生什么影响。

Oracle修改后的LRU算法对一些buffer headers创建了一个窗口,当所有全表扫描(FTS)的buffer headers被读入到buffer cache时会经过这个窗口。这确保了放在LRU chain中的MRU端的被频繁访问的buffers不会被替换掉。

与其它所有算法一样,修改的LRU算法也有限制,但这么多年来这些限制没有造成问题。然而,一旦客户开始使用Oracle来开发大型数据仓库应用程序时,两个显著的问题会出现:
.大型数据仓库有大量的索引,并且当大量索引使用范围扫描时,成千上万的索引叶子块必须被读入cache中。这个问题直到Oracle 8i,如果索引叶子块不在buffer cache中,Oracle将产生一个单块IO请求(db file sequential read)将数据块放入buffer cache。令人吃惊的是因为这不是一个多块IO请求,索引buffer被插入到LRU chain的MRU端,这破坏了开发良好的cache,现在完全存放着索引叶子块buffers。

.当数据块被请求时(基于索引叶子块),它们也会从IO子系统中(db file sequential read)被请求一次,因此再一次这些数据块被放入到LRU chain中的MRU端。当Oracle系统大小增加时,Oracle的buffer cache减少了使用性。

Oracle’s Touch-Count Algorithm
在Oracle 8.1.5中Oracle引入了一种完全修改好的LRU chain算法已经完全消除了所有LRU chain latch竞争问题。关于这种修改没有任何文档记录。发现算法改变是因为看到了新的隐含参数_db_percent_hot_default 和_db_aging_cool_count。当有新的参数出现或有旧的参数丢弃时,算法肯定有被修改。Oracle确实实现了计算机科学领域中通常所说的计数频率方案。

SQL> select x.ksppinm NAME,y.ksppstvl value,x.ksppdesc describ
  2  from x$ksppi x, x$ksppcv y
  3  where x.inst_id=USERENV('Instance')
  4  and y.inst_id=USERENV('Instance')
  5  and x.indx=y.indx
  6  and x.ksppinm in('_db_percent_hot_default','_db_aging_cool_count');

NAME                          VALUE      DESCRIB
----------------------------- ---------- ------------------------------------------------
_db_percent_hot_default       50         Percent of default buffer pool considered hot
_db_aging_cool_count          1          Touch count set when buffer cooled

正如你所期待的,通用方法就是每次触及buffer header时递增计数器。更频繁访问的buffer headers将有更高的触及计数并且确实访问更频繁,因此buffer将被保留在buffer cache。Oracle’s touch-count算法判断buffer header是否被频繁访问是基于buffer header被触及的次数来确定的。注意FTS(全表扫描)窗口的概念将不再需要并且已经被删除了。touch-count算法有三个关键点:midpoint-insertion,touch count incrementation与buffer promotion

Midpoint Insertion
与修改后的LRU算法最根本的背离是midpoint insertion。每个LRU chain被分成hot区与cold区。当一个buffer从磁盘被读入且找到了一个free buffer,这个buffer与buffer header将替换之前的buffer与buffer header的内容然后这个buffer header被移动到LRU chain的midpoint。单块读,多块读,快速完全索引扫描或全表扫描都没有差别。buffer header不会被插入到LRU chain的MRU端,而是LRU chain的midpoint。这确保了不会因为单个对象的大量数据块被读入到buffer cache中而使用LRU chain被破坏掉。

缺省情况下,hot区与cold区各占一半。midpoint确实在中间。然而这个可以通过隐含参数_db_percent_hot_default来配置。

当其它buffer headers被插入到midpoint或被promoted(提升)时,原有的buffer headers自然地将从LRU chain的hot区移动到cold区。在一个buffer header被插入后,只有一种方式可以保留在cache很长时间就是被不断重复地promoted。

因为窗口方案用于修改的LRU算法中而不再被使用,隐含参数_small_table_threshold因此被丢弃。然而在Oracle11g中,它又再次被使用,但是用于不同的目的。从Oracle 11g开始,_small_table_threshold参数是服务器进程开始执行直接路径读的阈值。直接路径读可以提高性能因为数据块从磁盘直接读取到服务器进程的PGA内存中而不用放入buffer cache。然而,这是更自私的读取操作并且可能实际上降低性能,因为其它的服务器进程不能从IO操作中获利。

SQL> select x.ksppinm NAME,y.ksppstvl value,x.ksppdesc describ
  2  from x$ksppi x, x$ksppcv y
  3  where x.inst_id=USERENV('Instance')
  4  and y.inst_id=USERENV('Instance')
  5  and x.indx=y.indx
  6  and x.ksppinm like '%&par%';
Enter value for par: _small_table_threshold
old   6: and x.ksppinm like '%&par%'
new   6: and x.ksppinm like '%_small_table_threshold%'

NAME                           VALUE                           DESCRIB
------------------------------ ------------------------------  -----------------------------------------------------
_small_table_threshold         60283                           lower threshold level of table size for direct reads

假设你是一个服务器进程必须要查询一行存放在特定数据块中的记录。基于这个SQL语句与数据字典,你知道数据块的文件号与块号。如果只关心查询速度,因此希望这个数据块已经存放在buffer cache中了。为了检查数据块是否存放在buffer cache中,需要得到buffer’s buffer cache内存地址,它存放在它的buffer header中。

为了找到buffer header,必须访问CBC结构。哈希文件号与块号,它将指向一个哈希桶。基于这个哈希桶,可以查找相关的CBC latch与持有它。在几次spin后,你可能可以获得latch,因此开始你的序列化CBC搜索。第一个buffer header如果不是你想要的,并且不幸地是在这个CBC中没有第二个buffer header,因此知道buffer当前没有放入buffer cache。

释放CBC latch并执行调用给操作系统,要求访问你需要的数据块。当你正等待时,你将被告知db file sequential read等待事件。最终从操作系统接收到这个数据块并在PGA中持有它。因为没有使用直接路径读,在你或其它服务器进程访问buffer之前,buffer必须被合理地插入到buffer cache并更新所有合理结构。

你将需要一个free buffer用来在buffer cache中存放刚读取的数据块,因此你将移到LRU chain的LRU端。但在你开始扫描LRU chain之前,你必须持有并获得相关的LRU chain latch。之后当休眠时通过spinning与posting等待事件latch:cache buffers lru chains来消耗CPU,最终获得latch。从LRU chain的LRU端开始,你查看buffer header是否它是一个不被频繁访问的free buffer,得到的回答是它是不被频繁访问的buffer。那么你现在就可开始buffer替换操作。你立即pin(固定)住这个buffer header。从buffer header中,可以获得数据块对应buffer在buffer cache中的内存地址,使用刚被读取的且仍在你PGA内存中的块来替换这个free buffer,执行任何要求buffer header所要进行的修改。你维护这个LRU chain并移动buffer header到LRU chain’s midpoint,释放LRU chian latch,并unpin这个buffer header。现在任何服务器或后台进程包括你可以访问这个buffer,这将都是在一瞬间就能完成。

Touch Count Incrementation
这个概念是一个buffer header每被touch一次,它的touch count将会增加。事实上并不是这样。缺省情况下,一个buffer header的touch count只有每3秒才会增加一次。这可以用来确保buffer活动时间超过几秒才算做被频繁访问

当一个buffer被插入到buffer cache中时,它的touch count被设置为0.然而,如果buffer在短期内被重复地touch,那么touch将不会进行计数。

Oracle也允许touch count被遗漏。这将没有latch被调用(这是消除latch竞争最好的方法),并且Oracle不会pin住buffer header。不使用序列化控制,两个服务器进程可以递增与更新buffer header’s的touch count到相同的值。

假设服务器进程S100在时间T0点得到的buffer header的touch count是13,并且开始递增为14。但服务器进程S200现在在时间T1点询问这个buffer header的touch count,并且因为服务器进程S100还没有完成touch count的递增操作,所以buffer header的touch count现在仍然显示为13。服务器进程S200现在开始将touch count从13递增到14。在时间T2点,服务器进程S100将buffer header的touch count修改为14,并且在时间T3点,服务器进程S200也将buffer header的touch count修改为14。这是不是touch count递增被遗漏了?没有结构被损坏,并且touch count确实已经被递增了,但不是递增两次。如果一个buffer确实被频繁地访问,它将再次被touch。通过这种模糊实现节省的是CPU的消耗与内核代码运行量。

Buffer Promotion
没有说当一个buffer被touch后,它将会被promoted到LRU chain的MRU端。这是因为buffer header的touching与buffer header的promotion现在是两个分开的操作。当一个buffer被考虑进行promotion时,也会考虑替换它。而服务器进程与数据库写进程都可以promote buffer header,但只有一个服务器进程将替换这个buffer并且与它相关的buffer header作为一个物理读取数据块的结果。数据库写进程执行替换没有意义,因为它没有替换的内容。

在一个服务器进程从磁盘读取一个数据块之后,它必须要找到一个不被频繁访问的free buffer来存放刚被读取的数据块。服务器进程要获得适当的LRU latch,然后从LRU chain的LRU端开始扫描buffer headers。记住buffer headers存放在LRU chain中,不是buffers中。如果服务器进程遇到了一个free buffer header,那么它检查它是否被频繁访问。如果被频繁访问,服务器进程将promote这个buffer header,然后继续扫描。如果这个free buffer header不被频繁访问,服务器进程将使用从磁盘读取到的数据块来替换这个buffer,并更新buffer header,移动buffer header到LRU chain的midpoint。注意这里不需要更新CBC结构,因为buffer没有被移动,只有LRU chain上的buffer header被移动。如果服务器进程遇到一个dirty buffer header,那么检查是否是一个被频繁访问的dirty buffer header。如果dirty buffer header被频繁访问,它将promote这个buffer header并继续扫描。如果dirty buffer header不被频繁访问,服务器进程将移动这个buffer header到写列表中。如果服务器进程遇到一个被pin 住的buffer header,那将继续扫描。pin住的buffer被禁止使用。

promotion操作只要达到最低值2(_db_aging_hot_criteria)就会中断。因此当一个服务器进程或数据库写进程在询问“每个buffer的touch count数是多少?”时,它实际是问“buffer的touch count是否大于或等于_db_aging_hot_criteria?”。如果每隔几秒一个buffer就会被touch,那么它应该被保留在cache中。如果不是,它将被快速替换掉。

当一个被频繁访问的buffer被promoted时,它的生命周期将变得更困难。promotion操作的一部分是touch count被设置为0(_db_aging_stay_count)。除非buffer是一个segment header或一个consistent read(CR) buffer,否则会出现这种情况。

SQL> select x.ksppinm NAME,y.ksppstvl value,x.ksppdesc describ
  2  from x$ksppi x, x$ksppcv y
  3  where x.inst_id=USERENV('Instance')
  4  and y.inst_id=USERENV('Instance')
  5  and x.indx=y.indx
  6  and x.ksppinm in('_db_aging_stay_count');

NAME                      VALUE        DESCRIB
------------------------- ------------ --------------------------------------------------------------
_db_aging_stay_count      0            Touch count set when buffer moved to head of replacement list

数据库写进程也可能promote被频繁访问的buffer headers。当一个数据库写进程处于休眠状态,它将每3秒钟被唤醒一次。每个数据库写进程都有一个属于它的写列表(dirty列表)并且它也与一个或多个LRU chain相关联。当一个LRU chain的数据库写进程被唤醒,它将检查它的写列表来查看写列表的长度是否足够执行一个IO写操作。如果数据库写进程决定构建一个写列表,它将扫描它的LRU chain来查找不被频繁访问的dirty buffer。非常像服务器进程查找free buffer那样,数据库写进程也将获得相关的LRU chain lath,从LRU chain的LRU端开始并检查buffer header是否为dirty且不被频繁访问。如果一个不被频繁访问的dirty buffer被找到,数据库写进程将会这个buffer header从LRU chain移动到它的写列表中(记住,这个buffer header仍然存放在CBC结构中,因此它能被其它进程找到)。如果写列表的长度仍然不足够执行一次IO写操作,那么数据库写进程将继续扫描它的LRU chain,查找更多的不被频繁访问的dirty buffer headers。

Hot Region to Cold Region Movement
一个buffer header的生命周期在LRU chain是从midpoint(正中间)开始的。因为其它buffer headers将被替换并且被插入到正中间,随着buffers被promoted,一个buffer header自然地将迁移到LRU chain的LRU端。promote一个buffer header的唯一方法就是buffer header标识为被频繁访问。当一个buffer跨过正中间(midpoint)时另一个显著事件会出现,那就是从hot region移动到cold region。

当一个buffer进入到cold region中时,它的touch count会被重设置为缺省值1(_db_aging_cool_count)。这有冷却hot buffer的效果,任何希望保留在cache中的buffer都不想出现这种情况。增加这个参数值将人为增加buffer值从而增加了buffer移动的可能性。因此缺省情况下,当一个buffer header进行到cold region时,它必须至少被touched一次来使其匹配promotion操作的条件(_db_aging_hot_criteria)。

SQL> select x.ksppinm NAME,y.ksppstvl value,x.ksppdesc describ
  2  from x$ksppi x, x$ksppcv y
  3  where x.inst_id=USERENV('Instance')
  4  and y.inst_id=USERENV('Instance')
  5  and x.indx=y.indx
  6  and x.ksppinm in('_db_aging_cool_count');

NAME                      VALUE     DESCRIB
------------------------- --------- ------------------------------------
_db_aging_cool_count      1         Touch count set when buffer cooled


SQL> select x.ksppinm NAME,y.ksppstvl value,x.ksppdesc describ
  2  from x$ksppi x, x$ksppcv y
  3  where x.inst_id=USERENV('Instance')
  4  and y.inst_id=USERENV('Instance')
  5  and x.indx=y.indx
  6  and x.ksppinm in('_db_aging_hot_criteria');

NAME                       VALUE      DESCRIB
-------------------------- ---------- ---------------------------------------------------------------
_db_aging_hot_criteria     2          Touch count which sends a buffer to head of replacement list

Touch Count Changes
可能会疑问为什么当一个buffer header被promoted和当它进入到cold region时Oracle要重新设置touch count。要理解这一点关键要理解中间点(midpoint)。中间点(midpoint)缺省情况下将每个LRU chain平分为hot与cold
region(_db_percent_hot_default=50),它可以被设置为0到100之间的任何数值。如果LRU chain变成一个100%的hot region,那么唯一的touch count重置将发生在buffer被promoted时。当Oracle释放出创建任何数量buffer pools的能力时,在每个pool中维护中间点(midpoint)的能力将允许高度优化和特定的LRU活动。尽管双重设置可能最初看起来比较愚蠢,但它确实有其真正的目的并为将来奠定了基础。

SQL> select '00 : '||count(*) x from x$bh where tch=0
  2  union
  3  select '01 : '||count(*) x from x$bh where tch=1
  4  union
  5  select '02 : '||count(*) x from x$bh where tch=2
  6  union
  7  select '03 : '||count(*) x from x$bh where tch=3
  8  union
  9  select '04 : '||count(*) x from x$bh where tch=4
 10  union
 11  select '05 : '||count(*) x from x$bh where tch=5
 12  union
 13  select '06 : '||count(*) x from x$bh where tch=6
 14  union
 15  select '07 : '||count(*) x from x$bh where tch=7
 16  union
 17  select '08 : '||count(*) x from x$bh where tch=8
 18  union
 19  select '09 : '||count(*) x from x$bh where tch=9
 20  union
 21  select '10 : '||count(*) x from x$bh where tch=10
 22  union
 23  select '11 : '||count(*) x from x$bh where tch=11
 24  union
 25  select '12 : '||count(*) x from x$bh where tch=12
 26  union
 27  select '13 : '||count(*) x from x$bh where tch=13
 28  union
 29  select '14 : '||count(*) x from x$bh where tch=14
 30  union
 31  select '15 : '||count(*) x from x$bh where tch=15
 32  union
 33  select '16 : '||count(*) x from x$bh where tch=16
 34  /

X
---------------------------------------------
00 : 1879125
01 : 697463
02 : 254482
03 : 227324
04 : 161410
05 : 141651
06 : 91699
07 : 70599
08 : 55605
09 : 25551
10 : 17181
11 : 29833
12 : 19978
13 : 13324
14 : 29006
15 : 9998
16 : 9649

17 rows selected

touch count被重新设置有重要影响。首先,这意味着touch count不会飙升到无穷大。touch count重新设置也意味着最被频繁访问的buffer headers将不需要有最高的touch counts。如果你注意到一个特定的buffer有一个较低的touch count,那么你可能捕获了一个被频繁访问的buffer,只是它可能刚刚被promoted或进入到LRU chain的cold region。事实上,最高touch count的buffer headers将存放在LRU chain的LRU端附近。

LRU Chain Contention Identification and Resolution
Oracle的LRU touch-count算法,与缺省的实例参数设置进行组合来使用微不足道的竞争来启用高性能LRU chain活动。当touch-count算法遇到压力时,这是IO和CPU活动的独特组合。

LRU chain latches命名为cache buffers lru chain。哈希chain latches被命名为cache buffer chains。命名很接近并且可能导致相当大的混乱。只要记住LRU chain latches的名字中lru就不会混乱。在Oracle 10g之前的版本中,等待事件被简化成latch free,为了判断特定的latch,需要使用v$session_wait视图中的p2列与v$latch中的latch#进行关联来进行查询。对于Oracle 10g及以后的版本,等待事件标识为latch:cache buffers lru chain。

如果不需要执行物理读来从磁盘读取数据,那么就不会存在LRU chain latch竞争,因为就不需要查找free buffer或者插入一个buffer header到一个LRU chain中。数据库写进程查找不被频繁访问的dirty buffers不会对LRU chain结构造成压力从而导致LRU chain latch的竞争。然而,任何时候一个服务器进程从磁盘读取数据块,它必须要找到一个free buffer,这将请求LRU chain活动(除了直接路径读)。如果IO读区花了10ms,那么你可能看到的是db file scattered read与db file sequential read等待事件而不是LRU chain latch竞争。但如果IO子系统返回数据块的时间少于5ms,那么压力就转移到CPU子系统了,并且这时LRU chain的活动将开始承受压力。

LRU chain latch竞争可能的结果是获取latch的问题,持有latch大长时间或者两个同时出现。如果操作系统的CPU受限,获得latch可能花费很长时间,因为没有足够的CPU周期。一旦latch被获得且LRU chain相关的内核代码被运行,如果CPU周期供应不足或者不被频繁访问的free buffers有限,LRU chain latch可能被持有很长时间足够造成严重的竞争。

因此,首先,必须要有强烈的物理读取活动。第二,IO子系统响应时间非常快,将大部分的等待时间从读取等待事件传递到LRU chain latch等待事件。这种竞争提供了许多可供组合使用解决方法:
.优化物理IO SQL语句
如果没有物理IO存在就不会有大量的LRU chain latch竞争。因此,从应用程序角度来说产,查找主要活动为执行物理块读取也就是物理IO活动的SQL语句。尽你所能地减少SQL语句的物理IO消耗。这意味着执行经典的SQL优化操作,包括使用索引,以及在性能关键时期减少顶级物理IO SQL语句的执行速度。

.增加CPU处理能力
与CBC latch竞争一样或任何其它latch竞争一样,如果有更多的CPU资源可以使用,内存管理将会花费更少的时间。这意味着latch持有时间与latch获取时间(spinning与sleeping)将被减少。增加CPU处理能力也意味着在竞争高峰期间寻找创建性方法来减秒CPU消耗。

.增加LRU latch数量
通过增加latches可以增加LRU的并发,这意味着增加隐含参数_db_block_lru_latches的值。如果有很多G的buffer cache增加latches可能是特别有效的。

.使用多个buffer pools
一种创造性策略来减少主LRU chain压力的方法就是实现keep与recycle pools。所有的buffer pools都可以增加LRUchain latches的数量。它们也使用touch-count算未能,并且有类似的touch count实例参数,比如_db_percent_hot_keep

.调用touch count实例参数
有几个可用touch count参数。但要注意,这些参数的值都很小,比如1和2。因引,即使参数从1修改为2都是相当大的改变可能导致意想不到的后果。只有在测试后将调整touch count参数作为最后的手段。

_db_percent_hot_default参数,它的缺省值为50。它表示在hot region的buffer headers的百分比。如果想要更多的buffer header存放在hot region,可以增加这个参数。减小这个参数将会给予buffer headers在遇到一个服务器进程或数据库写进程之前更多的时间来被touched。

_db_aging_touch_time参数,它的缺省值为3它是唯一能增加一个buffer header的touch count(x$bh.tch)时间窗口的方法。增加这个参数将减小突然爆发以buffer为中心活动的影响,同时会冒着贬值频繁被访问buffer的风险。

_db_aging_hot_criteria参数,它的缺省值为2。一个buffer header的touch count阈值必须满足或被超过才能被promoted(提升)。如果想一个buffer被promoted更困难,可以增加这个参数值。那么只有真正hot buffers才会被保留在cache中。

_db_aging_stay_count参数,它的缺省值为0。当一个buffer header被promoted时touch count被重设置后的值。一致性读与段头块除外。

_db_aging_cool_count参数,它的缺省值为1。当一个buffer header从hot region进入cold region时touch count被重设置后的值。减小这个参数值将使buffer header被promoted变得更困难。

_db_aging_freeze_cr参数,它的缺省值为false。使一致性读取的 buffers总是为cold状态,因此它们容易被替换。

Oracle Cache Buffer Chains

一个Oracle Buffer是一个Oracle段对象缓存块。一个Oracle buffer一开始时包含与Oracle块中相同的信息。一个buffer的内容依赖于段类型以及它是滞是一个段头块。buffer有许多种状态通过v$bh的state列来表示,它们可能被归纳成在种模式:free(可用),dirty(脏)与pinned(固定)。

Free Buffers
当一个buffer与磁盘上的数据块匹配时它的状态就是free。一个free buffer可以看作是一个镜像buffer,因为它镜像了磁盘上的数据块。下面的查询简单的显示了如何判断buffer cache中free buffers的数量。一个free buffer可能确实是空的(例如,在实例重启之后),但它将最有可能包含真实的块信息,比如行记录。一个free buffer可以被替换而不会产生任何损坏,因为有一个副本存储在磁盘上。当然,如果一个事务提交,那么至少被修改的buffer必须被记录到联机重做日志文件中。

SQL> select count(*) from v$bh where status='free';

  COUNT(*)
----------
        24

一个free buffer可能不是被频繁的访问。也许一个查询需要访问单行数据因此需要将数据块放入buffer cache中,而这个buffer之后再也没有被访问过。而另一方面,一个free buffer也可以是被频繁访问的。例如,如是一个特定的数据块被重复地查询,它将被频繁的访问,但它的状态仍然是free状态,因为buffer没有被改变过。如果你对freebuffer的定义简单又清晰,那么许多Oracle的算法将也变得清晰,这将使理解,检测与解决竞争更容易。

Dirty Buffers
当一个buffer它不能与磁盘上的相关块进行匹配时它的状态就是dirty。对一个buffer进行的任何改变都会使用它的状态变为dirty,因为buffer将不再与磁盘上的块相匹配。当内存中的改变还没有被写入磁盘而要对其进行覆盖时,dirty块是不能被替换的。一旦数据库写进程将一个dirty buffer写入磁盘,那么buffer将与磁盘上的块再一次匹配那么这个buffer的状态将变为free。

一个dirty buffer可能也不被频繁访问。假设一行记录被修改但其它进程不需要访问这个buffer。因为行记录被改变这个块确实是dirty的,但它不被频繁访问。当然,也有被频繁访问的dirty buffers。简单地重复更新一行记录将确保它的buffer的状态为dirty又被频繁的访问。

下面的查询显示dirty buffers的状态可能是xcur或write。将在cache buffer chains中详细介绍current与consistent模式的buffers。xcur状态意味着一个进程已经改变了一个current模式的buffer的状态为这种状态,并且进程可能现在更新buffer中的行记录,虽然行记录现在仍然受制于其它条件,比如行级锁。排他模式不会阻止多个用户改变相同buffer中的多行记录,它简单表示当current模式的buffer可以被改变。在RAC环境中这是至关重要的,可能有多个共享current模式buffers(scur),但在整个RAC数据库中每个块只有一个排他current模式buffer存在。

SQL> select status, count(*) from v$bh where dirty='Y' group by status;

STATUS       COUNT(*)
---------- ----------
xcur            20792
scur              919
pi               2567

Pinned Buffers
当一个buffer被pinned时,它不能被替换。另一种看待pinning的方式是对buffer的一种非官方锁。因为一个buffer不是一种关系结构,标准的锁机制不能应用。Pinning一个特定的buffer,latches或mutexes可以控制访问整组buffers。Pinning可以与latch与lock一起连用来确保适当的序列化,保护与并行控制被实现。

假设一个服务器进程将要读取一个buffer中的一行记录。当你仍然在访问这一行记录时,有人使用其它的buffer替换了你正在访问的buffer这是极端粗鲁的。这就像你正在读一本书时,有一个人说”让我看看”,并从你手中抢走一样。许多进程可以pin相同的buffer(读取相同的块),但是只有一个进程能pinned这个buffer,它不能被替换。当一个free buffer的行记录正被查询时,它的状态从free变为pinned再次回到free。当free buffer中的行记录被修改后,它的buffer状态将从free变为pinned,再变为dirty。

Oracle没有通过v$bh视图来显示pinned buffers,但任何被touched的buffer也就是被pinned了。当一个buffer正被移动到写列表中并且正更新touch计数时Oracle将也会pin这个buffer。

Buffer Headers的作用
当buffers内置在buffer cache中并且buffers确实已经被改变了,列表管理实际作用于buffer headers,而不是实际的buffers。一个buffer header是一个优化过的内存结构它包含关于一个buffer和与它相关的块信息,但不包含块数据比如行记录。

为什么对于buffer cache没有视图v$bc?,这是因为一个buffer与一个块的元数据被存储在buffer header,并且它的元数据对于我们的性能分析是需要的。因此视图被命名为v$bh,对于buffer header有三个关键的列表或链:
.Cache buffers chains(CBCs)被用来快速判断一个Oracle块是否内置在buffer cache中。

.最近最少使用(LRU)列被用来在cache中保留住被频繁访问的buffers并找到free buffers。

.写列表包含不久将被写入磁盘的dirty buffers。

重要的是理解buffer headers的这三个列表而不是实际的buffers。单个buffer header总是内置在一个CBC中和一个LRU链或一个写列表中。

三个列表的维护是在buffer header级别,不是buffer级别,更不是在数据块级别。我们许多人被教导当buffer内置在buffer cache中时,buffers它们本身是被链接的。这是不正确。每个buffer都与一个buffer header相关联,并且在各种列表中操作的是buffer header。

Cache Buffer Chains
简而言之,CBCs被用来回答“这个buffer是否在buffer cache中,如果在,它在哪里”这本质上是一个搜索类型的问题。很多类型的搜索算法可能被用来获得答案:二叉树,B+树,B*树,顺序搜索,哈希算法,或一些算法组合。Oracle选择使用一种哈希算法,紧接着使用快速顺序搜索。

哈希算法
哈希算法可以非常快速,因为整个结构通常被存储在内存中并且要求一个单独的数学计算,同时存在一些内存访问来回答搜索问题。哈希结构有许多变化,但所有的哈希结构都是由一个哈希函数,哈希桶与哈希链组成的。

哈希函数
哈希函数接收输入并使用定义的范围来产生一个输出。输入被叫作一个哈希值。x mod 10函数可以简单地被用来确保不管输入的正数哈希值,它的输出总是在0到9之间。哈希值输入11,输出将是1。一个好的哈希函数将会产生均匀分布的输出。当Oracle将要搜索一个buffer时,基于数据块的文件号与块号的组合(它也叫数据块地址DBA)来生成一个哈希值。因此哈希函数本质上是对buffer的数据块文件号和块号进行哈希运算。这是一种非常方便并可以快速哈希运算的情况。

哈希桶
哈希输入值将被哈希到桶,每个输出值代表了一个单独的桶。在许多哈希情况下,可能输入的哈希值的数量超过桶数。对于Oracle来说,可能的哈希输出值就是Oracle数据块的数量。但在任何情况下,哈希输入值的数量将与buffercache中的buffers的数量相等。

当有两个哈希值被哈希到相同的桶时,这叫作碰撞。碰撞对于哈希来说是很常见的。碰撞可以通过增加哈希桶的数量来最小化,但可能对于高性能程序来说是一种灾难,比如Oracle数据库。例如,假设x mod 10的哈希函数有1000哈希输入值,这将肯定会出现碰撞。为了避免碰撞,哈希算法输出完全均匀的输出将需要1000个哈希桶。使用一种极好的哈希算法与大量的哈希桶两种方法减少碰撞。如果哈希算法不变,那么可以增加哈希桶的数量。

哈希链
每个哈希桶都有一个相关联的哈希链。当一个搜索的对象被哈希到一个桶时,这个桶的链被顺序搜索来查找对象。如果对象在哈希链中没有找到,我们知道对象不在整个哈希结构中。如果哈希链很短,顺序搜索将很快完成。如是对象不在cache中,链长度最好为零。

Oracle的CBC结构是一种复杂的内存结构,并且Oracle必须要维持序列化控制。所以它使用了一种序列化结构:latch或mutex。

如何破坏CBC的性能
要学习如何解决性能问题的最好方法就是知道如何模拟问题,有三种典型的方法来降低CBC的性能:
.当减少latches的数量时,剩余latches的并发将会增加
.如果减少CBCs的数量,平均每个CBC的长度将会增加,剩余chains的并发与CBC的扫描时间也会增加
.如果buffer克隆变得激烈,那么频繁访问的chain将变得很长,会增加并发与CBC的扫描时间

减少latches来限制并发
使用单个latch,序列化将被保证,但是并发性将受到严重的限制。当另一个进程请求latch时而它被其它进程所持有时就会产生竞争。在这个例子中,简单地增加一个latch可以解决这个问题。如果存在上百成千个进程需要访问CBCs,那么可以看到存在严重的并发性能限制问题。幸运地是缺省情况下Oracle创建了上百个CBC latches。

Oracle知道它的哈希函数不完美并且将会产生碰撞。一种减少碰撞的方法是有大量的CBCs。但你第一反应会觉得更多的CBCs将会消耗更多的内存,但事实不是这样的。每个buffer header必须内置在一个CBC链上,与CBC链的数量及长度无关。当使用更多的CBC链时,而buffer headers的数量不变时,平均CBC链的长度会减小。因此,对于每个CBC链虽然有一些额外的内存消耗,但真正的内存消耗者是buffer headers的数量,不仅仅是CBC链的数量。

许多年以前规则定义latches的数量不应该超过CPU核数的两倍。很明显Oracle已经修改了规则,CBC latches只是Oracle数据库中许多latches中的一种。

Oracle可能处理多个CBC latches,有人会认为对于每个CBC将有一个latch,但Oracle认为这是不必要的且一个latch可以管理上百个CBC链。

如果CBC链比buffers多,这意味着有一些CBC链将不会关联buffer header,这将有效的使CBC链的长度变为零。

[oracle@jytest2 ~]$ sqlplus / as sysdba

SQL*Plus: Release 12.2.0.1.0 Production on Thu Mar 21 10:28:02 2019

Copyright (c) 1982, 2016, Oracle.  All rights reserved.


Connected to:
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production

SQL> col param format a50 heading "Instance Param and Value" word_wrapped
SQL> col description format a20 heading "Description" word_wrapped
SQL> col dflt format a5 heading "Dflt?" word_wrapped
SQL> select rpad(i.ksppinm, 35) || ' = ' || v.ksppstvl param,
  2  i.ksppdesc description,
  3  v.ksppstdf dflt
  4  from x$ksppi i,
  5  x$ksppcv v
  6  where v.indx = i.indx
  7  and v.inst_id = i.inst_id
  8  and i.ksppinm in
  9  ('db_block_buffers','_db_block_buffers','db_block_size',
 10  '_db_block_hash_buckets','_db_block_hash_latches'
 11  )
 12  order by i.ksppinm
 13  /

Instance Param and Value                           Description          Dflt?
-------------------------------------------------- -------------------- -----
_db_block_buffers                   = 97136        Number of database   TRUE
                                                   blocks cached in
                                                   memory: hidden
                                                   parameter

_db_block_hash_buckets              = 262144       Number of database   TRUE
                                                   block hash buckets

_db_block_hash_latches              = 8192         Number of database   TRUE
                                                   block hash latches

db_block_buffers                    = 0            Number of database   TRUE
                                                   blocks cached in
                                                   memory

db_block_size                       = 8192         Size of database     FALSE
                                                   block in bytes

引起CBC latch竞争的最好和最简单的方法之一就是创建一个大的buffer cache来缓存更多的块,然后将CBC latches的数量减少到一个。Oracle从10g开始就不允许CBC latches的数量小于1024,但是即使有1024个CBC latches和足够的逻辑IO能力,也能经常看到CBC latch竞争。

通过减少CBC的数量来增加CBC的扫描时间
如果CBCs很长,那么扫描它的时间将会引起显著的竞争。另外其它进程获得CBC latch的时间也会显著增强。一种很明显的方式是增加平均每个CBC的长度来减少CBC的数量,这可以通过减少哈希桶的数量来完成。简单地将实例参数_db_block_hash_buckets减少到50,确保你查询的块内置在buffer cache中,那么会很快得到CBC latch竞争。因为Oracle至少要确保64个哈希桶来忽略你的设置,但这仍然会有大量的竞争。

在现实中,一种解决CBC latch竞争的方法是增加哈希桶的数量,这将减少平均每个CBC的长度。如果一个特定的CBC很长且被频繁文章,那么这个解决方案将不能提高性能。此外Oracle创建了大量的CBC,因此增加哈希桶的数量不像增加CBC一样能显著的提高性能,但它有一种有效的方法应该值得考虑。

使用克隆Buffers来增加CBC的扫描时间
虽然长CBC的问题很少见,但如果出现了,那么情况是很严重的。理解这是如何发生的不仅仅可以帮助你解决这个问题还能更深入的理解CBCs,latch,undo与读一致性。它涉及RAC系统。

长CBC代表了一个非常有挑战性的问题。首先,哈希结构是很快速的因为几乎没有扫描,因此长CBC会迅速降低使用哈希算法的好处。第二,一个扫描进程必须处理一个CBC latch,不是随便一个CBC latch,这个CBC latch保护特定的CBC。一个长CBC意味着CBC latch将被持有更长时间并且当扫描列表将使用更多的CPU。另外,因为CBC latch被持有的时间更长,这将增加另外的进程竞争latch的可能性。当竞争latch的进程在spinning与在sleeping时发布等待事件时都是要消耗CPU的。但问题远不止如此。

正常情况下,Oracle的哈希算法使用的CBC的数量是buffers的两倍还多,因此CBC的长度很短。长CBC出现的唯一方式是多个buffers被哈希到相同的CBC上。通常这不是一个问题,但也可能出现。为了解析这种情况,先了解块克隆与哈希。当一个Oracle块被cached后,只有单个当前模式buffer能被修改。如果buffer中的一行需要被修改,单个当前模式buffer必须是可用的。当前模式buffers有时也叫CU buffers。在RAC系统中,如果需要的当前模式buffer内置在另一个实例中,那它必须被发送到你使用的这个实例中然后才可以修改buffer。

假设一个服务器进程在时间T100正运行一个查询。这个进程访问数据字典并知道它将必须访问一个特定块,因此它将被哈希到合适的CBC,获取合适的CBC latch,扫描CBC,并找到当前模式buffer的buffer header。然而在检查buffer header时,发现当前模式buffer在时间T200被修改过,是在服务器进程开始执行查询之后。这意味着在查询执行后需要的行记录已经被修改过了。 Oracle的缺省读一致性模式要求被返回的信息与查询开始执行时的一致。因此Oracle必须采取操作来确保被返回的信息对于时间T100来说是正确的。

Oracle现在要么找到一个buffer的副本,要么构建一个当前模式buffer的副本,因此这个buffer代表了时间T100所处处的情况。一个buffer副本通常叫做buffer克隆。克隆一个buffer是一种相对昂贵的处理。首先,必须找到一个free buffer,然后buffer header必须被合适的连接到CBC结构与LRU链结构。

理解潜在的重大性能影响的关键是理解被克隆的buffer的buffer header将内置在CBC结构中的什么位置。因为被克隆的buffer是一个合法的buffer,它在buffer cache中占据了空间,能被共享且必须被定位。这意味着它必须被合适的内置在CBC结构中。被克隆的buffer的文件号与块号与它的当前模式buffer的相同,这意味着它必须被哈希到相同的CBC。因此,如果一个buffer有50个克隆副本,与它相关的CBC将至少有50个buffer header那么长,并且如果与其它buffer出现碰撞可能更长。Oracle对此无能为力,因为哈算法是基于文件号与块号的。

不仅free buffer搜索算法有利于替换克隆的buffer,但Oracle试图限制每个buffer的克隆数量。Oracle想要每个buffer的克隆数量不超过隐含参数_db_block_max_cr_dba,它的缺省值为6。然而如果克隆变得很激烈,一个buffer的克隆副本很容易超过6个。

SQL> col name for a30
SQL> col value for a20
SQL> col describ for a50
SQL> select x.ksppinm NAME,y.ksppstvl value,x.ksppdesc describ
  2  from x$ksppi x, x$ksppcv y
  3  where x.inst_id=USERENV('Instance')
  4  and y.inst_id=USERENV('Instance')
  5  and x.indx=y.indx
  6  and x.ksppinm like '%&par%';
Enter value for par: _db_block_max_cr_dba

NAME                           VALUE                DESCRIB
------------------------------ -------------------- --------------------------------------------------
_db_block_max_cr_dba           6                    Maximum Allowed Number of CR buffers per dba

1 row selected.

有许多克隆的buffer不一定意味着有性能问题。如果真的出现性能问题,CBC latch竞争问题将非常明显。如果出现这种情况并发现克隆buffer的问题,那么考虑以下可能的补救措施:
.修复应用程序
这通常是必须要做的。这是非常痛苦的,需要开会,如果应用程序开发者参与将会非常专业化,并且通常要求应用程序以某些方式被修改来减少单个克隆buffer被频繁的访问。

.移动行记录
如果幸运的话,可能存在多行记录使得buffer被频繁访问。如果可能散这些行,因此多个buffer现在不再被频繁的访问。当修改传统的pct_free与pct_used存储参数是一种选择时,为了增加控制,可以考虑设置一个块可以存储的最大记录数。意外地是这不仅仅是简单地执行类似于alter table all_status minimizer records_per_block 5语句

.平衡工作负载
如果能控制工作负载强度,在克隆活动高峰期间,考虑减少与buffer克隆活动相关的工作负载。虽然这不是一个令人兴奋的解决方案,工作负载平衡也能对性能产生积极影响。

CBC竞争识别与解决方案
一些解决方案可以帮助你解决CBC竞争的问题。在尝试解决CBC latch问题之前,确保它们存在。

SQL> @swpctx
Remember: This report must be run twice so both the initial and
final values are available. If no output, press ENTER twice.

DB/Inst: RLZY/RLZY1                                               25-Mar 11:24am
Report:   swpctx.sql           OSM by OraPub, Inc.                Page         1
            System Event CHANGE (17 sec interval) Activity By PERCENT

                                       Time Waited  % Time    Avg Time     Wait
Wait Event Display Name                      (sec)  Waited Waited (ms) Count(k)
-------------------------------------- ----------- ------- ----------- --------
latch: cache buffers chains                 10.610   96.28        15.7        1
control file parallel write                  0.160    1.45         7.6        0
log file parallel write                      0.030    0.27        15.0        0
log file sync                                0.000    0.00         0.0        0

如果数据库系统是Oracle 10g之前的版本,那么top wait event将会是latch free,就需要确认latch问题是CBClatch。对于Oracle 10g及以后的版本,wait event将是latch: cache buffers chains。在大多数情况下,CPU子系统将被大量利用并且负担过重。以下是可能的CBC latch解决方案:
.优化逻辑IO SQL语句
当回答“buffer是否在buffer cache”中时CBC结构将变得紧张起来,期待的答案总是“Yes”,如果答案为“No”,将会看到顺序读或分散读等待事件。因此从应用程序角度来看,查找执行活动主要是buffer gets也就是逻辑IO的SQL尽你所能地减少逻辑IO消耗。这是典型的SQL优化,包括索引,以及在性能问题出现时减少执行速率。

.增加CPU处理能力
在大多数情况下,CPU子系统将被过多利用并且可能是操作系统瓶颈。latch的获得与相关的内存管理可能消耗过多的CPU资源。做任何可以减少CPU消耗与能增加CPU能力的事。查找在高峰期间没有执行或正在执行的进程。考虑增加或者使用更快的CPU。如果正在运行在虚拟环境中,考虑确保Oracle系统已经增加CPU资源。然而,请注意除非应用程序工作负载已经显著增加,增加的CPU处理能力通常将被快速地消耗掉。真正的解决方案可能是其它的方案。增加CPU能力可能是一个快速解决方案,但它可能不能真正地解决问题。

.检查buffer克隆问题
无论何进遇到CBC latch竞争问题,都需要检查是否存在buffer克隆的问题。这是很少见的情况,但如果遇到了,那么解决方案与其它解决方案是非常不同的。

.增加CBC latch数量
这通常会带来一些安慰,但不是真正的优化逻辑IO SQL。隐含参数_db_block_hash_latches控制着CBC latch的数量

.增加CBC buckets
它很难对性能产生影响,因为Oracle缺省情况下,创建了大量的buckets。除非之前减少了CBC buckets的数量,增加这个参数的大小将会显著地影响性能。