本文是对 antirez 博客中 Redis persistence demystified 的翻译和总结。主要从Redis的持久化机制,提供何种程度的可靠性以及与其他数据库的比较三个方面进行讨论。

0 持久化的基础:简化的写入操作步骤

在讨论持久化时,我们的最终目的是将数据保存到物理硬盘中。简化的写入操作经历如下步骤:

1. 客户端向数据库服务端发送写入或者更新数据的请求,此时数据位于客户端内存中

2. 服务端接收到写命令,此时数据位于服务端数据库应用内存中(站在服务端服务器视角,数据位于应用(数据库)内存中,即用户态内存)

3. 数据库调用系统函数向硬盘写数据,此时数据位于内核缓冲区中(kernel’s buffer)

4. 操作系统将数据从写缓冲区转移到硬盘控制器,此时数据位于硬盘缓冲区(disk cache)

5. 硬盘控制器将数据实际写入物理介质中

通常步骤2的实现因数据库的实现不同而不同,但相同的是,最终都会触发调用系统写函数进行数据写入。步骤3也因不同系统的实现而不同,但在讨论当前问题时,可将其视为单独的一步而不用关心其细节。

1 持久性(durability):什么时候才能认为数据是安全的

考虑数据安全时,我们通常考虑的是当系统发生异常时,数据是否得到了正确的保存,从而在系统恢复时能够得到恢复。考虑不同的系统异常情况:

1. 应用级的异常。这种异常是由于诸如数据库服务被异常关闭,如kill -9等。这时服务器仍旧正常运转。那么这时当上述讨论的第3步完成时,可以认为数据是安全的。因为,即使此后数据库应用被异常关闭,数据仍旧会被写入物理介质中,此后的操作已可以由操作系统独立完成。

2. 服务器级异常,例如掉电。这种情况下,只有当第5步完成时,才可以认为数据是真正安全的。

可见,对于持久化来说,关键的是上述3、4、5三个步骤的执行情况,从另一个角度考虑三个步骤的动作,他们分别表示的含义分别是:

  • 数据从用户态内存向系统态内存的转移频率(write操作的调用频率)
  • 系统多久将数据从系统态内存转移到硬盘控制器中
  • 硬盘控制器多久将数据写入物理介质中

在第三步中,数据库可以控制通过调用系统函数write频率,但是调用该函数消耗的时间却无法得到控制。因为write函数的成功返回,依赖于写入数据量的大小及硬盘的实际写入能力,当硬盘无法实际处理写入请求时,数据会被缓存到写入缓存中,如果进一步缓存被写满,此时write调用将会阻塞,直至可以完成全部数据的写入时,write调用才会成功返回。

在第四步中,数据的转移由硬盘控制器控制,通常该写入频率不会太高,因为大量碎片数据的写入相比一次写入大数据量更慢。在Linux的默认实现中,写入间隔是30s.这意味着,当这一步失败时,最多可能有30s内的数据无法持久化到硬盘中。而在实际中,可以调用系统函数fsync强制执行该步骤。同样地,该系统调用在无法成功完成时,也会阻塞用户进程,同时也会阻塞对当前文件执行写入操作的其他进程。

第五步的实现应用层面无法控制,因此不在讨论范围内。

因此,我们关于数据库数据持久化的讨论归结为如下两个问题:

应用级,由通过write系统调用保证。

系统级,由通过fsync系统调用保证。

2 可用性:持久化数据的可用性

以上,对数据持久化从写入角度进行了讨论。此外,在讨论数据库的持久化时,还需要讨论持久化数据的可用性,即当发生异常时,持久化数据是否可以用来恢复现场。这里有三种可能:a. 数据结构被损坏,不能恢复; b. 损坏的数据可以通过一定的工具得到修复;c. 数据可用,直接加载即可。

从现有的数据库实现角度,提供如下几类数据可用性保证:

  • 当某节点发生异常时,数据可以通过副本(Replica)恢复,因而持久化数据是否可用无关紧要。
  • 数据的持久化通过类似日志的方式实现(比如mysql的bin-log)
  • 数据的持久化通过追加模式的文件实现,这种情况下,如果对文件的写入是保证命令级原子性的,则也可以不用考虑数据损坏的情况。除非是在第5步写入时发生了系统异常。

3 Redis的持久化实现

将以上关于持久化的讨论归类为两个问题,即:

1. 持久性,即关于数据及时保存到磁盘的问题

2. 可用性,即关于持久化数据在数据库异常恢复时是否可用的问题

下面讨论Redis两种持久化方式在这两个方面的表现。

  • RDB

Redis的RDB持久化方式是内存快照方式,通过对内存即时进行快照持久化实现。

持久性方面:用户可以定义当满足某种条件时即进行快照持久化,通常,考虑到性能问题,这个时间间隔会设置为分钟级。因此RDB方式并不能提供很好的持久性。

可用性方面:在不考虑首次RDB生成的情况下,首次以后的RDB文件的生成Redis的实现采用了双文件的机制,即在进行RDB时,先生成新的临时文件,当新临时文件生成完成时,通过原子性的系统函数rename进行重命名。因此,在大部分的情况下(除了首次RDB),Redis都保证RDB文件是可用的。

可见,RDB在持久性方面不够但持久化数据的可用性较好。

尽管RDB并不能提供很好的持久性保证,但是作者仍旧建议在打开AOF的同时打开RDB,并利用其进行定时数据的备份,以便在出现故障或者巨大的程序缺陷时将其做为数据恢复的基础。此外,作者还提到,RDB持久化的方式磁盘的IO在数据量确定的情况下是确定的,而不管此时数据库处理的请求情况。

  • AOF

AOF通过追加日志命令的方式记录对数据产生影响的写入操作命令。

AOF方式以追加的方式进行持久化,因此提供了较好的持久化数据可用性。但是,AOF面临的问题时AOF文件的不断增长。Redis对此的解决是通过触发AOF重写来对减轻这个问题。AOF的重写类似于RDB,以内存数据为源,在AOF重写期间,先生成新的临时文件,重写期间产生的记录记录在老的AOF文件中,并在新临时文件生成后进行追加,当全部完成后进行文件替换。

持久性方面,与AOF的磁盘同步配置相关,Redis通过调用系统函数fsync实现数据的最终持久化。配置项为appendnfsync,选项有always, everysec, no,分别表示显式调用fsync函数的时机。对于常用的everysec选项,Redis至少保证2s的数据持久时间间隔,因此,最多有2s的数据是不可用的。对于always,则是命令级别;而对于no,依赖于操作系统的不同实现,Linux的默认实现中磁盘数据持久化的时间间隔为30s。

4 与其他数据库的比较

以上,首先对数据持久化的基础进行了简化抽象;然后将数据持久化问题归结为持久性及可用性两个方面,并分别对两个性质进行了解释;最后,对Redis的RDB及AOF两种持久化方式分别从持久性和可用性方面进行了考察。antirez还对PostrgreSQL与Mysql(Innodb)的持久化分别与Redis进行了比较:

假设Redis采用了AOF持久化配置,并且采用常用的appendnfsync everysec磁盘同步策略,则:

Redis提供最多不超过2s的持久性保证;

普通情况下,这个时间是1s

PostrgreSQL的持久化方面,有三个参数与Redis的持久化类似:

fsync: on/off, 打开时通过调用系统函数fsync()保证更新操作同步至磁盘。

synchronous_commit: on/local/off, 指定返回客户端’success’时,是否需要等待WAL(Write-ahead Logging:预写式日志,提供atomicity及durability保证)同步至磁盘。通常采用on 配置,如果采用off,则不保证返回的成功状态意味着写磁盘成功,通常这两步间将存在时间延迟。

wal_writer_delay: 预写式日志磁盘同步的预设延迟时间,默认为200ms,官方表示实际同步时间需要乘以3。

可见,在默认配置下,PostrgreSQL提供最坏不超过600ms的数据可用性保证。

Mysql(Innodb)的innodb_flush_log_at_trx_commit参数实现类似的控制,当设置为:

0,缓冲区将每秒写入日志文件一次并做磁盘同步,在事务提交时,不做任何动作

1,每次事务提交都将进行缓冲区到日志文件的写入,并做磁盘同步

2,事务提交时进行缓冲区到日志文件的写入,但不进行磁盘的同步

综上,antirez认为即使作为内存存储,Redis也提供了良好的持久性保证。

事实上,通过上述考察可以看出,通常作为缓存使用的Redis确实提供了良好的持久性能力(在实际应用中,很多开发者甚至都不开启持久化来获取Redis在速度方面的最大性能)。