WAL实现数据库的可靠性

  之前描述到InnoDB事务部分的内容,一直感觉通过搜集各方面资料整理起来的内容都比较的散乱,说句实话很多东西现在自己也都串不起来。最近在看其他文章的时候也涉及到了Write Ahead Log方面相关的知识,感觉又些许有了眉目,实际上WAL是实现事务可靠性的一种常见的手段,而不仅仅在MySQL上才有的哦。
  学习过数据库的人肯定对ACID(Atomicity,Consistency,Isolation,Durability)特性十分的熟悉,但在数据库实现中,如果在有数据更新的时候直接覆盖原始数据,那么在使用异常时候是很难保证A、D特性的。比如常见的持久化设备硬盘都是逻辑上按照512字节分扇区来组织管理的,数据库底层的数据页也是这个大小,假如当一个事务需要更新很多数据页的时候,这个更新过程是无法保证多个扇区的写满足原子性特性的,也就是说在写多个扇区的过程中服务或者系统挂掉的话,数据库就会处于不一致的状态,所以实现上在修改数据的时候我们肯定不能直接操作数据库中的原始文件。
  针对上述问题,传统上使用rollback journal的机制可以解决,其原理是:当需要对数据文件进行修改的时候,先将原始数据的内容拷贝到一个独立的rollback journal file当中,然后直接对原始数据文件进行更新操作;当更新过程中出现crash或需要ROLLBACK的时候,rollback journal file中的原始数据将会被用来进行数据还原;而如果事务最终执行了COMMIT,那么更新操作成功之后rollback journal file中的备份将会被删除掉。
  除此之外,WAL是另外一种广泛使用的保证数据安全更新的机制。在这种机制下,当需要执行更新操作的时候,原始的数据被还是被存在于原始的数据库文件中,执行修改的只是原始数据在内存中对应的缓存页buffer pool,同时所有的更新操作将会被记录在一个独立于数据文件的日志文件中,写完日志该事务就可以返回了,而且日志通常是使用顺序追加的方式记录,所以日志落盘操作相比于数据落盘操作会快很多,事务执行响应速度会很快。但事务返回后,对应相应数据库原始文件还没有执行更新,这种更改看似是临时性的,不过数据库能够保证再次请求相同数据的时候,在满足事务隔离性要求的前提下能够返回满足条件的结果,而且只要日志被持久化了,即使后面发生崩溃也能够使用日志进行数据重建。
  当然我们没有办法无限制的执行上述操作:首先内存资源是宝贵稀缺的,我们没有办法将所有修改后的脏页都驻留在内存当中;其次日志文件会随着更改操作变得越来越长,其占用磁盘空间不可能无限制的增加,而且通常情况下重做日志的尺寸都被设计的比较小;最后当系统重启的时候需要使用日志进行数据恢复,太长的日志将会使恢复过程变的十分漫长,系统启动时间将会很慢。针对上述问题,WAL的checkpoint机制是必不可少的,通过某些设定的触发条件或者周期性的选择内存中的脏页,将他们写入数据库文件后,该页面就是Clean的从而可以被回收使用,而对应的日志条目也可以被安全删除了。
  上面是WAL工作的大致原理,而我们最常见的MySQL存储引擎InnoDB也是使用WAL机制来保证数据安全的,其使用redo log来记录对数据库的修改操作。在InnoDB中,WAL实现的步骤可以总结描述为:(1).对数据的所有修改将会首先被作用在内存中的数据页拷贝,被修改的数据页不会立即同步到磁盘的数据文件中,被称为脏页驻留在内存中;(2).该操作会产生一个相关的redo log,首先是记录在局部的mtr(mini transcation)缓存中,mtr存在于一个局部的内存缓存中,会保证其中的操作要么全部提交给redo log,要么什么都不做,随后mtr会被传输到全局的内存redo log buffer中;(3).触发redo log buffer写入到redo log file当中,并执行flush刷新落盘操作;(4).在随后的某个时刻执行checkpoint的时候,脏页会被刷新到磁盘上。
  
  所以,上面问题的关键是redo log是如何组织的,以及checkpoint是如何触发的。
  
  在几乎所有的数据库中,都会有一个LSN(log sequence number)的概念,该变量会随着数据库操作用于记录日志的不断递增的8字节无符号长整形,其递增顺序也就表示了数据库操作的时间顺序,在数据库实现内部,以及后面的描述中会被大量引用,比如我们可以用LSN表示checkpoint的位置、脏页的版本信息。
  每一个数据页,在其首部都有一个字段预留记录LSN。对于磁盘上的数据页,其值表示该页最后被刷新时候LSN的大小,而对于内存中的脏页,该值标识该页最后一次被更新时候的LSN,而且会按照其值的升序组织在一个Flush列表当中,所以磁盘和内存页面的数据不一致,LSN也不一致。基于LSN,Page Clean线程就可以按照LSN较小的脏页刷新到磁盘上去,那么就能够保证checkpoint之前的所有数据都已经被持久化到硬盘上去了,而在数据库重启的恢复阶段,也可以根据磁盘上数据页的LSN来判断当前的redo log是否需要对其执行重做操作了。
  默认情况下InnoDB数据库会在数据库目录下生成两个redo log file(ib_logfile0、ib_logfile1),默认大小是48MB,这两个文件进行循环使用,每个日志文件含有一个FILE_HDR,日志内容被分割为一个个512字节大小的LOG_BLOCK组成,这和磁盘扇区的大小是一致的,可以保证单个日志块的原子写入。这里比较重要的是在ib_logfile0的头部,第2和第4个LOG_BLOCK被定义为LOG_CHECKPOINT_1和LOG_CHECKPOINT_2 ,是用来记录当前checkpoint指定点的位置的,这两个日志块用于启动恢复的时候至关重要。
  在数据库启动的时候,无论是正常重启还是崩溃后重启,都会进入一个恢复模式。在数据库启动的时候,会首先根据DoubleWrite buffer来恢复写入不完全的页面,然后读取磁盘中保存的LOG_CHECKPOINT的值以及redo log记录,按照LSN建立redo log entries条目;从LOG_CHECKPOINT点开始从后依次执行redo log指令,重建数据脏页;对于Undo中未完成的事务执行rollback回滚,执行完成后数据库将会处于一致性的状态。
  
  在InnoDB中checkpoint分为sharp和fuzzy两种实现,sharp是将所有的脏页都刷新到磁盘上,默认情况下只有在数据库关闭的时候会被使用,所以如果脏页比较多的时候数据库关闭可能会比较慢;而fuzzy方式是在数据库常态运行时候执行的checkpoint机制,它会根据某些触发条件选择将部分的脏页刷新到磁盘上去,因为考虑的因素较多所以实现起来较为的复杂。其实在InnoDB内部,buffer pool页面会被free、lru、flush这些链表所管理维护着,lru按照页面最近被访问时间进行排序,而flush则是按照脏页的min_LSN进行排序的,InnoDB就可以根据这些信息从不同的角度判断系统的运行状态,必要时方便的选择对应的脏页执行checkpoint机制。在数据库运行时候,触发fuzzy checkpoint执行的情况有:主线程以每1秒或10秒的周期按照一定的比例刷新;Page Cleaner线程会为了预留一部分的空闲页面而执行刷新操作;当redo log file内容太多导致不可用的时候,为保证重做日志可循环使用而执行刷新操作;脏页太多(比如占比达到75%以上)也会触发刷新操作。
  其实InnoDB在(内存、日志文件)允许的情况下会尽量延迟将页面刷新到磁盘上,因为我们会期望后续对该页面还会进行更多的修改操作,延迟将该页面刷新到磁盘就可以达到合并写入的效果,这也是实现数据库性能优化的方式,所以checkpoint的实现是很复杂的,也有很多调优的空间去发挥。 
  把脏页刷入磁盘这个过程也必须保证是可靠的,在InnoDB中是通过Double Write技术来实现的,此处就不再赘述了。

本文完!

参考