DBWn

我们DBWn进程负责将脏数据块写入磁盘。它是一个非常重要的进程,随着内存的不断增加,一个DBWn进程可能不够用了。所以从Oracle 8i起,我们可以为系统配置多个DBWn进程。初始化参数db_writer_processe决定了启动多少个DBWn进程。每个DBWn进程都会分配一个cache buffers lru chain latch。
DBWn作为一个后台进程,只有在某些条件满足了才会触发。这些条件包括:
当进程在LRU链表扫描以查找可以覆盖的buffer header时,如果已经扫描的buffer header的数量到达一定的限度时,触发DBWn进程;
如果脏数据块的总数超过一定限度,也将触发DBWn进程;
发生检查点(包括增量检查点(incremental checkpoint)和完全检查点(complete checkpoint))时触发DBWn;
每隔三秒钟启动一次DBWn;

DBWn、CKPT、LGWR进程之间的合作

将内存数据块写入数据文件实在是一个相当复杂的过程,在这个过程中,首先要保证安全。所谓安全,就是在写的过程中,一旦发生实例崩溃,要有一套完整的机制能够保证用户已经提交的数据不会丢失;其次,在保证安全的基础上,要尽可能地提高效率。众所周知,I/O操作是最昂贵的操作,所以应该尽可能地将脏数据块收集到一定程度以后,再批量写入磁盘中。
直观上最简单的解决方法就是,每当用户提交的时候就将所改变的内存数据块交给DBWn,由其写入数据文件。这样的话,一定能够保证提交的数据不会丢失。但是这种方式效率最为低下,在高并发环境中,一定会引起I/O方面的争用。Oracle当然不会采用这种没有伸缩性的方式。Oracle引入了CKPT和LGWR这两个后台进程,这两个进程与DBWn进程互相合作,提供了既安全又高效的写脏数据块的解决方法。
用户进程每次修改内存数据块时,都会在日志缓冲区(log buffer)中构造一个相应的重做条目(redo entry),该重做条目描述了被修改的数据块在修改之前和修改之后的值。而LGWR进程则负责将这些重做条目写入联机日志文件。只要重做条目进入了联机日志文件,那么数据的安全就有保障了,否则这些数据都是有安全隐患的。LGWR是一个必须和前台用户进程通信的进程。LGWR承担了维护系统数据完整性的任务,它保证了数据在任何情况下都不会丢失。
假如DBWR在写脏数据块的过程中,突然发生实例崩溃时,该怎么办?我们知道,用户提交时,Oracle是不一定会把提交的数据块写入数据文件的。那么实例崩溃时,必然会有一些已经提交但是还没有被写入数据文件的内存数据块丢失了。当实例再次启动时,Oracle需要利用日志文件中记录的重做条目在buffer cache中重新构造出被丢失的数据块,从而完成前滚和回滚的工作,并将丢失的数据块找回来。于是这里就存在一个问题,就是Oracle在日志文件中找重做条目时,到底应该找哪些重做条目?换句话说,应该在日志文件中从哪个起点开始往后应用重做条目?注意,这里所指的日志文件可能不止一个日志文件。
因为需要预防随时可能的实例崩溃现象,所以Oracle在数据库的正常运行过程中,会不断地定位这个起点,以便在不可预期的实例崩溃中能够最有效地保护并恢复数据。同时,这个起点的选择非常有讲究。首先,这个起点不能太靠近日志文件的头部,太靠近日志文件头部意味着要处理很多的重做条目,这样会导致实例再次启动时所进行恢复的时间太长;其次,这个起点也不能太靠近日志文件的尾部,太靠近日志文件的尾部说明只有很少的脏数据块没有被写入数据文件,也就是说前面已经有很多脏数据块被写入了数据文件,那也就意味着只有在DBWn进程很频繁地写数据文件情况下,才能使得buffer cache中所残留的脏数据块的数量很少。但很明显,DBWn写得越频繁,那么所占用写数据文件的I/O就越严重,那么留给其他操作(比如读取buffer cache中不存在的数据块等)的I/O资源就越少。这显然也是不合理的。
从这里也可以看出,这个起点实际上说明了,在日志文件中位于这个起点之前的重做条目所对应的在buffer cache中的脏数据块已经被写入了数据文件,从而在实例崩溃以后的恢复中不需要去考虑。而这个起点以后的重做条目所对应的脏数据块实际还没有被写入数据文件,如果在实例崩溃以后的恢复中,需要从这个起点开始往后,依次取出日志文件中的重做条目进行恢复。考虑到目前的内存容量越来越大,buffer cache也越来越大,buffer cache中包含几百万个内存数据块也是很正常的现象的前提下,如何才能最有效的来定位这个起点呢?
为了能够确定这个最佳的起点,Oracle引入了名为CKPT的后台进程,通常也叫作检查点进程(checkpoint process)。这个进程与DBWn共同合作,从而确定这个起点。同时,这个起点也有一个专门的名字,叫做检查点位置(checkpoint position,该检查点位置记录在控制文件里)。Oracle为了在检查点的算法上更加的具有可扩展性(也就是为了能够在巨大的buffer cache下依然有效工作),引入了检查点队列(checkpoint queue),该队列上串起来的都是脏数据块所对应的buffer header。而每次DBWn写脏数据块时,也是从检查点队列上扫描脏数据块,并将这些脏数据块实际写入数据文件的。当写完以后,DBWn会将这些已经写入数据文件的脏数据块从检查点队列上摘下来。这样即便是在巨大的buffer cache下工作,CKPT也能够快速的确定哪些脏数据块已经被写入了数据文件,而哪些还没有写入数据文件,显然,只要在检查点队列上的数据块都是还没有写入数据文件的脏数据块。而且,为了更加有效的处理单实例和多实例(RAC)环境下的表空间的检查点处理,比如将表空间设置为离线状态或者为热备份状态等,Oracle还专门引入了文件队列(file queue)。文件队列的原理与检查点队列是一样的,只不过每个数据文件会有一个文件队列,该数据文件所对应的脏数据块会被串在同一个文件队列上;同时为了能够尽量减少实例崩溃后恢复的时间,Oracle还引入了增量检查点(incremental checkpoint),从而增加了检查点启动的次数。如果每次检查点启动的间隔时间过长的话,再加上内存很大,可能会使得恢复的时间过长。因为前一次检查点启动以后,标识出了这个起点。然后在第二次检查点启动之前,DBWn可能已经将很多脏数据块已经写入了数据文件,而假如在第二次检查点启动之前发生实例崩溃,导致在日志文件中,所标识的起点仍然是上一次检查点启动时所标识的,导致Oracle不知道这个起点以后的很多重做条目所对应的脏数据块实际上已经写入了数据文件,从而使得Oracle在实例恢复时重复地处理一遍,效率低下,浪费时间。
上面说到了有关CKPT的两个重要的概念:检查点队列(包括文件队列)和增量检查点。检查点队列上的buffer header是按照数据块第一次被修改的时间的先后顺序来排列的。越早修改的数据块的buffer header排在越前面,同时如果一个数据块被修改了多次的话,在该链表上也只出现一次。而且,检查点队列上的buffer header还记录了脏数据块在第一次被修改时,所对应的重做条目在重做日志文件中的地址,也就是LRBA(Low Redo Block Address),Low表示第一次修改时对应的RBA。每个检查点都会由checkpoint queue latch来保护。
而增量检查点是从Oracle 8i开始出现的,是相对于Oracle 8i之前的完全检查点(complete checkpoint)而言的。完全检查点启动时,会标识出buffer cache中所有的脏数据块,然后以最高优先级启动DBWn进程将这些脏数据块写入数据文件。Oracle 8i之前,日志切换的时候会触发完全检查点。而到了Oracle 8i及以后,完全检查点只有在两种情况下才会被触发:
发出alter system checkpoint命令;
除了shutdown abort以外的正常关闭数据库。
注意,这个时候,日志切换不会触发完全检查点,而是触发增量检查点。Oracle 8i所引入的增量检查点每隔三秒钟或发生日志切换时启动。它启动时只做一件事情:找出当前检查点队列上的第一个buffer header,并将该buffer header中所记录的LRBA(这个LRBA也就是checkpoint position)记录到控制文件中去。如果是由日志切换所引起的增量检查点,则还会将checkpoint position记录到每个数据文件头中。也就是说,如果这个时候发生实例崩溃,Oracle在下次启动时,就会到控制文件中找到这个checkpoint position作为日志文件的起点,然后从这个起点开始向后,依次取出每个重做条目进行处理。
上面所描述的概念,用一句话来概括,其实就是DBWn负责写检查点队列上的脏数据块,而CKPT负责记录当前检查点队列的第一个数据块所对应的的重做条目在日志文件中的地址。而到底应该写哪些脏数据块,写多少脏数据块,则要到检查点队列上才能确定的。
我们用一个简单的例子来描述这个过程。假设系统中发生了一系列的事务,导致日志文件如下所示:
事务号  数据文件号  block号  行号  列  值  RBA
T1         8             25         10    1  10  101
T1         7             623       12    2   a   102
T3         8              80         56    3   b   103
T3         9              98         124  7   e   104
T5          7              623       13    3   abc 105
Commit SCN#  timestamp       106
T123     8               876       322  10  89  107

                  


这时,对应的检查点队列则类似上图所示。101、102 … 107表示LRBA,而8/25、7/623 … 8/876表示数据块号/文件号。

我们可以看到,T1事务最先发生,所以位于检查点队列的首端,而事务T123最后发生,所以位于靠近尾端的地方。同时,可以看到事务T1和T5都更新了7号数据文件的623号数据块。而在检查点队列上只会记录该数据块的第一次被更新时的RBA,也就是事务T1对应的RBA102,而事务T5对应的RBA105并不会被记录。当DBWn写数据块的时候,在写RBA102时,自然就把RBA105所修改的内容写入数据文件了。日志文件中所记录的提交标记也不会体现在检查点队列上,因为提交本身只是一个标记而已,不会涉及修改数据块。

这时,假设发生三秒钟超时,于是增量检查点启动。增量检查点会将检查点队列的第一个脏数据块所对应的LRBA记录到控制文件中去。在这里,也就是RBA101会作为checkpoint position记录到控制文件中。

然后,启动DBWn后台进程。DBWn根据一系列参数及规则,计算出应该写的脏数据块的数量,假设将RBA101到RBA107之间的这5个脏数据块写入数据文件,并在写完以后将这5个脏数据块从检查点队列上摘除,而留下了4个脏数据块在检查点队列上,如下图所示。

       

      

如果在写这5个脏数据块的过程中发生实例崩溃,则下次实例启动时,Oracle会从RBA101开始应用日志文件中的重做条目。

                 


从检查点对列上摘除脏块

而在Oracle 9i以后,在DBWR写完这5个脏数据块以后,还会在日志文件中记录所写的脏数据块的块号。上图所示。这主要是为了在恢复时加快恢复的速度。

这时,假设又发生三秒钟超时,于是增量检查点启动。这时它发现checkpoint position为RBA109,于是将RBA109写入控制文件。如果接着发生实例崩溃,则Oracle在下次启动时,就会从RBA109开始往下应用日志。


参考至:《教你成为10g OCP》韩思捷著