本节介绍一下数据库中的WAL,在此做个记录。

1. 什么是WAL?

WAL(Write Ahead Log)预写日志,是数据库系统中常见的一种手段,用于保证数据操作的原子性和持久性。

在计算机科学中,[预写式日志] (Write-ahead logging,缩写WAL)是关系数据库系统中用于提供原子性和持久性(ACID属性中的两个)的一系列技术。在使用WAL的系统中,所有的修改在提交之前都要先陷入log文件中。

数据库中ACID是指:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)

log文件中通常包括redo 和undo信息。这样做的目的可以通过一个例子来说明。假设一个程序在执行某些操作的过程中机器掉电了,在重新启动时,程序可能需要知道当时执行的操作是成功了还是部分成功或者是失败了。如果使用了WAL,程序就可以检查log文件,并对突然掉电时计划执行的操作内容跟实际上执行的操作内容进行比较。在这个比较的基础上,程序就可以决定是撤销已做的操作还是继续完成已做的操作,或者是保持原样。

WAL允许用in-place方式更新数据库。另一种用来实现原子更新的方法是shadow paging,它并不是in-place方式。用in-place方式做更新的主要优点是减少索引和块列表的修改。ARIES是WAL系列技术常用的算法。在文件系统中,WAL通常称为journaling。PostgreSQL也是用WAL来提供point-in-time恢复和数据库复制特性。

2. 备份

我们想一想,如果想保证对一个数据的操作可以恢复,可以怎么做?你不用去想数据库是怎么实现的,也不用想的太高深,其实这是一个很简单的问题,我们也常常在处理这种问题。最简单的方法其实就是备份一份数据:当我需要对一条数据做更新操作前,先将这条数据备份在一个地方,然后再去更新,如果更新失败,可以从备份数据中回写回来。这样就可以保证事务的回滚,就可以保证数据操作的原子性了。其实SQLite引入WAL之前就是通过这种方式来实现原子事务,称之为rollback journal。rollback journal机制的原理是:在修改数据库文件中的数据之前,先将修改所在分页中的数据备份在另外一个地方,然后才将修改写入到数据库文件中;如果事务失败,则将备份数据拷贝回来,撤销修改;如果事务成功,则删除备份数据,提交修改。

3. WAL

再继续上面的问题,如何做到数据的可恢复(原子性)和提交成功的数据被持久化到磁盘(持久性)?另一种机制就是WAL。WAL机制的原理也很简单:修改并不直接写入到数据库文件中,而是写入到另一个称为WAL的文件中;如果事务失败,WAL中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库文件中,提交修改。

3.1 WAL的优点

  • 读和写可以完全地并发执行,不会相互阻塞(但是写之间仍然不能并发);

  • WAL在大多数情况下,拥有更好的性能(因为无需每次写入时都要写两个文件);

  • 磁盘IO行为更容易被预测;

  • 使用更少的fsync()操作,减少系统脆弱的问题;

3.2 提升性能

我们都知道,数据库的最大性能挑战就是磁盘的读写,许多先辈在提供数据存储性能上绞尽脑汁,提出和实验了一套又一套方法。其实所有的方案最终总结出来就三种:随机读写改顺序读写、缓冲单条读写改批量读写、单线程读写改并发读写。WAL其实也是这两种思路的一种实现,一方面WAL中记录事务的更新内容,通过WAL将随机的脏页写入变成顺序的日志刷盘;另一方面,WAL通过buffer的方式改单条磁盘刷入为缓冲批量刷盘,再者从WAL数据到最终数据的同步过程中可以采用并发同步的方式。这样极大提升数据库写入性能,因此,WAL的写入能力决定了数据库整体性能的上限,尤其是在高并发时。

3.3 checkpoint

上面讲到,使用WAL的数据库系统不会再每新增一条WAL日志就将其刷入数据库文件中,一般累积到一定的量后批量写入,通常使用为单位,这是磁盘的写入单位。同步WAL文件和数据库文件的行为被称为checkpoint(检查点),一般在WAL文件累积到一定页数修改的时候。当然,有些系统也可以手动执行checkpoint。执行checkpoint之后,WAL文件可以被清空,这样可以保证WAL文件不会因为太大而性能下降。

有些数据库系统读取请求也可以使用WAL,通过读取WAL最新日志就可以获取到数据的最新状态。

3.4 具体实现

常见的数据库一般都会用到WAL机制,只是不同的系统说法和实现可能有所差异。MySQL、SQLite、PostgreSQL、ETCD、Hbase、Zookeeper、Elasticsearch等等都有自己的实现。

1) MySQL

MySQL的WAL,大家可能都比较熟悉。MySQL通过redo、undo日志实现WAL。redo log称为重做日志,每当有操作时,在数据变更之前将操作写入redo log,这样当发生掉电之类的情况时系统可以在重启后继续操作。undo log称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之前的状态。MySQL中用redo log来在系统crash重启之类的情况时修复数据(事务的持久性),而undo log来保证事务的原子性。

2)Zookeeper

和大多数分布式系统一样,Zookeeper也有WAL(Write-Ahead-Log),对于每一个更新操作,Zookeeper都会先写WAL,然后再对内存中的数据做更新,然后向Client通知更新结果。另外,Zookeeper还会定期将内存中的目录树进行snapshot,落地到磁盘上。这么做的主要目的: 一当然是持久化;二是加快重启之后的恢复速度,如果全部通过Replay WAL的形式恢复的话,会比较慢。

3)Elasticsearch

如果没有用fsync()把数据从文件系统缓存刷(flush)到硬盘,elasticsearch不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证可靠性,需要确保数据变化被持久化到磁盘。

在动态更新索引时,elasticsearch一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点。Elasticsearch在启动或者重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。

即使通过每秒刷新(refresh)实现了近实时搜索,elasticsearch依然需要经常进行完整提交来确保能从失败中恢复。但在两次提交之间发生变化的文档怎么边?Elasticsearch增加了一个translog,或者叫事务日志,在每一次对Elasticsearch进行操作时均进行了日志记录。

4)etcd

用过etcd的同学可能会发现,etcd的数据目录下有两个子目录wal和snap。他们的作用就是实现WAL机制用的。

  • wal: 存放预写式日志,最大的作用是记录了整个数据变化的全部历程。在etcd中,所有数据的修改在提交前,都要先写入到WAL中;

  • snap: 存放快照数据,etcd防止WAL文件过多而设置的快照,存储etcd数据状态;

WAL机制使得etcd具备了以下两个功能:

  • 故障快速恢复:当你的数据遭到破坏时,就可以通过执行所有WAL中记录的修改操作,快速从最原始的数据恢复到数据损坏前的状态;

  • 数据回滚(undo)/重做(redo): 因为所有的修改操作都被记录在WAL中,需要回滚或重做,只需要方向或者正向执行日志中的操作即可。

5) Hbase

Hbase实现WAL的方法是HLog。Hbase的RegionServer会将数据保存在内存中(MemStore),直到满足一定条件,将其flush到磁盘上。这样可以避免创建很多小文件。内存存储是不稳定的,HBase也是实用WAL来解决这个问题:每次更新操作都会写日志,并且写日志和更新操作在一个事务中。



[参看]:

  1. 什么是WAL?

  2. 预写式日志