将内存数据块写入数据文件实在是一个相当复杂的过程,在这个过程中,首先要保 证安全。所谓安全,就是在写的过程中,一旦发生实例崩溃,要有一套完整的机制能够保证用户已经提交的数据不会丢失;其次,在保证安全的基础上,要尽可能地 提高效率。众所周知,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 |
这时,对应的检查点队列则类似图5-5所示。101、102 … 107表示LRBA,而8/25、7/623 … 8/876表示数据块号/文件号。
图5-5 检查点队列1 |
我们可以看到,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-6所示。如果在写这5个脏数据块的过程中发生实例崩溃,则下次实例启动时,Oracle会从RBA101开始应用日志文件 中的重做条目。
图5-6 从检查点对列上摘除脏块 |
而在Oracle 9i以后,在DBWR写完这5个脏数据块以后,还会在日志文件中记录所写的脏数据块的块号。如图5-7所示。这主要是为了在恢复时加快恢复的速度。
图5-7 记录提交信息 |
这时,假设又发生三秒钟超时,于是增量检查点启动。这时它发现checkpoint position为RBA109,于是将RBA109写入控制文件。如果接着发生实例崩溃,则Oracle在下次启动时,就会从RBA109开始往下应用日志。
5.4.2.4 设置buffer cache
buffer cache的设置随着Oracle版本的升级而不断变化。Oracle 8i下使用db_block_buffers来设置,该参数表示buffer cache中所能够包含的内存数据块的个数;Oracle 9i以后使用db_cache_size来设置,该参数表示buffer cache的总共的容量,可以用字节、KB、MB为单位来进行设置。而到了Oracle 10g以后则更加简单,甚至可以不用去单独设置buffer cache的大小。因为Oracle 10g引入了ASMM(Automatic Shared Memory Management)这样一个可以进行自我调整的组件,该组件可以自动调整shared pool size、db cache size等SGA中的组件。只需要设置sga_target参数,再设置statistics_level为typical或all,则其他组件就能够根 据系统的负载和历史信息自动地调整各个部分的大小。
从Oracle 8.0以后,Oracle提供了三种类型的buffer cache,分别是default、keep、recycle。keep和recycle是可选的,default必须存在。Oracle 8i以后使用db_cache_size设置default池、db_keep_cache_size设置keep池、 db_recycle_cache_size设置recycle池。
通常将经常访问的对象放入keep类型的buffer cache里,而将不常访问的大表放入recycle类型的buffer cache里。其他没有指定buffer cache类型的对象都将进入default类型的buffer cache里。为对象指定buffer cache类型的方法如下:
SQL> create table test (n number) storage (buffer_pool keep); |
如果没有指定buffer_pool短语,则表示该对象进入default类型的buffer cache。
这里要说明的是,从名字上看,很容易让人误以为这三种buffer cache提供了三种不同的管理内存数据块的机制。但事实上,它们之间在管理和内部机制上没有任何的区别。它们仅仅是为DBA们提供了一个选择,就是能够 将数据库对象分成“非常热的”、“比较热的”和“不热的”这三种类型。因为数据库中总会存在一些“非常热”的对象,它们频繁地被访问。而如果某个时候系统 偶尔做了一次大表的全表扫描,就有可能将这些对象清除出内存。为了防止这种情况的发生,我们可以设置keep类型的buffer cache,并将这种非常热的对象都移入keep buffer cache中。同样的,数据库中也总会有一些很大的表,可能每月为了生成一张报表,而只需要访问一次就可以了。但有可能就是这么一次访问,就将大部分的内 存数据块清除出了buffer cache。为了避免这种情况的发生,可以设置recycle类型的buffer cache,并将这种偶尔访问的大表移入recycle buffer cache。
毫无疑问,如果我们要设置这三种类型的buffer cache,则需要自己研究并根据数据库中的对象进行分类,计算这些对象的大小以后,从而才能够正确的把它们放入不同的buffer cache。但是,不管怎么说,设置这三种类型的buffer cache只能算是最低层次的优化,也就是说在我们没有任何办法的情况下,可以考虑设置它们。但是如果我们能够对某条读取了过多数据块的SQL语句进行调 优的话,使其buffer gets降低50%的话,就会比设置多个buffer cache要好得多了。
Oracle 9i以后还提供了可以设置多种数据块尺寸(2、4、8、16 或 32)的buffer cache,以便存放不同数据块尺寸的表空间中的对象。使用初始化参数db_Nk_cache_size来指定不同数据块尺寸的buffer cache,这里的N就是2、4、8、16 或 32。创建数据库时,使用初始化参数db_block_size所指定默认的标准数据块尺寸,标准数据块尺寸用于system表空间。然后可以指定最多4 个不同的、非标准数据块尺寸的表空间。每种数据块尺寸的表空间必须对应一种不同尺寸的buffer cache,否则不能创建不同数据块尺寸的表空间。
SQL> create tablespace tbs_test_16k |
我们可以看到,由于没有指定16k数据块所对应的buffer cache,所以创建16k数据块的表空间会失败。我们先设置db_16k_cache_size,然后再试着创建16k数据块的表空间。
SQL> alter system set db_16k_cache_size=10M; |
表空间已创建。
不同尺寸数据块的buffer cache的管理和内部机制与默认数据块的buffer cache没有任何的分别。它最大的好处是,当使用可传输的表空间从其他数据库中将不同于当前默认数据块尺寸的表空间传输过来的时候,可以不做很多的处理 直接导入到当前数据库,只需要设置对应数据块尺寸的buffer cache即可。同时,多种数据块大小的表空间对于调优OLTP和OLAP混合的数据库也有一定的好处。OLTP环境下,倾向于使用较小的数据块,而 OLAP环境下,由于基本都是执行全表扫描,因此倾向于使用较大的数据块。这时,可以将OLAP的表转移到使用大数据块(比如32KB)的表空间里去。而 将OLTP的表放在中等大小的数据块(比如8KB)的表空间里。
要注意的是,keep池和recycle池只能使用标准块大小。
在如何设置buffer cache的大小上,从Oracle 9i开始通过初始化参数db_cache_advice,从而启动buffer cache顾问,该顾问提供了可以参考的建议值。Oracle会监控default类型、keep类型和recycle类型的buffer cache的使用,以及其他五种不同数据库尺寸(2、4、8、16 或 32KB)的buffer cache的使用。在典型负荷的时候,启用该参数,从而收集数据帮助用户确定最佳的db_cache_size的大小。该参数有以下三个值。
off:不收集数据。
on:开始分配内存收集数据,会造成CPU和内存的负担,可能引起4031错。
ready:不收集数据,但是收集数据的内存已经预先分配好了。通过把该参数值从off设置为ready,然后再设置为on,就可以避免出现4031错。
Oracle会根据当前所监控到的物理读的速率,从而估算出在不同大小尺寸的buffer cache下,所产生的可能的物理读的数量。Oracle会将这些收集到的信息放入视图v$db_cache_advice中。每种类型的buffer cache都会有相应的若干条记录来表示所建议的buffer cache的大小。比如下面,我们显示对于默认类型的、默认数据块尺寸的buffer cache的建议大小应该是多少。
SQL> SELECT size_for_estimate, buffers_for_estimate, |
这里的字段estd_physical_read_factor表示在相应buffer cache的尺寸(由字段size_for_estimate表示)下,估计从硬盘里读取数据的次数除以在内存里发生的逻辑读的总次数。如果在内存里的逻 辑读没有引起物理读,则该比值为空。在内存足够的前提下,这个比值应该是越低越好的。从该输出可以看到,当前buffer cache为32MB。如果buffer cache为12MB,而不是当前的32MB,则估计产生的物理读会是当前buffer cache尺寸下的1.1861倍,也就是增加了18.6%左右的物理读(1.1861-1)。而如果增加buffer cache,将其设置为60MB,会使得物理读减少45%左右(1-0.554)。而如果继续增加buffer cache,会看到物理读不再会减少。也就是说,如果我们有足够的物理内存,则只需要将buffer cache设置为60MB即可。因为再继续增加buffer cache,也不会带来更多的好处。
5.4.2.5 实例恢复的原理
前面我们讲到过,当数据库突然崩溃,而还没有来得及将buffer cache里的脏数据块刷新到数据文件里,同时在实例崩溃时正在运行着的事务被突然中断,则事务为中间状态,也就是既没有提交也没有回滚。这时数据文件里 的内容不能体现实例崩溃时的状态。这样关闭的数据库是不一致的。
下次启动实例时,Oracle会由SMON进程自动进行实例恢复。实例启动时,SMON进程会去检查控制文件中所记录的、每个在线的、可读写的数据 文件的END SCN号。数据库正常运行过程中,该END SCN号始终为空,而当数据库正常关闭时,会进行完全检查点,并将检查点SCN号更新该字段。而崩溃时,Oracle还来不及更新该字段,则该字段仍然为 空。当SMON进程发现该字段为空时,就知道实例在上次没有正常关闭,于是由SMON进程就开始进行实例恢复了。
SMON进程进行实例恢复时,会从控制文件中获得检查点位置。于是,SMON进程到联机日志文件中,找到该检查点位置,然后从该检查点位置开始往 下,应用所有的重做条目,从而在buffer cache里又恢复了实例崩溃那个时间点的状态。这个过程叫做前滚,前滚完毕以后,buffer cache里既有崩溃时已经提交还没有写入数据文件的脏数据块,也还有事务被突然终止,而导致的既没有提交又没有回滚的事务所弄脏的数据块。
前滚一旦完毕,SMON进程立即打开数据库。但是,这时的数据库中还含有那些中间状态的、既没有提交又没有回滚的脏块,这种脏块是不能存在于数据库中的,因为它们并没有被提交,必须被回滚。打开数据库以后,SMON进程会在后台进行回滚。
有时,数据库打开以后,SMON进程还没来得及回滚这些中间状态的数据块时,就有用户进程发出读取这些数据块的请求。这时,服务器进程在将这些块返回给用户之前,由服务器进程负责进行回滚,回滚完毕后,将数据块的内容返回给用户。
Oracle提供了初始化参数fast_start_mttr_target让我们指定完成实例恢复所花费的时间(该时间只包括前滚并打开数据库的 时间,不包括回滚的时间),该参数以秒为单位。比如我们设置该参数为30,表示如果发生实例崩溃,那么下次重新启动时,数据库最多用30秒的时间完成前 滚,并打开数据库。在数据库运行过程中,就会根据该时间,来估算30秒大致对应多少量的重做记录,这实际上就决定了检查点位置,如图5-8所示。
图5-8 检查点队列3 |
图5-8中的红色竖线就是检查点位置。Oracle应用完检查点位置以后所有的重做记录所花费的时间就是 fast_start_mttr_target所指定的时间。也就是说,检查点位置以后的重做记录所对应的脏块会被留在检查点队列上,而不被DBWn写入 数据文件。因此,该参数越大,说明要应用的重做记录就越多,那么留在检查点队列上的脏块就越多,也就说明DBWn写脏块越不频繁,占用I/O越少,那么前 台用户查询语句的I/O就能够越快地被响应。但是实例恢复的时间也会越长。反之,该参数越小,说明要应用的重做记录就越少,那么留在检查点队列上的脏块就 越少,也就说明DBWn写脏块越频繁,因而占用I/O越多,那么前台用户查询语句的I/O就不能较快地被响应。但是实例恢复的时间会更短