绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
揭密Oracle之七种武器之四:揭密Buffer Cache中的链表
2020-06-30 13:39:56
揭密Oracle之七种武器之四:揭密Buffer Cache中的链表 揭密Oracle之 七种武器 第一章 搭建测试环境(目前已到第三章) http://www.itpub.net/thread-1605241-1-1.html 揭密Oracle之七种武器二:DTrace语法:跟踪物理IO http://www.itpub.net/thread-1609235-1-1.html 揭密Oracle之七种武器之三:破译古老的谜题---共享CBC Latch的秘密 http://www.itpub.net/thread-1617245-1-1.html 还有一篇补遗: 揭密buffer Cache中的链表补遗 http://www.itpub.net/thread-1632432-1-1.html 有日了没写东西了。又是看房又是讲课,好多朋友问我是不是不写了,怎么会呢,分享知识,也是自我总结的一个过程,对自己的提高也是有帮助 的吗。 前段时间,一直有人问我Buffer Cache的链表,LRU、辅助LRU、检查点队列等等。检查点队列已经有很多文章讨论过了,我就不再重复的制造轮子 。 另外,还有主LRU冷热端的相关内容,这一块我也不再详细描述,因为也有相关的文章。 我主要说一下主LRU、辅助LRU和LRUW相关的内容。 本篇文章没有使用DTrace和GDB,难度较低,但实验内容较多。我先将结果写在前面,如果没时间按个看实验,大概了解一下也行。 1、辅助LRU的定位,是让进程可以在辅助LRU中尽快找到可用块,因此,辅助LRU中存放的,都是相较主LRU,更加没有用的块。比如全表扫描的 块,都会被放在辅助LRU。另外,当空间紧张时,前台进程会将脏块移到LRUW,这些从LRUW写出的脏块,写完成后也会放入辅助LRU。这样辅助LRU中块更 多,前台进程更加容易在辅助LRU中找到可用块。 2、非全表扫描,先到辅助LRU中寻找可有块,找到后会将其从辅助LRU中,移到主LRU冷端头。 3、如果非全表扫描较多,辅助LRU中块会越来越少,为了保持比例(辅助LRU占整个LRU总块数的20%到25%左右),SMON进程会3秒一次持有LRU Latch,将主LRU冷端末的块移到辅助LRU。 4、全表扫描也先到辅助LRU中寻找可用块,但全表扫描的块仍将留在辅助LRU,不会调往主LRU冷端头。因此全表扫描的块将很快被覆盖。全表 扫描操作将只使用辅助LRU(也会用到主LRU,只会用到很少量的主LRU),一次大的全扫操作,可以将辅助LRU的所有块覆盖一遍或多遍。 5、数据库刚启动时,或刚Flush Buffer_cache时,所有块会被放在辅助LRU中。 6、前台进程(服务器进程)扫描主、辅LRU时,会将遇到的TCH为2以下的脏块,移到主LRUW。 7、DBWR三秒一次,扫描检查点队列的长度、确定是否要写脏块。另外,DBWR每三秒还会扫描主LRUW,将其中的脏块移到辅助LRUW,然后写到磁 盘。 8、从检查点队列写到磁盘中的块,不会改变它在LRU链表中的位置。从LRUW写到磁盘中的块,会被放于辅助LRU,以供马上覆盖。 基本上就这些了。下面开始吧。本篇比较简单,没有DTrace,更进一步的DTrace测试,放在下篇中吧。只是实验较多,繁琐了些。

第 一 章 一个测试理解什么是主、辅LRU

LRU是Buffer Cache池中的重要链表,它的作用我不再详述,已经有很多相关资料。这次主要和大家讨论下主LRU、辅助LRU的作用。 先来看一个测试。 步1:环境介绍 先来看看Buffer Cache的大小: SQL> show sga Total System Global Area 1073741824 bytes Fixed Size 1284344 bytes Variable Size 960497416 bytes Database Buffers 104857600 bytes Redo Buffers 7102464 bytes Buffer Cache大小100M。 再来看看测试表大小: SQL> set linesize 1000 SQL> col segment_name for a30 SQL> SQL> select segment_name,bytes/1024/1024 from dba_segments where segment_name='A3_70M'; SEGMENT_NAME BYTES/1024/1024 ------------------------------ --------------- A3_70M 80 SQL> SQL> select segment_name,bytes/1024/1024 from dba_segments where segment_name='A4_70M'; SEGMENT_NAME BYTES/1024/1024 ------------------------------ --------------- A4_70M 80 两张测试表,A3_70M、A4_70M,各自大小80M。本来是想把它们两个大小建为70M的,所以名字带有后缀_70M,但建的大了一点,不过,这无所 谓,不会影响我们的测试结果。 步2:刷新Buffer Cache池,观察LRU链长度 SQL> alter system flush buffer_cache; SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 6238 6238 6219 0 0 6237 6237 6224 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 16 rows selected. 这个视图返回16行,但只有两行有数据。其他行都是0,原因是什么? 查一下select name from v$latch_children where name ='cache buffers lru chain'; 就可以知道,我一共有16个cache buffers lru chain Latch,但只有两个被使用了。 每一个cache buffers lru chain latch,都算作一个“工作组”,work set。意义是指每个cache buffers lru chain latch,都对应一组主 、辅LRU链,和脏LRU链。 x$kcbwds视图中CNUM_SET列,统计工作组所有块数量。简单点说,也就是LRU链上所有块的数量。 CNUM_REPL列是主、辅LRU链表中所有的块数。 ANUM_REPL列是辅LRU链上所有的块数。 用CNUM_REPL-ANUM_REPL,即为所有主LRU链上的块数。 CNUM_WRITE、ANUM_WRITE这个列对应LRUW(也即为脏LRU)链,CNUM_WRITE为LRUW链中所有块数,ANUM_WRITE为辅助LRUW中的块数。两个相减, 要以得到主LRUW中的块数。 这个测试中,先不用观察LRUW。我们先只关注主、辅LRU。 来看我们的显示结果吧,两个工作组加起来,一共12475个块。 另外,可以看到,CNUM_REPL的ANUM_REPL列的几乎相等,这说明绝大多数块,都在辅助LRU链表中,这是为什么呢?这是我们今天第一点结论, flush了buffer cache、或刚刚开启数据库时,所有的可用Buffer都会链接到辅助LRU中。 步3:先查询a3_70M,再查询a4_70M,分别观察物理读。(注意顺序,先查询A3_70M,再查询A4_70M) SQL> set autot trace SQL> select count(*) from a3_70m; Execution Plan ---------------------------------------------------------- Plan hash value: 850873958 --------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | --------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 1643 (1)| 00:00:20 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | TABLE ACCESS FULL| A3_70M | 1511K| 1643 (1)| 00:00:20 | --------------------------------------------------------------------- Statistics ---------------------------------------------------------- 207 recursive calls 0 db block gets 9292 consistent gets 9271 physical reads 0 redo size 414 bytes sent via SQL*Net to client 381 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 4 sorts (memory) 0 sorts (disk) 1 rows processed SQL> select count(*) from a4_70m; Execution Plan ---------------------------------------------------------- Plan hash value: 2047399966 --------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | --------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 1643 (1)| 00:00:20 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | TABLE ACCESS FULL| A4_70M | 1502K| 1643 (1)| 00:00:20 | --------------------------------------------------------------------- Statistics ---------------------------------------------------------- 207 recursive calls 0 db block gets 9292 consistent gets 9259 physical reads 0 redo size 414 bytes sent via SQL*Net to client 381 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 4 sorts (memory) 0 sorts (disk) 1 rows processed 可以看到,第一次查询A3_70M和A4_70M,物理读分别是9200多。 步4:再次查询a3_70M,再查询a4_70M,分别观察物理读。(顺序无所谓,先查谁都可以) SQL> select count(*) from a3_70m; Execution Plan ---------------------------------------------------------- Plan hash value: 850873958 --------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | --------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 1643 (1)| 00:00:20 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | TABLE ACCESS FULL| A3_70M | 1511K| 1643 (1)| 00:00:20 | --------------------------------------------------------------------- Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 9269 consistent gets 213 physical reads 0 redo size 414 bytes sent via SQL*Net to client 381 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed SQL> select count(*) from a4_70m; Execution Plan ---------------------------------------------------------- Plan hash value: 2047399966 --------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | --------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 1643 (1)| 00:00:20 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | TABLE ACCESS FULL| A4_70M | 1502K| 1643 (1)| 00:00:20 | --------------------------------------------------------------------- Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 9269 consistent gets 9135 physical reads 0 redo size 414 bytes sent via SQL*Net to client 381 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed 发现没有,A3_70M的物理读在第二次查询时大量下降,只有213次。而A4_70M在第二次查询时,物理读下降很不明显,还是9200次左右。 可以再测个几遍,情况依旧。A3_70M的物理读很少,而A4_70M的物理读很多。 这是我们今天的问题一:考虑这是为什么。 下面继续我们的问题2。 步5:连续两次查询A3_70M和A4_70M SQL> select count(*) from a4_70m; Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 9269 consistent gets 8686 physical reads 0 redo size 414 bytes sent via SQL*Net to client 381 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed SQL> select count(*) from a4_70m; Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 9269 consistent gets 8722 physical reads 0 redo size 414 bytes sent via SQL*Net to client 381 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed 这是两次A4_70M的结果对比,多次查询A4_70M之后,物理读下降到了8700多,然后不再下降。 再来看两次连续查询A3_70M的结果对比: SQL> select count(*) from a3_70m; Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 9269 consistent gets 600 physical reads 0 redo size 414 bytes sent via SQL*Net to client 381 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed SQL> select count(*) from a3_70m; Statistics ---------------------------------------------------------- 0 recursive calls 0 db block gets 9269 consistent gets 0 physical reads 0 redo size 414 bytes sent via SQL*Net to client 381 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed 第一次有600次物理读,第二次就没有了。 问题2出来了,考虑这是没什么。 步6:暂时总结 1、A3_70M和A4_70M表大小一样。行数一样、所占空间也一样,都是80M 2、第一次查询时,我们是先查询A3_70M,再查A4_70M。如果你反过来,先查A4_70M,将看到A4_70M的物理读在第二次查询时大大减少,连续两 次查询时物理读为0。 两个问题: 1、为什么第一次查询时,先查的表,在第二次查询时物理读会大大减少,而后查的表则不会。 2、为什么第一次查询时,先查的表连续两次查询时物理读会减少为0,而后查的表不会。 一个提示:可以观察一下我们开始提到的x$kcbwds视图。 步7:如何观察块处在哪个链上。 如何进一步分析这个问题?其实这两个问题的回答,都需要着落到一件事上,就是要分析两个表对应的块,分别在什么链上。是在主LRU,还是 辅LRU。如何进行这个观察呢?其实很简单,x$bh中有个LRU_FLAG列,通过这个列,就可以确定块在哪个链表上。 我们来看一下这个视图: SQL> select lru_flag,count(*) from x$bh group by lru_flag; LRU_FLAG COUNT(*) ---------- ---------- 6 3081 2 6734 8 2 0 2658 在我的测试库中,我观察到的LRU_FLAG有4种值,6、2、8和0。这4个值分别代别什么意义呢?确定这个其实很简单,DUMP一下块对应的Buffer 就行了,以LRU_FLAG为6的为例: (1)、随便查找LRU_FLAG为6的一行: SQL> select lru_flag,file#,dbablk,TS# from x$bh where LRU_FLAG=6 and rownum=1; LRU_FLAG FILE# DBABLK TS# ---------- ---------- ---------- ---------- 6 5 12459 4 (2)、如下DUMP一下这个BUffer: SQL> alter session set events 'immediate trace name SET_TSN_P1 level 5'; Session altered. SQL> alter session set events 'immediate trace name BUFFER level 0x014030ab'; Session altered. 上面这两个命令,“SET_TSN_P1 level 5”中的5,是表空间号加1得到的。也就是TS#+1。 第二个命令中“BUFFER level 0x014030ab”,其中0x014030ab,是5号文件12459号块的DBA。 这个DBA是如何计算出的?其实我是在另一个会话中DUMP一下5号文件12459号块,查看DUMP结果才得到的,Oracle也提供了一个包,其实有个函 数可以根据文件号、块号生成DBA,包名、函数名我都忘了,只好用笨方法。 好,来查看DUMP结果吧,到user_dump_dest目录中,找到日期最靠后的(我一般是ls -lFrt),它就是我们刚刚生成的DUMP文件。在它的开头, 我们可以看到如下内容: *** 2012-07-03 08:58:36.571 *** SERVICE NAME:(SYS$USERS) 2012-07-03 08:58:36.523 *** SESSION ID:(874.3) 2012-07-03 08:58:36.523 Dump of buffer cache at level 10 for tsn=4, rdba=20983979 BH (7bbf0764) file#: 5 rdba: 0x014030ab (5/12459) class: 1 ba: 7b9dc000 set: 5 blksize: 8192 bsi: 0 set-flg: 0 pwbcnt: 0 dbwrid: 0 obj: 9738 objn: 9738 tsn: 4 afn: 5 hash: [7d7e687c,8e94b67c] lru: [7bbe80b8,7bbf0704] lru-flags: moved_to_tail on_auxiliary_list ckptq: [NULL] fileq: [NULL] objq: [7bbf075c,7bbe8110] st: XCURRENT md: NULL tch: 0 flags: only_sequential_access LRBA: [0x0.0.0] HSCN: [0xffff.ffffffff] HSUB: [65535] buffer tsn: 4 rdba: 0x014030ab (5/12459) scn: 0x0000.00418edb seq: 0x01 flg: 0x06 tail: 0x8edb0601 frmt: 0x02 chkval: 0xc400 type: 0x06=trans data 我对这里面的东西,不过多解释了,其实有一行: lru-flags: moved_to_tail on_auxiliary_list 这一行说明了块对应Buffer所在LRU链表的状态。Buffer目前在辅助LUR,moved_to_tail说明Buffer正移向链表末尾。 好了,我们已经总结出来,LRU_FLAG列为6,说明Buffer在辅助LRU链表。 其他的我不再一一列出测试过程了,总结一下,LRU_FLAG为6、4,说明BUffer在辅助LRU中。为0、2,说明在主LRU的冷端,为8、9的,说明在 主LRU的热端。 步8:问题1:为什么第一次查询时,先查的表,在第二次查询时物理读会大大减少,而后查的表则不会 让我们来观察一下A3_70M、A4_70M 的块都在什么链上 SQL> select lru_flag,count(*) from x$bh a,dba_objects b where a.obj=b.data_object_id and object_name='A3_70M' group by lru_flag; LRU_FLAG COUNT(*) ---------- ---------- 2 8554 0 1 SQL> select lru_flag,count(*) from x$bh a,dba_objects b where a.obj=b.data_object_id and object_name='A4_70M' group by lru_flag; LRU_FLAG COUNT(*) ---------- ---------- 6 3082 2 366 0 1 这两条语句,我就不再过多解释了,根据LRU_FLAG列的值,我们可以确认,A3_70M,共有8555个块在Buffer Cache中,所有相关的块都在主LRU 链。A4_70M,只有3400多个块在Buffer Cache中,而且大部分块都在辅助LRU。 为什么A3_70M的块,被读进Buffer Cache时,大多被放置在主LRU的冷端?而A4_70M的块,则被放进辅助LRU呢? 能回答这个问题,我们今天的问题也就有了答案。 步9:回答问题1 有如下三点基础知识: 1、当数据库刚刚打开、或刚刚做过FLush Buffer_cache操作时,所有的块,都被放进辅助LRU。 2、进程寻找可用块时,先会在辅助LRU中查找,然后才会到主LRU中找。 3、Oracle会保持20%至25%左右的块在辅助LRU链,80%的在主LRU。这一点,从观察x$kcbwds中的CNUM_REPL、ANUM_REPL列,可以很容易的验证 。 有了这三点基础知识,让我们来得出结论吧: 当所有块都在辅助LRU时,Oracle为了急于保证主、辅LRU,块数量80%,20%的比例,第一次查询时,会将大量的块移到主LRU。也就是说,第一 次查询A3_70M时,大至步骤是这样的,分两种情况: 一、主LRU几乎为空时: 1、在辅助LRU找到可用块。 2、将它移到主LUR 3、将数据读入此块。 二、当主、辅LRU的块数量比例到达80%、20%后 1、在辅助LRU找到查用块 2、所处链表不变,直接将数据读入此块。 3、物理读完成后,块还在辅助LRU中。 由于第一次查询A3_70M时,主LRU几乎为空,所以,它的绝大部分的块被放在了主LRU,少部分块被放入了辅助LRU。 根据比例,辅助LRU中将会有3000到3200个左右的块。根据我们的测试结果,全表扫描A4_70M后,有3000多个块在辅助链上。根据这个,得出我 们今天第二个结论,正常的全表扫描操作,将只会反复使用辅助LRU中的块。 当然,我们的测试中,还是有少量A4_70M的块进入到了主LRU,不过数量不多,只有几百个。 好了,回答一开始的问题吧,第一次查询A3_70M,Oracle会了保持主、辅LRU的比例,将很多被A3_70M占用的块移进了主LRU,所以,A3_70M在 Buffer Cache中可以有很多块,再次查询A3_70M时,物理读大大减少。 而全表扫描A4_70M,只能反复使用辅LRU中的块,几乎无法占用主LRU中的块,因此在Buffer Cache中块数较少,每次物理读都很高。 步10:回答问题2 为什么连续两次查询A3_70M,就没有物理读了?可以连续两次全表扫描一下A3_70M,查看结果: SQL> select lru_flag,count(*) from x$bh a,dba_objects b where a.obj=b.data_object_id and object_name='A3_70M' group by lru_flag; LRU_FLAG COUNT(*) ---------- ---------- 6 1004 2 8254 0 1 一共9000多个块,8000多个在主LRU,1000多一点在辅助LRU。A3_70M表的全部块,都可以被缓存到Buffer Cache中。 当再次查询A4_70M时,A4_70M将又反复占用辅助LRU中的块,A3_70M留在辅助LRU中的1000多一点块会被覆盖,所以,当A3_70M、A4_70M交替查 询时,每次A3_70M在辅助LRU中的块都会被覆盖,因此,它会有少量物理读,但,当连续查询A3_70M时,辅助LRU中的块没被覆盖,就不会有物理读了。 好,问题已经解答了,让我们总结一下吧。 总结: 1、flush了buffer cache、或刚刚开启数据库时,所有的可用块都会链接到辅助LRU中。 2、查找可用块的过程,是先找辅助LRU,再从主LRU的冷端尾开始找。 3、全表扫描在辅助LRU找到块后,不会将块读进主LRU。所以,全表扫描的结果很容易被覆盖。 4、非全表扫描时,在辅助LRU找到块后,会将块移到主LRU的冷端头。这一点我们还没验证过。 5、全表扫描的块,如果再次以非全表扫描方式访问,TCH列会增加,但会一直留在辅助LRU,不会被移到主LRU冷端头。只有等到TCH值超过2时,才会在 下次被扫描到时,移到主LRU热端。 第4点和第5点,我们并没用测试验证,但找个这样的测试也是很简单的。留给大家自己验证一下吧,研究Internal,结果不是目的,过程更有 意义。 最后还有一个注意事项,这个测试只能在10G下做,因为11GR2后有些改变,即使是开库后第一次全表扫描,也不会把块移往主LRU,这样其实更合理,Oracle其实一直没有停止对内核优化步伐。 另外,本篇中除这个测试外,其他测试都可以在10G、11GR2下做。

第 二 章 LRUW

脏块什么时候会进入LRUW ? 流行的说法有以下几种: 1、DBWR 每3秒醒来时,会将脏块收集到LRUW,当LRUW中脏块达到一定阀值时,将触发DBWR写LRUW中的脏块。 2、当前台进程(也就是服务器进程)扫描LRU时,会将发现的脏块移到LRUW中,当LRUW中脏块达到一定阀值时,将触发DBWR写LRUW中的脏块。 3、当前台进程扫描LRU的一定数量的块后,都没有发现可用块,此时会触发紧急写。前台进程等待,DBWR将脏块从检查点队列移到LRUW,从 LRUW写到磁盘。 以上这三种说法,第一种并不存在,或者说,都观察不到。 通过x$kcbwds视图的CNUM_WRITE、ANUM_WRITE列,就可以观察到主、辅LRUW链的长度。可以一直不停的多观察几十秒,没有发现这两个列不为0 。3秒的说法,不功自破。 但是,并不能说3秒写磁盘的操作不存在,只是,并不是以这种形式。 第二种说法,流传很广,也的确会有这种情况出现,但真实情况和流行说法还是会有不同。 具体怎么来验证一下呢。先来做个测试: 步1:环境配置: 写脏块有两种途径:通过检查点队列、通过LRUW。为了避免从检查点队列写影响我们的测试结果,我们要将增量检查点关闭。 其实,不是关闭,而是将增量检查点周期设的很大,在我们的测试期间,保证它不会触发。 需要设置如下几个参数: SQL> show parameter log_checkpoint_timeout NAME TYPE VALUE ------------------------------------ ----------- ------------------------------ log_checkpoint_timeout integer 100000 SQL> show parameter mttr NAME TYPE VALUE ------------------------------------ ----------- ------------------------------ fast_start_mttr_target integer 0 将log_checkpoint_timeout设置为了10万秒。fast_start_mttr_target设置为0。 另外,10G之后,fast_start_mttr_target设置为0时,Oracle将打开一个Self Tune Checkpoint(自调节检查点)的功能,将它也关闭: SQL> alter system set "_disable_selftune_checkpointing"=true; System altered. 经过这样的设置,增量检查点虽然没有关闭,也和关闭差不多了,10万秒才会被触发。 当然,日志切换时还会触发一个“日志切换时的增量检查点”,虽和普通的增量检查点有不同,但大体上类似增量检查点。这个只要建个稍大 点的日志文件,就可以解决,比如,我的日志文件是500M。 步2:观察当前系统中的脏块 SQL> select count(*) from v$bh where dirty='Y'; COUNT(*) ---------- 46 步2:在另一个会话中,扫描个非全表扫描 select /*+index(a2_70m)*/ * from a2_70m where id1<=100000000; 索引范围扫描,将先找辅助LRU、再找主LRU。 步3:在步2中的索引范围扫描执行期间,不停的显示: select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; select * from v$sysstat where name in ('dirty buffers inspected'); 第一条语句可以查看LRUW的长度信息,第二条语句,可以观察前台进程在扫描LRU时,跳过的脏块数。我们可以观察到如下结果: …………………………………………省略一部分相同的结果…………………………………………………… SQL> select * from v$sysstat where name in ('dirty buffers inspected'); STATISTIC# NAME CLASS VALUE STAT_ID ---------- ---------------------------------------------------------------- ---------- ---------- ---------- 94 dirty buffers inspected 8 650 1344569897 SQL> SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 6238 6238 1522 0 0 6237 6237 1541 0 0 SQL> select * from v$sysstat where name in ('dirty buffers inspected'); STATISTIC# NAME CLASS VALUE STAT_ID ---------- ---------------------------------------------------------------- ---------- ---------- ---------- 94 dirty buffers inspected 8 650 1344569897 SQL> SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 6238 6237 1523 1 0 6237 6235 1541 2 0 SQL> select * from v$sysstat where name in ('dirty buffers inspected'); STATISTIC# NAME CLASS VALUE STAT_ID ---------- ---------------------------------------------------------------- ---------- ---------- ---------- 94 dirty buffers inspected 8 653 1344569897 SQL> SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 6238 6237 1520 1 0 6237 6235 1539 2 0 SQL> select * from v$sysstat where name in ('dirty buffers inspected'); STATISTIC# NAME CLASS VALUE STAT_ID ---------- ---------------------------------------------------------------- ---------- ---------- ---------- 94 dirty buffers inspected 8 653 1344569897 …………………………………………省略一部分相同的结果…………………………………………………… SQL> SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 6238 6238 1507 0 0 6237 6237 1527 0 0 SQL> select * from v$sysstat where name in ('dirty buffers inspected'); STATISTIC# NAME CLASS VALUE STAT_ID ---------- ---------------------------------------------------------------- ---------- ---------- ---------- 94 dirty buffers inspected 8 653 1344569897 …………………………………………省略一部分相同的结果…………………………………………………… 前面的观察结果,dirty buffers inspected的值一开始为650,当变为653时,同时,可以在CNUM_WRITE列看到,此列的值增加了3。 这证明有三个脏块被跳过,移到了LRUW上。但是,不同于通常的说法,没有等LRUW中脏块数达到什么阀值,很快的,这三个脏块就被写的磁盘 了。 3个块脏块,绝对没有到达任何阀值。那么,是什么情况触发的写脏块呢? 3秒,是否是三秒呢? 其实测试很简单,set time on可以打开时间提示符,打开提示符后再试: 13:23:25 SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 6238 6238 1523 0 0 6237 6237 1549 0 0 13:23:25 SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 6238 6231 1542 7 0 6237 6237 1512 0 0 13:23:25 SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 6238 6206 1510 32 0 6237 6237 1505 0 0 ……………………………………省略部分相同内容…………………………………… 13:23:26 SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 6238 6195 1509 43 0 <--------------此处主LRUW上有43个脏块 6237 6237 1515 0 0 ……………………………………省略部分相同内容…………………………………… 13:23:27 SQL> 13:23:27 SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 6238 6195 1504 43 43 <--------------主LRUW上的43个,被移到了辅助LRUW上。 6237 6227 939 10 10 ……………………………………省略部分相同内容…………………………………… 13:23:27 SQL> select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE ,ANUM_WRITE from x$kcbwds where cnum_set>0; CNUM_SET CNUM_REPL ANUM_REPL CNUM_WRITE ANUM_WRITE ---------- ---------- ---------- ---------- ---------- 6238 6238 1538 0 0 6237 6237 940 0 0 在13:23:25时,有脏块被移到LRUW,我没有将dirty buffers inspected的值显示出来,其实dirty buffers inspected正好也增加了7。之后 脏块数慢慢增加,到13:23:27,两条LRUW上的脏块已经合计53个。 而且,可能很容易观察到,13:23:27 时,有43个脏块,从主LRUW移到辅助LRUW,然后被写到磁盘。 从13:23:25 ,到13:23:27,正好3秒。 你可以做更多的测试,我的测试结果,每次LRUW上有脏块,总是不超过3秒,就会被写到磁盘。 如果你觉得这个测试太简单了,猜测的部分太多,没关系,耐心等下,后面会有DTrace版的验证方法。这里不再详述。 回到开头的流利说法: 1、DBWR 每3秒醒来时,会将脏块收集到LRUW,当LRUW中脏块达到一定阀值时,将触发DBWR写LRUW中的脏块。 2、当前台进程(也就是服务器进程)扫描LRU时,会将发现的脏块移到LRUW中,当LRUW中脏块达到一定阀值时,将触发DBWR写LRUW中的脏块。 其实这两种说法都有不准确的地方,准确说是这样的: 1、DBWR 每3秒醒来,之后它会做两件事: (1)、检查检查点队列长度,如果脏块太多、恢复时间有可能超过fast_start_mttr_target参数的值,开始沿着检查点队列写脏块。 (2)、检查LRUW,有脏块就写。 2、当前台进程(也就是服务器进程)扫描LRU时,会将发现的脏块移到LRUW中,等待3秒一次DBWR醒来,将脏块写磁盘。 前面说的流行说法还有第三点: 3、当前台进程扫描LRU的一定数量的块后,都没有发现可用块,此时会触发紧急写。前台进程等待,DBWR将脏块从检查点队列移到LRUW,从 LRUW写到磁盘。 这种说法容易理解,这是紧急情况。前台进程已经在等待Free Buffer Waits了,DBWR不会再按检查点队列依次写,而是从LRU中碰到脏块就移 到LRUW、然后写到磁盘。从LRUW写和从检查点队列写区别是什么呢?为什么这种紧急情况写,就要从LRUW写呢? 下面继续。

第 三 章 写完的脏块如何处理

从LRUW写和从检查点队列写区别是什么呢?为什么这种紧急情况写,就要从LRUW写呢? 回答这个问题,主要要看写完的脏块会如何处理。 如果脏块从检查点队列中写到磁盘,脏块在LRU、LRUW链表的位置,不会任何变化,这一点很容易证明: SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid),dbms_rowid.rowid_block_number(rowid),id1,id2 from a2_70m where id1<=1; DBMS_ROWID.ROWID_RELATIVE_FNO(ROWID) DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID) ID1 ID2 ------------------------------------ ------------------------------------ ---------- ---------- 4 20 1 10 我从A2_70M选择一行,它在4号文件20号块。 然后用如下语句观察x$bh中它的状态: set pagesize 50000 set linesize 10000 select file#,dbablk,tch,lru_flag,ba,decode(state,0,'free',1,'xcur',2,'scur',3,'cr', 4,'read',5,'mrec',6,'irec',7,'write',8,'pi', 9,'memory',10,'mwrite',11,'donated'), decode(bitand(flag,1), 0, 'N', 'Y') dirty,US_NXT,US_PRV,LRU_FLAG from x$bh a where file#=4 and dbablk=20 order by FILE# , DBABLK; 结果如下: FILE# DBABLK TCH LRU_FLAG BA DECODE( D US_NXT US_PRV LRU_FLAG ---------- ---------- ---------- ---------- -------- ------- - -------- -------- -------- 4 20 2 0 80AEA000 xcur N 80BF6694 80BF6694 0 从Dirty列可以看到,它并不是一个脏块。下面修改它: SQL> update a2_70m set id2=id2+0 where id1=1; 1 row updated. 再次查看: 22:28:54 SQL> / FILE# DBABLK TCH LRU_FLAG BA DECODE( D US_NXT US_PRV LRU_FLAG ---------- ---------- ---------- ---------- -------- ------- - -------- -------- -------- 4 20 2 0 80AEA000 xcur Y 80BF6694 80BF6694 0 已经是个脏块了。LRU_FLAG为0,说明它在主LRU上。这是因为ID1列上有索引,更新产生的读操作,是非全表扫描,所以它被放在主LRU上。全 表扫描的块,将被保留在辅助LRU中。 将增量检查点改的频繁些: SQL> alter system set log_checkpoint_timeout=5; System altered. 5秒一次,很快的,4号文件20号块已经不是脏块了,这是观察结果: 22:31:18 SQL> / FILE# DBABLK TCH LRU_FLAG BA DECODE( D US_NXT US_PRV LRU_FLAG ---------- ---------- ---------- ---------- -------- ------- - -------- -------- -------- 4 20 2 0 80AEA000 xcur N 80BF6694 80BF6694 0 虽然不是脏块了,但它的LRU_FLAG列值,US_NXT、US_PRV列值,都没有变化。 这说明脏块虽然被写到磁盘中了,但它在Buffer Cache众链表中的位置,没有变化。 再来看看从LRUW中写出的情况,将log_checkpoint_timeout重新改回很大的值: SQL> alter system set log_checkpoint_timeout=100000; System altered. 再次修改4号文件20号块: SQL> update a2_70m set id2=id2+0 where id1=1; 1 row updated. 确认它已经是脏块了: 22:43:39 SQL> / FILE# DBABLK TCH LRU_FLAG BA DECODE( D US_NXT US_PRV LRU_FLAG ---------- ---------- ---------- ---------- -------- ------- - -------- -------- -------- 4 20 1 0 80AEA000 xcur Y 80BF6694 80BF6694 0 下面将a4_70m改为CACHE。目的是让它可以进入主LRU: SQL> alter table lhb.a4_70m cache; Table altered. 全扫描一个a4_70m: select count(*) from a4_70m; 再来观察4号文件20号块: 22:51:21 SQL> / FILE# DBABLK TCH LRU_FLAG BA DECODE( D US_NXT US_PRV LRU_FLAG ---------- ---------- ---------- ---------- -------- ------- - -------- -------- -------- 4 20 1 4 7B72E000 xcur N 7B7F7E7C 7B7F7E7C 4 已经不是脏块了,但它的LRU_FLAG列值为4,说明它已经被移到辅助LRU中了。并且,它的US_NXT、US_PRV列指针也都变化了。 这就是从检查点队列和从LRUW写脏块的最大区别,从检查点队列写完脏块,脏块只是变的不脏了,其他没有任何变化。但从LRUW写完脏块,块 要被放入辅助LRU,这样,块将会被很快覆盖掉。 Oracle之所设计两种写脏块模式,就是为了应对脏块较多的紧急情况。此时如果还从检查点队列写,辅助LRU中的块数量不会增加,前台进程还 是无法快速找到可用块。如果从LRUW写,写完的块被放入辅助LRU,只要这些脏块的TCH值不超过2,它们马上就可以被其他进程重用。 好了,关于LRUW,还有一个问题,我刚才的实验,update a2_70m set id2=id2+0 where id1=1; ,这条语句,会将块放在主LRU中,如果块在 辅助LRU中,又会怎样呢?下面来测试下: 先全扫描a2_70m: SQL> select * from a2_70m; 此时查看4号文件20号块: 22:56:11 SQL> / FILE# DBABLK TCH LRU_FLAG BA DECODE( D US_NXT US_PRV WA_NXT WA_PRV LRU_FLAG ---------- ---------- ---------- ---------- -------- ------- - -------- -------- -------- -------- ---------- 4 20 1 4 7C6C2000 xcur N 7C7F5884 7C7F5884 7C7F588C 7C7F588C 4 LRU_FLAG为4,代表它在辅助LRU中,因为还没有修改,所以它不是一个脏块。 执行Update: SQL> update a2_70m set id2=id2+0 where id1=1; 1 row updated. 再查看4号文件20号块的状态: 22:56:12 SQL> / FILE# DBABLK TCH LRU_FLAG BA DECODE( D US_NXT US_PRV WA_NXT WA_PRV LRU_FLAG ---------- ---------- ---------- ---------- -------- ------- - -------- -------- -------- -------- ---------- 4 20 2 4 7C6C2000 xcur Y 7C7F5884 7C7F5884 7C7F588C 7C7F588C 4 已经是脏块了,但LRU_FLAG仍为4,仍在辅助LRU中。 好,已经达到我们的目的,在辅助LRU中制造一个脏块。 下面全扫描a3_70m,在辅助LRU中占用大量块。4号文件20号块会被覆盖吗: 22:56:28 SQL> / FILE# DBABLK TCH LRU_FLAG BA DECODE( D US_NXT US_PRV WA_NXT WA_PRV LRU_FLAG ---------- ---------- ---------- ---------- -------- ------- - -------- -------- -------- -------- ---------- 4 20 0 8 7C6C2000 xcur Y 7C7F5884 7C7F5884 7C7F588C 7C7F588C 8 没有被覆盖,LRU_FLAG状态为8,说明已经移到主LRU热端了。这是因为它的TCH值大于、等于2。但它仍是脏块。
分享好友

分享这个小栈给你的朋友们,一起进步吧。

调试数据库 ---- 源码研究方法论
创建时间:2020-06-16 17:28:11
最能让你坚持下去的源码学习方法 ---- 调试数据库。Oracle的各种DUMP、Trace和Event,增加了研究这个数据库的“乐趣”,使用Oracle成为一个可研究的数据库。开源数据库当然也可以通过钻研源码的方式去研究,但这样的学习周期太长。本课程教你用调试技术不断为MySQL/PostgreSQL扩展功能,在学习源码的同时,不断开发自己的、类似Oracle DUMP、Trace、Event的小工具,这就是我所说的“正向反馈”。用正向反馈,激励自己坚持下去,最终成功。
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • vage
    栈主

小栈成员

查看更多
  • 叶子,你好
  • 小雨滴
  • 潘佳伟
  • 东风快递
猜你喜欢
戳我,来吐槽~