数据后像与前滚
设计原则
事务的持久性要求事务提交时本次事务的修改必须完成持久化工作,而事务修改的block或page在大部分场景下并不是连续的,在持久化设备上表现为大量的随机IO。通过记录后像,可以将随机IO转换为对持久化设备更为有利的顺序IO,并将dirty block或dirty page(指被修改过但尚未完成持久化的block或page)的多次修改合并,节约block或page的持久化次数。
后像是平衡性能和事务持久性的最有效手段,因此Oracle和MySQL都采用了后像方案,并将其称为redo log。在设计后像时,需要考虑如下几点:
-
后像是一个顺序日志流,高并发时会有大量的事务争用日志写入资源,如何降低高并发下的资源冲突;
-
后像的效率如何,数据库异常重启后前滚的效率如何;
-
后像文件的组织方式是否能在性能和可靠性之间找到好的平衡点;
-
后像的设计是否有利于 PITR 、数据复制能延伸功能的高效实现;
MySQL设计原理
物理日志与逻辑日志
物理日志(Physical Log)记录完整的page内容或者page中被修改的内容,采取的方法是记录目标page的物理地址、page内的偏移、修改后的内容、内容的长度。物理日志的优点主要有两个:
-
独立性:日志的恢复不依赖于原 page 的内容,哪怕原始 page 发生了损坏,仍然可以恢复;
-
幂等性:同一条日志重复执行多次结果也是正确的,也不会发生异常;
物理日志有上述优点,缺点也很明显,就是日志量会非常大,一个逻辑操作会设计多处修改,每处都需要物理地记录下来(如BTree分裂操作需要记录的内容和一个完整的page基本相当)。逻辑日志(Logical Log)记录的是表上的某个具体操作,例如插入一行记录或者删除一行记录等等,内容非常简洁,但缺点也很明显,缺乏独立性和幂等性。
物理逻辑日志将物理日志和逻辑日志结合起来,平衡两者的优点。采取的原则如下:
-
physical-to-a-page :将操作细分到 page ,每个 page 单独记录日志;
-
logical-within-a-page : page 内的修改记录逻辑日志;
此外,不管是哪种日志类型,还存在page一致性问题。一致性包括page内的一致性和page间的一致性。page内一致性指某个操作涉及page内的多处修改,例如插入操作涉及page内的PAGE HEADER、USER RECORD、PAGE DIRECTORY等多个部分,一致性要求这些部分都要完成修改。page间的一致性指某个操作涉及page间的修改,且page间存在强的依赖关系,不一致会导致数据结构不一致,例如page间的双向链表指针。
InnoDB存储引擎采用的就是物理逻辑日志方式记录redo log,那InnoDB又是如何解决上述这些异常的呢?
-
独立性: InnoDB 不解决独立性问题,而是要求必须有一个基础 page ,然后在此 page 上 apply redo log ,所以引入 checkpoint 机制和 double write 机制。 Checkpoint 机制会将 dirty page 按照一定规则刷入数据文件(当然 checkpoint 机制不仅解决基础 page 问题,还解决了恢复时长问题), double write 机制解决 page 部分写问题,确保有一个正确的基础 page 来 apply 物理逻辑日志;
-
幂等性: InnoDB 引入了 lsn 机制,每个 page 都会记录本次修改的 lsn ,在 apply redo log 时,如果 page 的 lsn 大于等于日志的 lsn ,表示该条日志已经 apply 过,从而解决重复执行的问题;
-
一致性: InnoDB 引入了 mini transaction 机制,通过 Fix Rules 原则保证了内存中 page 操作的原子性和一致性,通过 mini transaction 内 redo log 的原子性保证 page 间的一致性;
关于checkpoint、doublewrite和mini transaction机制,下面章节会进一步详细展开。
LSN与检查点
所有最终要持久化的修改最终都要记录redo log,所以MySQL直接将redo log的偏移量作为度量修改大小和修改时序(前后关系)的参考,这就是lsn(log sequence number),占用8个字节。lsn出现在多个对象和机制中,表达不同的含义:
-
redo log : redo log 在 redo log buffer 、 redo log files 中的位置和长度;
-
page :标志最近一次修改的逻辑时间;
-
checkpoint :标识最近一次 checkpoint 的逻辑时间,该 lsn 之前的 dirty page 都已经刷入持久化设备;
checkpoint解决两个问题:1)物理逻辑日志需要的基础page问题;2)系统恢复时间过长问题。原则上有一个基础page,我们就可以通过apply redo log恢复至最新的page。不过随着redo log的不断增长,这个apply的过程就不断变长,所以我们就需要一种机制将dirty page刷入持久化设备,降低apply的时长,这个机制就是checkpoint。为了保证数据的正确性,checkpoint机制需要满足WAL原则,并存在如下依赖关系:
-
刷 dirty page 前,需要确保该 page 对应的 redo log 已经刷入持久化设备;
-
redo log file 是循环覆盖的,被覆盖前必须确保对应的 dirty page 必须已经刷入持久化设备,否则后像被覆盖,可恢复性将不再具备;
-
page 缓存区的大小限制、恢复时长等综合因素将触发 dirty page 刷入持久化设备;
上述措施是checkpoint需要考虑的基本原则,由于checkpoint和缓冲区管理有较强的依赖关系,将在“缓存管理”章节进一步介绍。
Mini Transaction
数据库是以事务为单位提供数据管理服务的,但在实现层面MySQL将事务进一步分解为mini transaction,并以mini transaction作为其内部的最小执行单元。从数据库实现的角度来看,有些修改之间存在依赖关系,必须要么都实施,要么都不实施,否则内部数据结构就会被破坏,系统就会异常。例如修改page间的双向指针,相关page都必须修改成功,才能确保系统正常。mini transaction将这些操作定义在一个mini transaction中,必须原子地实施。从redo log来看,通过type中的MLOG_SINGLE_REC_FLAG标志位和MLOG_MULTI_REC_END日志将一组有依赖关系,必须保证page间一致性的redo log封装在一个mini transaction中。归属于一个mini transaction的redo log必须被原子地执行,从而保证page间的一致性。
MySQL将mini transaction设计的更加通用,将其作为执行事务的基本单元,设计锁、page、缓存、log等多个方面,并且满足Fix Rules(即Latch原则)、WAL、Force-Log-at-Commit。Fix Rules原则保证了page间和page间的操作时原子的、一致的。WAL要求dirty page刷入持久化设备前,对应的redo log必须先刷入持久化设备,从而将page的随机IO转换为redo log的顺序IO,并降低dirty page的持久化频率。Force-Log-at-Commit要求事务提交时必须将该事务相关的所有redo log同步刷入持久化设备,从而保证事务ACID特性中的持久化特性。
Double Write
MySQL的redo log是物理逻辑日志,apply redo log前必须要有一个正确的基础page。MySQL的page大小和存储设备的page大小很可能并不一致,所以在page刷入持久化设备的过程中,意外掉电等异常很可能导致page是不完整的。通过FIL HEADER和FIL TAILER可以发现这些不完整的page,但还需要一种机制将不完整的page恢复为完整的page,这就是doublewrite机制。
图3.2-1 double write原理
如图3.2-1所示,MySQL在system table space中开辟了double write segment,为了保证持久设备的连续写性能,前32个frag page舍弃不用,申请两个连续的extent。同时在内存中也开辟了1个2M的缓存,刷page进持久化设备的过程如下:
-
step1 :首先将 page 拷贝到 double write buffer 中;
-
step2 : double write buffer 中的 page 积累到一定程度后刷入 double write segment ;
-
step3 :将已经刷入 double write 中的 dirty page 刷入数据文件;
由于step2和step3是串行的,部分写不可能在step2和step3都发生,从而保证了基础page的可恢复性,即如果数据文件中的page如果存在部分写,那么double write segment中一定有一个该page的完整page。double write是连续的存储空间,一定程度上缓解了double write对性能的影响。
当然如果存储设备能够保证page的原子性写,可以关闭double write功能。Redo log的block大小为512个字节,是所有存储设备的最小写入单元,可以保证原子性,所以redo log不需要采用double write机制。
Redo Log格式与文件组织
图3.2-2 redo log files布局
如图3.2-2所示,MySQL redo log文件有多个大小相同的物理文件组成(文件大小和数量可分别通过参数innodb_log_file_size和innodb_log_files_in_group配置),redo日志循环使用这些物理文件。每个文件有若干个block组成,每个block的大小为512个字节,block是写redo文件的最小单元。每个redo文件的前4个block存放全局信息,其它block用于存放具体的redo日志。
表3.2-1 LOG FILE HEADER结构
表3.2-2 CHECK POINT结构
每个log file的第1个block存放LOG FILE HEADER,记录该redo file的总体信息,详细情况如表3.2-1所示。第1个log file的第2个block和第4个block存放CHECK POINT信息,记录最近一次checkpoint完成的信息,其它log file的对应区域保留为空。采用两个block存放checkpoint信息是防止单个磁盘区域损坏后,仍然可以获得最近的checkpoint信息。CHECK POINT详细情况见表3.2-2所示,LOG_CHECKPOINT_LSN非常关键,记录了checkpoint完成后的lsn,即该lsn前的dirty page都已经刷入持久化设备,所以该lsn前的redo log都可以被释放。redo文件的大小和数量是固定的,block的大小也是固定的,每个文件固定预留出4个block,所以可以根据lsn号算法该lsn对应于哪个redo log文件,以及在该文件中的偏移。
图3.2-3 redo log block结构
表3.2-3 LOG BLOCK HEADER结构
如图3.2-3所示,redo log block包括LOG BLOCK HEADER、LOG BODY和LOG BLOCK CHECKSUM三个部分:
-
LOG BLOCK HEADER :记录 block 的总体信息,详细情况见表 3.2-3 , LOG_BLOCK_HDR_NO 根据 lsn 计算得到,日志文件每次被复用,该 block no 都不同;
-
LOG BODY :存放具体的 redo log ,以 redo log record 为单位存放, 1 个 block 中可以存放多个 mini transaction 的 redo log records , 1 个 mini transaction 的 redo log records 也可能跨越多个 block ;
-
LOG BLOCK CHECKSUM :尾部,占 4 个字节,整个 block 的 checksum ,用于校验本 block 是否损坏;
图3.2-4 redo log record结构
如图3.2-4所示,具体单条redo log record由4个部分组成,type表示redo log record的类型,space和page no表示本条redo log record针对哪个page,body是具体的日志内容。type的种类达XXX,body随着type的不同而不同:
-
MLOG_SINGLE_REC_FLAG(128) : type 占 1 个字节,最高位是标志位,如果为真表示本条 redo log record 就是 1 个 mini transaction ,否则表示本条 redo log record 只是 mini transaction 中 1 条 record ;
-
MLOG_MULTI_REC_END(31) :表示 1 个 mini transaction 的结束,本 redo log record 没有 body 部分;
-
MLOG_1TYPE(1) 、 MLOG_2TYPES(2) 、 MLOG_4TYPES(4) 、 MLOG_8TYPES(8) 、 MLOG_WRITE_STRING(30) :记录 page 链表指针、文件头、 segment page 等修改,详细情况见表 3.2-4 、表 3.2-5 、表 3.2-6 ;
-
MLOG_REC* 、 MLOG_LIST_* 、 MLOG_COMP_REC_* 、 MLOG_COMP_LIST_* :记录 BTree page 的 insert 、 delete 、 update 和 page 分裂合并操作,表 3.2-7 和表 3.2-8 分别给出插入和删除的 redo record 格式;
-
MLOG_FILE_CREATE(33) 、 MLOG_FILE_RENAME(34) 、 MLOG_FILE_DELETE(35) 、 MLOG_PAGE_CREATE(19) 、 MLOG_INIT_FILE_PAGE(29) 、 MLOG_PAGE_REORGANIZE(18) :记录文件和 page 的操作;
-
MLOG_UNDO_* :记录 undo log ;
表3.2-4 MLOG_1BYTE、MLOG_2BYTES、MLOG_4BYTES结构
表3.2-5 MLOG_8BYTES结构
表3.2-6 MLOG_WRITE_STRING结构
表3.2-7 MLOG_COMP_REC_INSERT结构
表3.2-8 MLOG_COMP_REC_DELETE结构
日志持久化
图3.2-5 redo log record持久化机制
从上面章节我们知道MySQL将mini transaction(MTR)作为执行单元,redo日志也是以mini transaction为单位写入到redo log buffer,并最终写入到redo日志文件中。如图3.2-5所示redo日志从生成到写入日志文件经过如下过程:
-
mini transaction 维护一个本地缓存, mini transaction 执行期间生成的 redo log records 存放在本地缓存中;
-
Mini transaction 执行完成后,将本地缓存中的 redo log records 提交到公共的 redo log buffer 中(缓存的大小可通过参数 innodb_log_buffer_size 设置);
-
当整个事务提交时, redo log buffer 中的 redo log block 写入到 redo log 文件中;
存储及缓存按照上述三个层次进行组织,但实际执行过程还是比较复杂的。首先看mini transaction的执行和提交过程:
-
step1 :如果待提交的 redo log records 超过了 redo log buffer 的 1/2 ,将 redo log buffer 扩大一倍;
-
step2 :如果本次写入会覆盖检查点,强制进行一次同步 checkpoint ;
-
step3 :检查本次修改的 data page 是否是该 table space 自上次 checkpoint 以来的第一次修改。如果是第一次则增加 1 条 MLOG_FILE_NAME 日志(此为 5.7 的优化,重启恢复时只需要打开相关的 table space 文件);
-
step4 :根据 redo log records 的数量,打上 MLOG_SINGLE_REC_FLAG 标志,或者增加 MLOG_MULTI_REC_END ;
-
step5 :将 redo log records 从 mini transaction 的本地缓存拷贝到公共的 redo log buffer ;
-
step6 :如果 redo log buffer 中被写的最后 1 个 block 未写满,设置该 block 的 LOG_BLOCK_FIRST_REC_GROUP ,并根据 log buffer 、 max_modified_age_async 、 max_checkpoint_age_async 情况设置 check_flush_or_checkpoint (该标志一旦被设置,用户线程在修改 data page 时会刷 dirty page 或 redo log );
-
step7 :将本 mini transaction 涉及的 dirty page 加入到 flush list 中;
-
step8 :释放本 mini transaction 持有的 page latch 、内存等资源;
在上述过程中,step2~step6一直持有log_sys->mutex锁,即在此期间其它mini transaction及后台线程都无法操作公共的redo log buffer。step8释放本mini transaction持有的page latch,即在step8之后,其它会话才能操作本mini transaction操作的data page。
将redo log从redo log buffer刷入redo文件是一个持续的异步过程。Redo log buffer空间不足、事务提交、定期(每1秒1次)、DML执行前redo log buffer可用空间小于1/2、checkpoint(遵守WAL)、shutdown数据库实例、切换binlog等都会触发将redo log从redo log buffer写入redo文件。一般情况下,redo block都是写入操作系统缓存,由操作系统异步持久化,从而提升效率。但事务提交要满足持久化特性,日志必须持久化成功,这时需要调用fsync确保真正写入持久化设备。由于fsync执行时间比较长,MySQL在调用fsync前会释放log_sys->mutex,这样其它事务可以继续写日志,下次fsync可以通过将多个事务的redo log一次写入持久化设备,从而达到组提交的目的。
重做日志恢复
当数据库正常关闭时,数据库会完成所有dirty page的持久化,并将最后持久化的lsn记录到system table space第1个page的FIL_HEADER的FIL_PAGE_FILE_FLUSH_LSN中。当数据库重启后,会读取FIL_PAGE_FILE_FLUSH_LSN,同时读取redo log文件中的两个checkpoint。比较FIL_PAGE_FILE_FLUSH_LSN和checkpoint(max(checkpoint1, checkpoint2)),如果相等则不需要恢复,否则需要恢复,恢复的区间为(checkpoint lsn, redo log last lsn)。
图3.2-6 并行恢复内存结构
如图3.2-6所示,MySQL对space id和page no做hash,维护一个hash列表,列表中每个bucket对应1个recv_addr_t结构。如果有hash冲突,则将recv_addr_t通过add_hash串在一起。recv_addr_t通过recv_t结构将本page的redo log recod串在一起,每个recv_t对应于一条redo log record,并记录该record的type、len、data(日志内容)、start lsn、end lsn。同一个page的recv_t按照lsn顺序穿在一起。
系统恢复期间,从(checkpoint lsn,redo log last lsn)区间批量读取redo log record,并存放到图3.2-6的内存结构中。如果内存无法容纳,则先开始apply这些redo log record。apply完毕后,再批量读取,再apply,如此循环直至完成所有redo log record的前滚工作。
在apply redo log record期间,各page是可以并行apply的(物理逻辑日志的设计原则为physical-to-a-page)。如果page的lsn大于等于redo log record的lsn则跳过该条redo log record(日志的幂等性)。如果所涉page不在缓冲区,则批量读取相关page到内存。如果缓存区内存不足,可以将已经apply完毕的dirty page写入持久化设备。
Oracle设计原理
物理日志与逻辑日志
图3.3-1 逻辑日志、物理日志、物理逻辑日志示例
如图3.3-1所示,逻辑日志记录逻辑操作,物理日志记录block的物理变更,物理逻辑日志结合逻辑日志和物理日志的优点:
-
将操作细分到 block ,每个 block 独立记录日志;
-
每个 block 内记录逻辑日志;
Oracle和MySQL一样采用物理逻辑日志记录redo log,并同样需要解决如下问题:
-
独立性: Oracle 日志本身不解决独立性问题,要求必须有一个基础 block ,然后在此 block 上 apply redo 日志,所以需要 checkpoint 机制。 checkpoint 机制会将 dirty page 按照一定规则刷入持久化设备,确保有一个正确的 block 来 apply 物理逻辑日志;
-
幂等性:引入 scn 机制,每个 block 都会记录本次修改时的 scn 和 seq 。 Apply redo 日志时,通过比对 redo log 和 block 中的 scn 和 seq ,解决重复执行的问题;
-
一致性:通过 Fix Rules 原则保证内存中 block 操作的原子性和一致性,通过 mini transaction 将 undo block 、 data block 等相关的日志打包到一个 redo record 中;
Oracle采用物理逻辑日志的方式组织redo日志,最终体现在change vector和redo record这两个结构上。Change vector记录单个block的逻辑修改,redo record由若干个change vector组成,表现为一个mini transaction。例如,一般情况下,1个redo record由1对change vector组成,分别对应某次修改涉及的undo block和data block。
SCN与检查点
Oracle数据库需要一个轻量高效的机制对系统中发生的事件进行排序,从而理清各事件发生的前后关系。为此,Oracle设计了(system change number),一个高效的逻辑时钟,表现为单调递增的数值,由2个字节的wrap scn和4个字节的base scn组成。Scn驻留在SGA中,由system commit number latch保护,任何线程或进程要操作scn都要先获得该latch(scn设计为2个字节+4个字节,4个字节是CPU操作整型的最小单元,在某些场景下latch保护可以进一步优化)。Scn的推进机制如下:
-
每个事务的开启和提交都需要明确的时序关系,所以都会导致 scn 加 1 ;
-
每个 block 通过 cache layer.scn 标识本 block 被修改的时序,如果 block 在同一个 scn 下被频繁修改,则通过 cache layer.seq 标识各次修改之间的前后关系。但 seq 只占 1 个字节,所以同一个 scn 内某 block 的修改次数超过 255 , scn 加 1 ;
-
每隔 3 秒自动加 1 ;
-
Oracle 内部发生其它事件;
Oracle内部会控制scn增长率,参数为_max_reasonable_scn_rate,默认32K,即每秒增加不超过32K。即使按照峰值32K计算,scn用满也需要250年。当然,scn只是逻辑时间,在Oracle内部可以非常高效地表示事件的前后关系,但对DBA来说可读性差,所以后台smon进程在sysaux table space中维护了一张SMON_SCN_TIME表,记录scn与墙上时间的对应关系:
-
Oracle9.2 每 5 分钟更新 1 次,共计维护 1440 条记录,所以粒度为 5 分钟,可查询的历史时间为 (1440*5)/(24*60)=5 天;
-
Oracle10 每 6 秒更新 1 次,共计维护 144000 条记录,所以粒度为 6 秒,原则上可查询的历史时间为 (144000*6)/(24*60*60)=10 天,实际上 Oracle 后继版本采用动态调整算法,当 scn 增长缓慢时 SMON_SCN_TIME 表的更新频率也变慢;
checkpoint机制同样也用于解决基础block和恢复时长问题,也需要遵守WAL原则。为了识别是否需要恢复,采用实例恢复还是介质恢复,Oracle在checkpoint完成时会记录如下scn:
-
system checkpoint scn :系统检查点 scn ,存在于控制文件中, Oracle 会根据 checkpoint 情况持续更新该 scn ;
-
datafile checkpoint scn :文件检查点 scn ,存在于控制文件中,每个数据文件一个, Oracle 会根据 checkpoint 情况持续更新该 scn (只读 table space 不更新);
-
datafile stop scn :结束 scn ,存在于控制文件中,每个数据文件一个,正常运行期间为 null ,系统正常关闭时设置为结束时的 scn ;
-
datafile header start scn :文件检查点 scn ,存在于每个数据文件的文件头中, Oracle 会根据 checkpoint 情况持续更新该 scn ;
checkpoint机制和缓冲区管理有较强的关系,checkpoint的触发及写入机制将在“缓存管理与检查点”章节做进一步介绍。
Fractured Block
当数据库block size大于存储系统的block size时,掉电、操作系统异常等异常很可能导致block断裂(fractured block)。Oracle、MySQL的redo log采用的都是物理逻辑方案,基础block的缺失都会导致无法恢复。为此,MySQL通过double write机制解决partial write问题,那Oracle又是如何解决的呢?
Oracle并没有采用MySQL的事前预防方案,而是采用事后补救方案,主要基于如下考虑:
-
如果在恢复过程中发现有 fractured block ,从备份数据中提取一个可用的基础 block ;
-
double write 方案每次写 dirty page 都要写两次持久化设备,且是串行的,影响性能;
-
Oracle 的默认 block size 为 8K ,发生 fractured block 的概率较小;
-
在关键应用系统中,会有更强的断电保护措施,存储设备的原子写能力也会更强,发生 fractured block 的概率进一步降低;
可见Oracle为了正常运行期间的性能最大化,采取了事后补救方案,而事后补救方案的核心是备份过程中不能再出现fractured block。备份解决fractured block问题的原理如下:
-
Hot Backup 方案:由于采用的是操作系统 cp 命令, cp 命令和 dbwr 很可能同时操作同一个 block ,所以数据文件的拷贝很可能出现 fractured block 。为此,一旦启动 hotbackup ,某 block 第一次被修改时,会将该 block 的整块内容作为后像保持到 redo log 中(是整个 block ,而不仅仅是 block 中被修改的内容, change vector 的操作类型为 18.1 block image )。这样即使拷贝出的数据文件中有 fractured block , redo log 中也有该 block 的基础 block ;
-
RMAN 备份方案:由于 RMAN 能够完全识别出数据库 block 的格式,通过 cache layer ( checkval 、 scn 、 seq )和 footer 就可以发现 fractured block 。如果是 fractured block 就重复读取,直至读到正常的 block ;
Redo Log格式与文件组织
图3.3-2 redo log files全景图
如图3.3-2所示,Oracle中与redo日志发生直接或间接关系的主要进程有:
-
DBW :将 database buffer cache 中的 dirty block 写入 data file 中,但需要考虑 WAL 原则,即 dirty block 写入 data file 前需要确保该 dirty block 对应的 redo log 已经写入到 online redo log file 中;
-
LGWR :将 redo log block 持续地从 redo log buffer 写入到 online redo log file 中;
-
CKPT :将 checkpoint 信息写入到 control file 和 data file (头部)中;
-
ARC :运行在归档模式下,将 online redo log files 及时归档,并将归档信息写入到 control file 中;
可见和redo log相关的文件有control file、data file、online redo log file和archive redo log file。Control file是一个二进制物理文件,是数据库启动的入口,存放数据库的总体信息,主要包括:
-
FILE HEADER :记录 control file 的总体信息,如版本号、 DB ID 、 DB NAME 、 block size ( control file 也是以 block 为单位组织的), file size ( control file 的大小,即 block 的数量), file type 等;
-
DATABASE ENTRY :数据库详细信息,如 data file 的数量、 redo log file 的数量、检查点信息( system checkpoint scn );
-
CHECKPOINT PROCESS RECORDS :检查点详细信息;
-
EXTENTED DATABASE ENTRY :备份相关信息;
-
REDO THREAD RECORDS : redo log buffer 相关信息;
-
LOG FILE RECORDS : redo log file 相关信息;
-
DATA FILE RECORDS :数据文件相关信息( datafile checkpoint scn 、 datafile stop scn );
-
临时文件条目、表空间条目、 RMAN 配置条目、闪回日志文件条目、进程实例映射条目等等;
图3.3-3 redo log file布局
如图3.3-3所示,online redo log files由多个redo log group组成(至少2个),LGWR循环写redo log group。每个redo log group中可以有多个log file(redo log member),这些log file存放的内容是相同的,从而防止单个持久化设备异常导致redo log丢失。LGWR是循环写日志文件的,所以每个redo log file可能处于下列状态之一:
-
CURRENT :当前正在写的日志文件,做实例恢复时 current 状态的日志文件是必须的;
-
ACTIVE :不是当前正在的写的日志文件,但日志对应的 dirty block 尚未刷入持久化设备,实例恢复时 active 状态的日志文件也是必须的;
-
INACTIVE :日志对应的 dirty block 已经刷入到持久化设备,所以 inactive 状态的日志文件可以被恢复,但介质恢复时仍然需要 inactive 状态的日志文件;
-
UNUSED :尚未写入任何日志的文件,一般是刚加入系统的日志文件;
-
CLEARING :正在进行日志清空的文件( alter database clear logfile ),清空完成后状态转换为 unused ;
表3.3-1 File Header Block部分关键信息
表3.3-2 Redo Header Block部分关键信息
表3.3-3 Block Header关键信息
图3.3-4 redo file结构
下面来看redo log file的内部组织结构。如图3.3-4所示,每个redo log file由固定大小的block组成,block的大小随操作系统而变化,一般为512个字节。Redo log file中各block的情况分布如下:
-
第 1 个 block : file header block ,存放本 redo log file 作为文件的总体信息,关键信息有文件类型、 block 大小、文件大小等,详细情况见表 3.3-1 ;
-
第 2 个 block : redo header block ,存放本 redo log file 作为 redo log 类文件的总体信息, db id 和 sid 给出了数据库信息, file number 给出了在日志组中的位置, low scn 和 next scn 给出了本日志文件的起始 scn 和结束 scn ,详细情况见表 3.3-2 ;
-
剩余 block : redo record block ,存放具体的 redo records , redo record 的长度是变化的,所以有可能 1 个 block 中存放多条 redo record ,也有可能 1 条 redo record 跨越多个 block ;
-
除第 1 个 block 之外,其它 block 都有 1 个固定大小的 block header 结构(占 16 个字节),用于描述本 block 的总体信息,详细情况见表 3.3-3 ;
表3.3-4 redo record header部分关键信息
表3.3-5 change vector header部分关键信息
图3.3-5 change vector结构
Change vector针对的是具体某个block的后像,其组成部分如图3.3-5所示,具体包括3个关键部分:
-
change vector header :头部,主要描述 change vector 的总体信息,如操作的类型( op, 做了哪种操作),操作的目标对象( block 的地址 dba 以及该 block 的类型 cls ),操作的逻辑时序( scn 和 seq ),详细情况见表 3.3-5;
-
change record : change vector 由多个 change record 组成,用于存放具体的后像。如 insert ( 11.2 insert row piece )由 3 个 change record 组成,分别为 ktb ( kernel transaction layer )、 kdo ( rowid )、 insert 字段的后像值。 Update ( 11.5 update row piece )由 4 个 change record 组成,分别为 ktb ( kernel transaction layer )、 kdo ( rowid )、 update 所涉字段的编号及后像值;
-
length vector :描述各个 change record 的长度,内容为 lv_len+len1+len2+...+lenN ,每个 len 占 2 个字节, lv_len 描述 length vector 的长度, lenN 描述 change recordN 的长度, length vector 及各个 change record 的长度都 4 个字节对齐;
图3.3-6 redo record结构
Change vector是针对某个具体block的,而Oracle是以mini transaction为单位组织和管理数据修改的,把归属于同一个mini transaction的一组change vector存放在一个redo record中。例如,针对某条记录的更新,设计记录的undo、记录本身、索引的undo、索引本身,将这些修改的change vector封装在一个redo record中。Redo record的结构如图3.3-6所示,包括如下组成部分:
-
redo record header :描述本条 redo record 的总体信息,主要包括 redo record 的地址( rba )、长度( len )、逻辑时间( scn )和墙上时间( timestamp ),详细情况见表 3.3-4 ;
-
change vector :存放具体某个 block 的修改信息(前像);
至此,我们可以根据rba地址从redo log file中找到对应的redo record,并据此解析出我们需要的信息:
-
通过 rba 可以计算出 redo record 所在的 redo 文件、 block no ,以及在 block 内的偏移;
-
通过 record header 中的 len 可以得到本条 redo record 的长度,也可以据此得到下一条 redo record 的位置;
-
record header 只有就是第 1 条 change vector ,根据 change vector header 之后的 length vector 获得本 change vector 的总长度,并能据此得到下一条 change vector 的位置;
-
获得 change vector header 以及各个 change record 的长度之后,就可以提取所有前像内容;
表3.3-6 change vector header主要操作码
图3.3-7 redo record&change vector示例
最后我们看一下DML语句对应的redo record和change vector是如何组织和布局的。如图3.3-7所示,假设表t1,该表上有索引index1,事务分别执行insert、update和delete语句。生成redo record和change vector的过程如下:
-
事务的第 1 条语句是 insert 语句,该语句涉及 data block 、 index block 的修改,以及对应 undo 记录的生成,这些都需要记录 redo 日志,并作为 1 条 mini transaction 打包到 1 条 redo record 中:
-
change vector1(5.2) :启动事务需要更新 undo segment header block 中的 transaction control 和 transaction table ,本 change vector 记录 undo segment header 的后像;
-
change vector2(5.1) :对 data block 执行 insert 操作需要产生 undo 记录,本 change vector 记录该 undo 记录的后像;
-
change vector3(11.2) :记录 data block 中本次 insert 操作的后像;
-
change vector4(5.1) 、 change vector5(10.2) :分别记录对 index block 执行 insert 操作时, undo block 和 index block 的后像;
-
-
事务的第 2 条语句是 update 语句,该语句同样涉及 data block 、 index block 修改,以及对应的 undo 记录,对应于第 2 条 redo record :
-
Change vector1(5.1) 和 change vector2(11.5) 分别记录 undo block 和 data block 的后像,对应于记录的 undo 和记录本身;
-
oracle 会将索引的更新转换为索引的删除和插入操作,每个操作又封闭有 undo block 和 index block 上的后像,索引对应 change vector3(5.1) 、 change vector4(10.4) 和 change vector5(5.1) 、 change vector6(10.2) ;
-
-
事务的第 3 条语句是 delete 语句,该语句同样涉及 data block 、 index block 修改,以及对应的 undo 记录,对应于第 3 条 redo record :
-
change vector1(5.1) 和 change vector2(11.3) 对应于记录的 undo 和记录本身;
-
change vector3(5.1) 和 change vector4(10.4) 对应于索引的 undo 和索引本身;
-
-
事务的第 4 条语句是 commit 语句,对应于第 4 条 redo record , change vector ( 5.4 )记录 undo segment header block 中 transaction table 中的状态设置,表示事务已经提交;
上述过程给出了Oracle在事务执行期间生成redo record的最简过程,单条语句也可能涉及大量的数据修改,这时会生成多条redo record。对于单条语句的redo record,其记录的主要内容如下:
-
insert(11.2) :记录插入行的各列值;
-
update(11.5) :记录被更新列的目标值;
-
delete(11.3) :记录删除行的 RowID ;
-
undo(5.1) :记录 undo record heap ;
日志持久化
Oracle在下列情况下将redo log buffer中的redo record刷入持久化设备中:
-
每 3 秒触发;
-
阈值达到触发,包括 redo log buffer 已使用 1/3 空间,或者 redo log buffer 已缓存 1M 日志;
-
用户提交触发,满足事务的持久化特性;
-
DBW 触发,满足 WAL 原则;
用户提交触发redo record持久化,在高并发下日志持久化过程将是并发冲突的关键瓶颈点。为此,Oracle在持续优化日志持久化机制,表现为初始版本只有1个redo log buffer,在Oracle7引入多个redo log buffer以提升并发性,并在Oracle8、Oracle9中进一步增强为public redolog strands(PBRS),Oracle10引入了private redo log buffer机制,即private redolog strands(PVRS),进一步降低冲突,提升并发性。由于这些机制的基本原理是相同的,所以首先从1个redo log buffer讲起。
图3.3-8 redo log buffer区域划分
如图3.3-8所示,redo log buffer也是循环使用的,可以划分为空闲区域、LGWR正在持久化区域、用户进程正在写入区域。指针A指向空闲区域的头部,用户进程每次申请新的待写入区域,指针A都会向前推进。指针B指向空闲区域的尾部,一旦LGWR完成某段区域的持久化,该区域就会被释放出来,表现为指针B从Point1推进到Point2。
了解了redo log buffer的区域原理后,我们来看用户进程写redo record的过程。用户在自己的PGA区域中缓存redo record,当用户提交或者达到一条完整的redo record后,就将该redo record拷贝到redo log buffer中。过程如下:
-
获取 redo copy latch ;
-
获取 redo allocation latch ;
-
为本次 redo record 分配 redo log buffer 空间,即移动指针 A ;
-
释放 redo allocation latch ;
-
将 redo record 从 PGA 复制到 redo log buffer 的对应区域中;
-
释放 redo copy latch ;
-
如果此时 redo log buffer 已使用空间达到 1M 或者达到总空间的 1/3 ,通过 LGWR 进程写持久化设备;
-
如果 redo record 是提交日志,通知 LGWR 写持久化设备,并将自己和日志地址放到 xxx list 中;
-
将 redo record apply 到对应的 data block 和 undo block 上;
Data block或undo block的实际变更发生在对应的redo record拷贝到redo log buffer之后。用户进程在通知LGWR时,首先获取redo writing latch,并检查写标志位。如果获取不到redo writing latch,或者写标志位已经被置上,表明LGWR已经在运行中,不需要重复发送通知,否则要将LGWR进程唤醒。LGWR进程唤醒后的执行过程如下:
-
step1 :获取 redo writing latch ;
-
step2 :设置写标志位;
-
step3 :释放 redo writing latch ;
-
step4 :获取 redo allocation latch ;
-
step5 :计算本次要持久化的内存区域,原则上指针 B 到指针 A 的区域都需要持久化,但此时可能存在尚未完成从 PGA 到 redo log buffer 的拷贝, LGWR 通过监视 redo copy latch 判断拷贝完成的时机,最终计算出 point2 ;
-
step6 :释放 redo allocation latch ;
-
step7 :将指针 B 到 point2 之间的内存持久化到日志文件中;
-
step8 :获取 redo allocation latch ;
-
step9 :将指针 B 移到 point2 位置;
-
step10 :释放 redo allocation latch ;
-
step11 :检查 xxx list ,如果已持久化的日志地址大于记录的日志地址,通知这些用户进程提交完成;
-
step12 :检查 xxx list ,如果还有用户进程在等待日志持久化,进入 step4 继续持久化;
-
step13 :检查 xxx list ,如果没有用户进程在等待日志持久化,获取 redo writing latch ,将写标志置为空,释放 redo writing latch ,将自己阻塞到后台;
LGWR持续地尽最大可能地持久化日志,通过设计redo allocation latch、redo copy latch将提交的并发度尽可能最大化:
-
LGWR 写日志到持久化设备是最消耗时间的,但此时不占用任何 latch ,不影响用户进程的并发提交;
-
用户进程的最长时间发生将日志从 PGA 拷贝到 redo log buffer 上,此时仅占用 redo copy latch ,不影响 LGWR 和其它用户进程申请空间;
可见,LGWR在写日志期间,大量的并发事务仍然可以向redo log buffer写日志,这样LGWR在下一轮持久化时可以一次性完成所有事务的提交,这就是组提交。
图3.3-9 并行redo log buffer布局
在单个redo log buffer时,同时只能有一个用户进程向redo log buffer拷贝redo record(由redo copy latch保护)。单个用户进程本身会频繁地向redo log buffer拷贝redo record(1条redo record拷贝一次),高并发下大量用户进程并发向redo log buffer写redo record,表现为大量用户进程争用redo allocation latch和redo copy latch。为此,Oracle将单个redo log buffer优化为多个redo log buffer,每个buffer都有自己的redo allocation latch和redo copy latch,从而提高并发性。同时用户进程写redo log buffer的过程做如下调整:
-
以立即获得模式尝试所有 redo log buffer 的 redo copy latch ,如果全部都无法获得则随机阻塞在某个 redo copy latch 上等待;
-
获得了 redo copy latch ,就决定了使用哪个 redo log buffer ,然后申请对应的 redo allocation latch ;
LGWR进程只有1个,所以redo writing latch数量不变。不过在计算各redo log buffer的持久化区域时需要获得所有redo log buffer的redo allocation latch,一旦完毕就可以释放这些redo allocation latch。等待用户进程完成各redo log buffer中的持久化区域的拷贝,然后确定各redo log buffer日志写入online redo log file的顺序,并开始持久化过程。
多个redo log buffer主要用于解决大量用户进程高并发地写redo log buffer,而大量用户进程的并发性实际上取决于cpu的数量。因此,Oracle根据cpu数量自动化管理redo log buffer的数量,计算公式为celing(1+cpu_count/16),当然也可以通过参数_log_parallelism_max和_log_parallelism_dynamic进行干预。
最后再讨论一下redo log浪费和PL/SQL提交改进。LGWR将redo log buffer刷入redo log file的单位redo block。当刷入时如果redo block尚未写满,Oracle出于性能考虑会将尚未写满的部分做好填充,然后写入redo log file。否则LGWR不得不从持久化设备上读出本redo block,然后更新本次内容,再写入redo log file,发生2次IO,影响性能。当然在高并发下,组提交是大概率事件,redo log浪费不会太严重。
PL/SQL代码块中用户的代码逻辑可能是循环提交,这时Oracle会做优化,不会每次提交都将redo record复制到redo log buffer,并等待LGWR将对应搞得redo block写入持久化设备。而是牺牲一定的持久性,在循环完成的哪个时间点再进行提交(当然如果日志量过大也会提前出发)。在Oracle11版本将这些优化开放给用户选择,用户可以通过commit_logging和commit_waiting进行控制。
PVRS与IMU
通过多个redo log buffer一定程度上缓解了redo log buffer的争用,但此处仍然是系统的热点所在。为此,Oracle在redo log buffer基础上引入了private redo log buffer,即PVRS。相对于PVRS,原来的多个redo log buffer称为PBRS。PVRS是在shared pool中开辟大量小的private redo log buffer(32位版本每个buffer的大小为64K,64位版本每个buffer的大小为128K),每个private redo log buffer有一个redo allocation latch保护。用户进程启动某个事务时,申请绑定某个private redo log buffer,即申请该private redo log buffer的redo allocation latch,之后该事务在运行过程一直占用该private redo log buffer,针对data block产生的change vector直接记录在该private redo log buffer中,不再需要频繁地向redo log buffer进行拷贝。当然,如果申请不到private redo log buffer,仍然走原来的流程,即PBRS。
PVRS缓存了data block的change vector,那么undo block的change vector怎么办呢?为此Oracle设计了IMU机制。IMU同样在shared pool中开辟大量小的IMU buffer(64K~128K),事务在运行期间产生的undo日志不直接存放到undo block中(对undo segment header block的修改还是实时进行的),而是存放在IMU buffer中。在事务提交前,没有实际产生undo block,所以也就不会产生undo dirty block,不会发生相关IO,用户回滚也非常高效。
为此,change vector成对打包(分别针对undo block和data block)写入redo log buffer,以及修改data block的过程调整如下:
-
获得一个 private redo log buffer 和一个 IMU buffer ;
-
为本事务所涉的 data block 打上 PVRS 标签(不修改 data block );
-
将每条与 undo block 相关的 change vector 保存到 IMU buffer 中;
-
将每条与 data block 相关的 change vector 保存到 private redo log buffer 中;
-
在事务提交时,将 IMU buffer 与 private redo log buffer 中的 change vector 合并为一条 redo record ,并复制到 PBRS 中;
-
根据 redo record 修改相关 undo block 和 data block ;
通过上述缓存机制极大地降低了申请PBRS的次数,一个事务申请一次,从而降低了redo copy latch和redo allocation latch的争用。当然IMU buffer引入了IMU latch,每个IMU buffer都需要一个IMU latch保护。不过从undo的角度来看,一个IMU latch替代了一个redo copy latch和redo allocation latch,且IMU本身latch随着IMU buffer的增加而增加,降低了争用概率。
由于PVRS和IMU数量有限,且PVRS和IMU本身的容量也有限。当事务开始时申请不到PVRS和IMU,仍然走PBRS路径。即使在事务执行过程中,有PVRS和IMU释放出来,该事务也仍然走PBRS路径。当PVRS和IMU缓存满时,事务也会提前结束PVRS和IMU,将redo record复制到PBRS中,后继执行过程走PBRS路径。
PVRS和IMU的引入还带来一个潜在问题,当redo log file发生切换时,private redo log buffer中的redo record尚未写入到redo log file中。这时很可能出现private redo log buffer中缓存的redo record的scn小于当前redo log file的low scn。为了解决该问题,Oracle会计算所有private redo log buffer、IMU buffer、redo log buffer的缓存总大小。如果当前redo log file的剩余空间等于缓存的总大小时,强制将缓存中的日志写入redo log file,并进行日志文件切换。此时各缓存可能并没有用满,所以redo log file最后一点区域可能是空的,这就是为什么归档日志文件有时比在线日志文件小一点的原因。
重做日志恢复
数据库正常关系时,系统检查点scn、各数据文件检查点scn和结束scn、各数据文件头部的开始scn都是相等的。这时数据文件中的数据是最新的,重启后无需根据redo log file做任何恢复或前滚操作。
数据库非正常关系时,系统系统检查点scn、各数据文件检查点scn、各数据文件头部的开始scn都是相等的,但各数据文件的结束scn为无穷大。这时数据文件中的数据很可能不是最新的,需要进行实例恢复。从控制文件检查点条目中找到lrba(检查点队列中第一个dirty block第一次修改的redo record的地址),以该rba为起点,apply完redo log file之后的所有redo record。
数据文件异常,从备份系统中提取该数据文件的历史版本。该数据文件头部的开始scn小于该数据文件检查点scn,此时需要进行介质恢复。以数据文件头部的开始scn为起点,apply万redo log file中之后所有与本数据文件相关的redo record。可能的方法是通过各redo log file的low scn和next scn(在redo header block中)定位到起始的redo log file,再遍历redo record,通过redo record中的scn定位到起始的redo record。
在apply redo record的过程中,会比较data block中的scn、scn(cache layer)与change vector中的scn、scn(change vector header)。当满足下列两个条件之一时,不需要apply,跳过本change vector,从而解决redo log的幂等性问题。两个条件分别为:
-
data block 中的 scn 大于 change vector 中的 scn ;
-
data block 中的 scn 等于 change vector 中的 scn ,但 data block 中的 seq 大于等于 change vector 中的 seq ;
总结与分析
事务的持久性要求事务提交时redo log必须完成持久化工作,从而导致redo log的持久化机制成为数据库高并发下的热点争用资源。为此,Oracle和MySQL都设计了事务本地缓存、公共缓存、日志文件三级机制。本地缓存以mini transaction为单位将日志从本地缓存拷贝到公共缓存,公共缓存以block为单位再将日志从公共缓存持久化到日志文件中。MySQL通过log_sys->mutex保护三级机制之间的协调,Oracle则通过redo copy latch、redo allocation latch、redo writing latch进行协调,以获得更好的并发性能,并在此基础上进一步研发出多个redo log buffer,private redo log buffer和IMU。Oracle在高并发上相对于MySQL做了大量的优化工作,所以在多核环境下Oracle的并发性优势会非常明显。
MySQL以日志在日志文件中的相对顺序(lsn)作为度量数据库内部事件的先后关系,Oracle则采用逻辑时间(scn)来标识事件的前后关系。lsn的优势是简单,但和日志中的偏移强绑定,在集群或分布式环境中,涉及多个独立的日志,标识全局时序关系会非常困难。Scn是独立的逻辑时间,既避免了日志系统的束缚,又避免了墙上时间的种种限制。在集群或分布式环境下,各节点可以按照某种同步算法进行同步,从而为所有节点上的事件进行排序。Oracle为了解决高频更新下scn推进得过于频繁,在每个block上增加了seq,降低数据的频繁更新给scn带来过大压力的问题。
在block或page损坏上,MySQL将partial write单独区分出来,并通过double write来解决。Oracle则拉通所有的block损坏,通过备份、备机等方式来修复损坏的block。这与两者的设计理念有关,MySQL主要运行在低端环境中,且默认的page大小为16K,所以掉电或异常重启导致partial write的概率较大。Oracle的默认block大小为8K,并认为partial write是小概率事件。为了正常情况下的性能最大化,没有采用事前预防的方案,而是归结到通用的block损坏,通过事后修复的方案来补救。相比Oracle和MySQL,PostgreSQL采取了折中方案,并没有每次写dirty page时都写两次,而是在每次checkpoint之后dirty page第一次被写出时需要同时写一份到xlog中,该方案的缺点是增加xlog的大小。
在redo log设计上,MySQL与Oracle也非常相似,insert记录所有列,update记录更新所涉相关列,delete记录标识(Oracle为RowID,MySQL为主键列),undo记录整条前像。两者都以mini transaction为单位管理日志,MySQL的mini transaction粒度较小,Oracle出于性能的考虑,在引入private redo log buffer和IMU之后,mini transaction的粒度进一步增大。另外,Oracle的功能特性较多,所以redo log的类型远多于MySQL。
在前滚效率上,MySQL和Oracle都实现了并行前滚。MySQL在前滚完成后再进行回滚,只有前滚动作和回滚动作都完成之后才可以对外提供服务。Oracle也需要先进行前滚再进行回滚,不过为了尽快提供服务,前滚完成可以提供访问服务,回滚动作由SMON进程在后台逐步实施。
在redo log file的组织上,MySQL比较简单,仅仅是循环写redo log file。虽然设计之初考虑过文件镜像和归档功能,但都没有实现,而将这些可用性特性留给了外部系统,如存储设备。Oracle除了实现镜像和归档功能之外,将相关信息归类到控制文件、数据文件和日志文件中:
-
checkpoint 信息记录在控制文件中, MySQL 则记录在第一个日志文件中,对于第一个日志文件来说,对顺序写的日志文件有一定影响;
-
数据文件头记录开始 scn ,能够对单个数据文件进行更加精细化的介质恢复, MySQL 只能基于日志做实例恢复( xbackup 的恢复粒度也比较粗);
Oracle基于后像做了很多拓展功能,如LogMiner、物理复制、PITR、Stream等等。MySQL在redo设计上相关要素都有,原则上页可以实现上述功能。不过MySQL服务层已经有binlog,binlog在逻辑上更加简洁,所以复制、PITR等大部分功能都是在binlog上构建的。但binlog是逻辑日志,在效率上要低于redo日志。