Ruishine  

在系列文章的上一篇 《图解 Database Bffer Cache 内部原理(一)》中,已经对 HASH 链表进行了详细介绍,本文将介绍另一类链表,检查点队列链表 。

检查点队列链表

1)检查点队列

Buffer Cache 其实就是磁盘数据文件在内存中的缓存,以修改块的操作为例,如 update,只是修改 Buffer Cache 中的 Buffer,修改完成后,update 操作就算完工了。

这样 Buffer 和磁盘中的 Block 就不一致了,Buffer 中有用户最新修改的数据,这些数据还没有写入磁盘。这样的 Buffer 就是脏 Buffer(也可称脏块)。脏块由 DBWR 进程统一写磁盘,脏块写入磁盘后就不是脏块了,所以 DBWR 写脏块这个过程,也叫刷新脏块。

Buffer Cache 那么大,可能有几万、几十万个 Buffer,DBWR 要写脏块的时候,如何在 Buffer Cache 中找出哪些 Buffer 是“脏”的呢?这就需要一个链表了,它能将所有的脏 Buffer 都串起来,DBWR 写脏块时,就是按这个链表的顺序来写的。

这样的脏块链表有两个,检查点队列(CKPT-Q)和 LRUW 。本文先来介绍 CKPT-Q 。

假设现在没有任何脏块,用户发出命令,要求修改 5 号文件中的 1234 号块。流程如下:

update 5 号文件中的 1234 号块时,块的状态首先被改为脏块。这个工作是在 CBC Latch 的保护下进行的,因为块状态在 BH 中 。之后的操作,则如图 21 所示。
在这里插入图片描述
图 21 修改 5 号文件中的 1234 号块

 
进程首先持有 checkpoint queue latch,然后将 5 号文件中的 1234 号块加入检查点队列(CKPT-Q)。

当然,块被修改时还要产生对应的 Redo 数据。比如,在 5 号文件中的 1234 号块中,第一行第一列是NAME,原来的值是“张三”,现在将其改为“李四”,会将 Redo 数据补充上,如图 22 所示。
在这里插入图片描述
图 22 修改 NAME 列<

 
图 22 的右边是 Redo 的相关信息。假设 Redo 现在用到了 19 号 Redo 文件。5 号数据文件 1234 号块的修改,后映像被记录在 19 号 Redo 文件 1 号块的第 16 字节处。19.1.16 是这条 Redo 记录的地址,这个地址也叫 RBA ,即 Redo Block Address 。

下面,假设 5 号文件中的 4321 号块被修改了,产生的 Redo 记录紧接着上条记录存放,如图 23 所示。
在这里插入图片描述
图 23 修改 5 号文件中的 4321 号块

 
在图 23 中,现在又有一个块被修改了,它也被加入检查点队列中。它的修改时间比 5 号文件中的 1234 号块稍晚,因此,在检查点队列中,它排在 5 号文件的 1234 块之后。它的 Redo 记录也在 5 号文件中的 1234 号块的 Redo 记录之后,假设它的 Redo 在 19 号 Redo 文件 1 号块的第 400 字节处,那么其 RBA 就是 19.1.400 。

按上文所说的,检查点队列的顺序应该和块变脏时产生的 Redo 记录顺序是一致的,但并不是每次修改块都会对检查点队列做出改变。比如,假设 5 号文件的 1234 号块又被修改了一次,如图 24 所示。
在这里插入图片描述

图 24 文件中的1234号块又修改了一次

 
在 Redo 中又产生了一条新的 Redo 记录,位置开始自 19 号文件中 2 号块 80 字节处,RBA 是 19.2.80,但检查点队列没有任何变化。

事实上,当块第一次被修改时,也就是由不脏变脏时,它就会被加入检查点队列中。当脏块又被修改一次时,完成修改操作的 Server 进程将不需持有 CheckpointQueue Latch,也不需要对检查点队列做任何变动。因此,Checkpoint QueueLatch 通常不会造成竞争。因为并不是每次修改块都需要持有此 Latch,只有在块由不脏变脏时,才需要此 Latch,即如果我们对一个脏块反复修改,是不需要持有此 Latch 的。

按照上文所述的内容,5 号文件的 1234 号块第一次被修改后(此时它还是脏块),又被修改一次。两次被修改产生了两条 Redo 记录,位置分别是 19.1.16 和 19.2.80 。19.1.16 是第一次由不脏变脏时产生的 Redo 记录,这条 Redo 记录的地址称为 LowRBA ,简称 LRBA 。19.2.80 是块已经变成脏块后,最后一次修改产生的 Redo 记录,它的地址称为 High RBA ,简称 HRBA 。

HRBA 没有意义,而 LRBA 作用巨大。可以看到,整个检查点队列的顺序和 LRBA 的大小是一致的。可以说,检查点队列中脏块的顺序,就是 LRBA 的顺序。

比如,有一个块由不脏变脏了,也就是说它被修改了,假设它是 6 号文件 135 号块。由于它是在 5 号文件的 4321 号块之后修改的,因此,在检查点队列中,它会排在 5 号文件的 4321 号块之后。修改它所产生的 Redo 记录,也会放在 5 号文件的 4321 号文件的 Redo 记录之后,如图 25 所示。
在这里插入图片描述
图 25 增加一条修改记录

 
在检查点队列中,6 号文件的 135 号块排在 5 号文件的 4321 号块之后。Redo 记录方面,6 号文件的 135 号块的 Redo 记录是 19.2.300,也排在 5 号文件的 4321 号块对应的 Redo 记录之后。

到现在为止,共发生了 4 次修改,其中有两次修改是针对同一个块的,因此共有 3 个脏块,检查点队列中的脏块数也是 3 个。

再次强调一下,对于 Buffer Cache 中的所有 Buffer,当它变脏时,都会立即被链接到检查点队列中。将变脏的块链接到检查点队列中是修改操作的一部分,只有当这个链接操作完成时,修改操作才算完成。

回到前面的主题,在图 25 中,检查点队列里现在共有 3 个脏块。这 3 个脏块什么时候会被写到磁盘中呢?

写脏块的操作是由 DBWR 进程负责的。我们都知道,DBWR 每 3 秒会被唤醒活动一次。那么 DBWR 被唤醒后会干些什么呢?其实很简单,它醒来后,会查看检查点队列的长度,也就是查看脏块数量。如果 DBWR 认为脏块太多,它就开始将脏块写到磁盘中。

暂时先不讨论 DBWR 在什么情况下会认为脏块太多,先来看看写脏块的过程。

上例中有 3 个脏块,假设此时 DBWR 醒来,查看脏块数,发现脏块数过多,开始写脏块,如图 26 所示。
在这里插入图片描述
图 26 DBMR 写脏块过程

 
5 号文件的 1234 号块被 DBWR 写入磁盘。因为它排在检查点队列头处,DBWR 会按照检查点队列的顺序写脏块,所以第一个被写入磁盘的就是它。

当然,DBWR 会先获得 Checkpoint Queue Latch,然后扫描检查点队列,并按检查点队列顺序写一部分脏块。

DBWR 扫描 Checkpoint Queue Latch,确定了要写的脏块后,会把脏块从检查点队列中移走,移到对象队列(OBJ-Q)中,关于这部分内容会在后面讲述。待 DBWR 将要写的脏块移动完毕后,会马上释放 Checkpoint Queue Latch 。

事实上,DBWR 并不会一次将队列中的所有脏块写完,只会写一部分,具体写多少脏块,此处暂不讨论。

假设 DBWR 此次活动只写了一个脏块,写完后的状态如图 27 所示。
在这里插入图片描述

图 27 写脏块后的状态

 
其实在脏块写到磁盘前,它就已经被移出检查点队列了。现在,5 号文件的 1234 号块已经不脏了,检查点队列中也没有它的身影,只剩下另两个脏块,5 号文件的 4321 号块和 6 号文件的 135 号块。

2)检查点队列与实例恢复

如果这个时候宕机了,这两个脏块中用户修改的数据就丢了,因为宕机后 BufferCache 就没了,内存中的所有信息就都丢失了,如图 28 所示。
在这里插入图片描述

图 28 出现宕机情况

 
即使内存中的所有信息都丢失了,图 28 右边的 19 号 Redo 文件中的信息也不会丢失,因为它是已经写进文件中的信息。

脏块之所以叫脏块,就是因为它里面有没有写进磁盘的信息。刷新脏块就是将脏块写到磁盘。因此,脏块又叫“与磁盘不一致的块”。现在,这两个脏块丢了。当然,我们都知道,再次启动数据库时,Oracle 会做实例恢复,自动恢复这两个脏块。但 Oracle 怎么知道要恢复的脏块有哪些?用户修改的数据又是什么呢?

第一个问题很简单,既然所有的脏块在变脏时,都会被链接进检查点队列中,那么检查点队列中就包含了所有的脏块。所以,Oracle 实例恢复的目标,就是检查点队列中的所有脏块,如图 29 所示。
在这里插入图片描述
图 29 恢复检查点队列

 
但是还有个问题,数据库已经宕机,内存中的信息都已经没有了,检查点队列自然也没有了。虽然实例恢复的目标是检查点队列中的脏块,但 Oracle 又该到哪儿找检查点队列呢?

其实,只要观察一下就会发现,检查点队列和 Redo 的顺序是有对应关系的。当块变脏时,Oracle 会做两件事,一是生成 Redo,二是将脏块链接到检查点队列。因此,Redo 文件中 Redo 记录的顺序就是脏块的顺序,如图 30 所示。
在这里插入图片描述
图 30 Redo 记录的顺序就是脏块的顺序

 
在图 30 中,检查点队列中的第一个脏块是 5 号文件的 1234 号块,它的 Redo 记录在 Redo 文件的最开头,即 19 号文件中 1 号块第 16 字节处。19 号日志文件中的第二条日志记录,也就是 RBA 为 19.1.400 的 Redo 记录,它对应的脏块排在检查点队列的第二个位置处。19 号日志文件中第三条日志记录,RBA为 19.2.80,它是 5 号文件 1234 号块第二次被修改时产生的 Redo 记录,它不对应新的脏块。再往下, RBA 为 19.2.300 的 Redo 记录对应检查点队列的第三个脏块。

Redo 文件中 Redo 记录的顺序和块变脏的顺序是一致的,和检查点队列的顺序也是一致的。因为,块在变脏的时候,会产生 Redo 记录,同时被链接到检查点队列中。既然它们两个顺序一致,回到前面的问题,在进行实例恢复时 Oracle 如何找到要恢复的脏块?

事实上,只需找到检查点队列头对应的 Redo 记录 19.1.400 ,然后从此处开始顺序向下恢复即可,如图 31 所示。
在这里插入图片描述
图 31 找到队列头顺序恢复

 
在进行实例恢复时,Oracle 从 19.1.400 处开始读取每一条 Redo 记录,从而进行恢复。如图 32 所示的红色椭圆框出的部分,就是需要恢复的 Redo 记录。
在这里插入图片描述
图 32 需要恢复的记录

 
至于说 Oracle 如何完成恢复,很简单,图例中已经把 Redo 记录中包含的信息大概地画了出来,一条 Redo 信息包含被修改的块所属的文件号,对应的块号,被修改的行号、列号,还有后映像,也就是修改为什么值。比如,要恢复 19.1.400 这条 Redo 记录,如图 33 所示。
在这里插入图片描述
图 33 恢复 19.1.400 记录

 
19.1.400 这条 Redo 记录中有记载,用户修改的是 5 号文件的 4321 号块,第一行第一列,修改后的值是 XXXX。恢复进程从磁盘中将 5 号文件的 4321 号块读进 BufferCache,再将后映像 XXXX 写到 Buffer Cache 中4 321 块的第 1 行第 1 列。

这就是恢复所做的操作,依次恢复从 19.1.400 开始的所有 Redo 记录,直到文件的尽头,这就是实例恢复了。

这里面还有个问题就是,Redo 记录 19.2.80 是不需要恢复的,因为它对应的脏块 5 号文件 1234 号块已经不脏了。这个块被修改过两次,第一次修改,它的第一行第一列被改成了“李四”。这一次它是由不脏块的块变成脏块,除了产生 Redo 记录外,Oracle 还会将它链接到检查点队列。

它的第二次修改还是第一行第一列,被改成“王五”,在进行这一次修改时,它已经是个脏块了,并不是从不脏变成脏块,因此它在检查点队列中的位置不会变,如图 34 所示。
在这里插入图片描述
图 34 19.2.80记录不需要恢复

 
5 号文件的 1234 号块被修改过两次,第一次是第一行第一列被修改为“李四”,Redo 记录在 19.1.16 处,这次会让块由不脏变脏,因此在修改的同时,它要加入检查点队列。第二次修改,仍是第一行第一列,被改为“王五”,Redo 记录在 19.2.80 处。这时,它还是脏块,因此,它在检查点队列中的位置不受影响,如图 35 所示。
在这里插入图片描述
图 35 1234号块被修改过两次

 
因为增量检查点触发,5 号文件 1234 号块被写入磁盘。写入磁盘中的 1234 号块,第一行第一列是最后一次被修改的值“王五”。

因此在实例恢复时,RBA 为 19.2.80 的 Redo 记录是可以不用恢复的。因为它所对应的脏块在宕机前就已经不脏了,如图 36 所示。
在这里插入图片描述
图 36 19.2.80记录不是脏块

 
在图 36 中,19.1.400 是恢复的起始点,可以看到,5 号文件的 1234 号块也会被恢复,如图 37 所示。
在这里插入图片描述
图 37 5号文件的1234号块也会被恢复

 
在图 37 中,5 号文件 1234 号块会从磁盘读到 Buffer Cache 中,将本来已经是“王五”的值,再一次修改成“王五”。

这样重复恢复是正常的,因为 Oracle 的 Redo 恢复机制本来就是可以重复恢复的。不过,这会浪费 I/O、CPU 资源,因此 Oracle 会尽量避免这种情况。其方法这里不再描述,可以在网上查找 Oracle 恢复相关内容。

到这里,Oracle 已经解决了宕机出现的两个问题,“从哪里开始恢复”和“恢复哪些块”。本例是从 19.1.400 处开始恢复,这个值 Oracle 必须将它保存到磁盘上。需要实例恢复时,从磁盘上读取它,再从它所指向的 Redo 处开始恢复。但如何将恢复起始位置 “19.1.400” 保存到磁盘上呢?另外一个进程要上场了: CKPT ,如图 38 所示。
在这里插入图片描述
图 38 将恢复起始位置保存到磁盘上

 
前文已经讲述,实例恢复是从检查点队列头所记录的 LRBA 处开始恢复的。检查点队列头又称为检查点位置(Checkpoint Position)。检查点队列全部在内存中,宕机之后,队列相关信息将全部丢失,Oracle 是如何知道检查点位置对应的 LRBA 的呢?

Oracle 的处理方法为:另外找个进程 CKPT,每 3 秒一次将检查点位置对应的 LRBA 记录到控制文件中。

下面总结一下检查点队列与实例恢复相关内容:

  • 块被修改时会产生 Redo 记录。
  • 块在由不脏变脏时会被链接到检查点队列中。
  • 检查点队列中块的排列顺序和 Redo 记录的顺序基本一致。
  • DBWR 每 3 秒检查一次检查点队列的长度,也就是脏块数。如果队列过长,将触发写脏块。
  • DBWR 会沿着检查点队列的顺序写脏块。
  • CKPT 每 3 秒一次,将检查点队列头对应的 LRBA 写进控制文件。
  • 如果发生数据库崩溃、宕机等情况,需要进行实例恢复。实例恢复的起始点就是控制文件中记录的检查点队列头所对应的 LRBA。
  • 实例恢复开始时,Oracle 找到此 LRBA,再定位到某个 Redo 文件的某个位置,开始依次进行实例恢复。

3)DBWR如何写脏块

前文我们说到“DBWR发现检查点队列中脏块数量过多”时会开始写脏块,那么,对于“数量过多”,Oracle 的评判标准是什么?或者说,DBWR 每 3 秒醒来后,会检查哪些指标来判断是否要写脏块?它检查、判断的标准是什么?

有几个参数可以影响 DBWR 是否写脏块,从 Oracle 10g 后,需要注意的参数是 fast_start_mttr_target(简称 MTTR)。

Oracle 对此参数的解释是,此参数表示用户希望 Oracle 在多长时间内完成实例恢复。如果设置为 300,也就是说 Oracle 将在 300 秒内完成实例恢复。

为了达到这一目标,Oracle 会针对所记录的数据库硬件写性能、产生的 Redo 量来估算恢复一个脏块所需的时间。比如,Oracle 根据硬件情况,估算出数据库恢复一个脏块需要 20 毫秒,那么,当脏块数量超过 15000 个时,DBWR 将会写脏块(因为 15000 个脏块恢复所需要的时间正好是 300 秒)。一旦脏块数量超过 15000 个,将无法在 300 秒内完成恢复。

可以通过设置 MTTR 参数增大或减小期望的实例恢复时间,调节 DBWR 进程写脏块的频率。此参数通常不必调节,只有当你观察到增量检查点的频率不是最佳时,才可以考虑调节一下。

在 Oracle 10g 之后,如果不设 MTTR 参数,它的值将保持为 0 。这时,将激活自调节检查点。也就是说,从 Oracle 10g 开始直到 Oracle 12c,自调节检查点默认都是打开的。

在自调节检查点下,不需要关心 MTTR 参数设置为多少,Oracle 会自动根据硬件写性能、日志产生量,计算脏块数阈值。脏块数一旦超过阈值,就会开始写脏块。只有在自调节检查点不能满足我们的需要时,才需要调整 MTTR 参数。绝大多数的数据库其实已经不需要调节此参数了。这是 Oracle 的发展趋势,自动管理、自动运维,减少 DBA 负担。但是,对于写操作较多的数据库,自调节检查点有可能不能满足需要,还需要手动调节 MTTR,以获得最佳的性能。

其实,DBWR 决定要写脏块时,会对要写的脏块进行整合,从而将相邻的脏块合并。整合的算法是什么呢?很简单,DBWR 会将脏块从检查点队列中移到对象队列(OBJ-Q)中,如图 39 所示。
在这里插入图片描述
图 39 将脏块移动

 
假设检查点队列中共有 8 个脏块,分别是 A 到 H 。DBWR 在 3 秒醒来时发现脏块过多,决定要写脏块,这时它确定的范围假设是 A 到 F,如图 40 所示。
在这里插入图片描述
图 40 脏块对应的对象链表

 
每个对象、表或索引在 SGA 中都有各自的对象链表。比如,在图 40 中,假设只有两张表:PROD 和 SALES 。其中脏块 A、C、F 属于 PROD 表,脏块 B、D、E、H、G 属于 SALES 表。当 DBWR 准备写脏块时,会先将脏块从检查点队列取出,移到各个对象各自的对象链表。因为这几个块同属于某个表或索引,它们相邻的几率会更大。

然后,还要再进一步合并,如图 41 所示。
在这里插入图片描述
图 41 进一步合并

 
在图 41 中,假设脏块 A 是 5 号文件 102 号块,C 是 5 号文件 80 号块,F 是 5 号文件 101 号块,那么就会进行再次合并,再次合并后的结果如图 42 所示。
在这里插入图片描述
图 42 合并的结果

 
由于脏块 A 和 F 一个是 102 号块、一个是 101 号块,两个块相邻,因此 F 会被提到前面,这就是 DBWR 的块合并算法。

合并完后,就要开始写了,如图 43 所示。
在这里插入图片描述
图 43 开始写脏块

 
对于脏块 C,它是 5 号文件 80 号块。由于它和其他脏块并不连续,因此它将被直接写进磁盘(如图 43 所示)。但是写脏块 F、A 时会有所不同。它们两个虽然在链表中合并了,但在 Buffer Cache 中的内存块并不相连,为了让它们两个合并成一个大 I/O,以便一次性写进磁盘,Oracle 在共享池中专门开辟一块空间,作为 I/O 合并缓冲区,如图 44 所示。
在这里插入图片描述
图 44 开辟 I/O 合并缓冲区

 
写脏块到磁盘前,会将连续的脏块(如脏块 F、A)写到共享池这块专门的缓存区中。在这里,F 和 A 的地址是相连的,它们两个合在一起,构成了一个 16KB 的大内存块(假设块大小是 8KB )。然后再从共享池的这块 I/O 缓存中,一次性将 16KB 的数据写进磁盘,如图 45 所示。
在这里插入图片描述
图 45 从缓存中将 16KB 写进了磁盘

 
如果一次要写的脏块太多,DBWR 不可能将所有脏块进行合并,那就只能分批进行了,也就是将脏块分成多个 Batch ,分别对每个 Batch 进行合并,如图 46 所示。本次 DBWR 要写的脏块被分成了 3 个 Batch 。DBWR 先从第一个 Batch 开始,这个 Batch 中的脏块会被移出检查点队列,移到各个对象的对象链表中,然后在对象链表中进行合并,再写进磁盘。
在这里插入图片描述
图 46 分批写脏块方式

 
假设第一个 Batch 中共有 11 个脏块(如图 46 所示),开始写这 11 个脏块前,DBWR 会记录一个等待事件 db file parallel write,直到这一个 Batch 所有脏块的写操作都完成了,db file parallel write 事件才结束。在图 46 中共有 3 个 Batch,也就是说,所有脏块写完后,db file parallel write 事件的等待次数会增加 3 次。

第一个 Batch 共有 11 个脏块,转移到对象链表,经过合并后,脏块变成 7 个大块。

其中,PROD 表的对象链表中有 3 个脏块,经过合并,变成一大一小两个脏块。然后 DBWR 会进行写 I/O 操作。PROD 对象链表有两次写 I/O 操作,分别用于写一大一小两个脏块。

注意,只要异步 I/O 是可用的,DBWR 就会在这里使用异步写 I/O 。

合并后,在 SALES 表的对象链表中还剩下两大一小 3 个脏块,DBWR 将再次发出 3 个异步写 I/O 指令。再接着,就是 XXXX 表的对象链表上的一小一大两个脏块了,这又是两次异步写 I/O 。

在写 I/O 时,无论是写一个 8KB 大小的块,还是写多个连续脏块合并起来的大脏块,都只算是一次写 I/O 。写 I/O 的次数可以在 v$filestat 的 phywrts 列查看。v$filestat 中的结果是以文件为单位进行统计的,如果需要计算数据库总的写 I/O 次数,可以使用 Sum 计算总数。或者查看 v$sysstat 中的 physical write IO requests 资料值,它也是所有数据文件写 I/O 次数的总和。

如果想了解 Oracle 一共写了多少个块,通过 v$sysstat 中的 physical writes 资料查看即可。

虽然在一个 Batch 中所有脏块写 I/O 都是异步的,但 Batch 和 Batch 之间是同步的。DBWR 会等待一个 Batch 中的所有写 I/O 操作都完成之后,再开始处理下一个 Batch 。

如果要判断 DBWR 的写 I/O 是否存在问题,主要依据以下 3 个指标:

  • 写 I/O 的总块数,即 Oracle 一共写了多少个块,通过 v$sysstat 中 physical writes 资料可以得到总块数。
  • 写 I/O 的总次数,即 Oracle 一共进行了多少次写 I/O 操作。通过 v$filestat 视图中的 phywrts 可以得到总次数。或者查看 v$sysstat 中的 physical write IO requests 资料值,也可以了解所有数据文件写次数的总和。
  • Batch 的个数。通过等待事件 db file parallel write 的次数,就可以知道 Batch 的个数。当然,也可以知道每个 Batch 的完成时间。

可以很容易得到结论:对于写 I/O,块数相同的情况下,如果 I/O 次数越少,那么每次 I/O 写的块数就越多,也就是相连的块数越多。这样,写的性能就越好。

除此以外,Batch 越大、总的 Batch 个数越少,写的性能也将越好。这是因为,Batch 越大,相邻的块的比例就会越多。比如 Batch 有 100 个块或者 300 个块。在 300 个块中找相邻的,肯定会比在 100 个块中找到的多。另外一点原因是,因为 Batch 与 Batch 之间是同步的,Batch 内是异步的,所以 Batch 个数越少,“同步”的次数就越少。以两个 Batch 和 5 个 Batch 为例,如果有 5 个 Batch,每个 Batch 都要等待 Batch 中所有的异步 I/O 都完成,那么共需要等 5 次;而两个 Batch 则只需要等两次。

所以,单个 Batch 越大、Batch 总数越少,写 I/O 的性能也就越好。

摘自:书籍《Oracle 内核技术揭密》

posted on 2021-04-09 22:31  夜光兔  阅读(116)  评论(0编辑  收藏  举报