关于Redis我的一部分工作是阅读博客,论坛以及twitter时间线(time line)。对于开发者来说,能够了解用户社区,非用户社区如果理解他正在开发的产品是非常重要的。据我所知,持久化特性是最易产生误解的Redis特性。
这篇博客中,我会尽力客观公正,不为Redis做宣传,不忽略可能让Redis出丑的诸多细节。我所期望的仅是说明Redis持久化机制,可靠性,以及和其他数据库系统相比较的优势。
操作系统与磁盘
首先需要考虑的是数据库的耐用性(durability)。为此,我们看看一个简单的写操作实现过程:
1. 客户端发送一个写入命令到数据库(数据在客户端内存中)
2. 数据库收到这个写入命令(数据在数据库内存中)
3. 数据库调用系统调用将数据写到磁盘上(数据在内核的buffer中)
4. 操作系统将写Buffer转交到磁盘控制器(数据在磁盘cache中)
5. 磁盘控制器实际将数据写入物理媒质(磁盘或者闪存)
注:上面仅是一个简化过程,实际上存在更多级的缓存。
实际数据库实现中,步骤2常常是一个复杂的缓存系统,有时写入是由不同线程或者进程处理的。然后数据库最终会将数据写入磁盘,以我们的角度看,这一点是有意义的地方。那就是,数据将从内存传送给内核(步骤3) - 或许理解为从用户空间到内核空间更合适一点。
另一个容易忽略细节的步骤3。更复杂的是大多数内核实现了不同层次的缓存,即文件系统级缓存(如linux中的页缓存)和一个更小的包含等待提交给磁盘的数据的buffer缓存。使用特定API可以跳过这两个缓存(如Linux的open系统调用中的O_DIRECT和O_SYNC标志),但以我们的角度看,我们可以将之视为一个不透明的缓存层(我们不知道细节)。当数据库实现了缓存,那么去使能page缓存以避免数据库和内核同时进行缓存就足够了。buffer缓存通常要打开,否则每次文件写入都要提交给磁盘,对于大多数应用来说这太慢了。
数据库通常要做的是调用系统调用,系统调用提交buffer缓存到磁盘。
什么时候我们的写操作是安全的
如果我们仅仅考虑数据库软件错误(进程被杀死或者崩溃)而不触及内核,那么写操作在成功执行完步骤3就可以认为是安全的。即write系统调用(或者其他系统调用)成功返回后。执行完这一步,即使数据库软件崩溃,内核也会负责将数据写入到磁盘控制器。
如果我们继续考虑更严重的事件,如断电,那么只有在执行完步骤5才是安全的,即当数据已经实际写入到物理设备。
我们可以认为最重要的步骤就是步骤3,4,5. 即:
数据库软件从用户空间传输数据到内核空间的频率
内核将buffer中数据写到磁盘控制器的频率
最后是磁盘控制器将数据写入物理设备的频率
注:
当我们讨论磁盘控制器时,我们实际是指磁盘控制器或者磁盘自己的缓存。在耐用性比较重要的场景,系统管理员通常去使能这一层的缓存。
对大多数系统来说,磁盘控制器默认只执行write through(只缓存读操作)。只有在有电池或者超级电容器进行断电保护的时候,激活write back模式(缓存写操作)才是安全的。
POSIX API
从数据库开发者的角度来看,我们感兴趣的是数据实际写入物理设备的过程,但最关注的是API在写入过程中所能提供的控制。
我们从步骤3开始,我们能够使用write系统调用将数据传递到内核buffer,所以我们的角度看,我们使用POSIX API尽可能的控制了数据的写入。然而,我们不能控制这个系统调用在成功返回之前所耗费的时间。内核buffer大小有限如果。如果磁盘不能应对应用程序的写入带宽要求,内核写buffer将会被耗尽,内核将会阻塞写入。当磁盘可以接收更多数据时,write系统调用才会返回。最终要实现的目标是将数据写入物理设备。
步骤4:在这一步,内核将数据传递到磁盘控制器。默认情况下,内核会尽量减少传递数据的频率,因为传递大数据块会更高效。例如,Linux默认会在write调用后30秒钟将数据提交到磁盘控制器。这意味着,如果在此期间出现失败,所有最近30秒写入的数据可能丢失。
POSIX API提供了一组系统调用强制内核将buffer中数据写入到磁盘:最有名的可能就是fsync系统调用(也可参考msync和fdatasync)。fsync为数据库系统提供了一种强制内核将数据写入磁盘的方法,但是你能想象的到,这代价不菲。只要内核buffer中有数据,每次调用fsync发起一次些操作。fsync将会阻塞调用进程,直至所有写入操作完成,在Linux系统中,如果耗时过长,其他对同一文件进行写入的线程也会被阻塞。
我们不能控制的
到目前为止,我们可以控制步骤3和4,那么步骤5呢?正式的回应是,我们不能使用POSIX API控制这一步。或许,某些内核实现将尽力告知驱动器将数据提交到物理设备,但控制器也可能为了优化写入小了对写入操作重新排序,不会立即将数据写入磁盘而是再等待几毫秒。这时我们无能为力。
在文章后面部分,我们将我们的应用场景简化为两个数据安全级别
使用write系统调用的写入的数据对进程失败是安全的
使用fsync系统调用的是对系统失败(如断电)安全的。实际上,我们知道犹豫控制器缓存我们不能保证这一点,但由于所有数据库系统都有这种问题,所以我们不考虑。此外系统管理员也常常使用特定工具以控制物理设备的行为。
注:不是所有数据库都使用POSIX API。一些私有数据库使用内核模块对硬件进行更多直接的控制。但是问题的主要特征保持不变。你可以使用用户空间buffer,内核buffer,并最终会将数据写入到磁盘以保证数据安全(是一个慢操作)。一个典型的使用内核模块的数据库是Oracle。
数据损坏:
在前一节,我们分析了系统高层(应用程序和内核)将数据写入到磁盘保障数据安全的问题。然而这仅仅是数据耐用性的一面。另外一点是:数据库(包括系统)失败后,数据库是否可读,或者其内部结构是否损坏导致数据不能正确读取,或者需要恢复工具重建数据。
例如,许多SQL和NoSQL数据库实现了某种形式的树数据结构存储数据和索引。这一数据结构将会在写操作时被修改。如果系统在写操作过程中停止工作,这一树数据结构是否仍旧正确。
一般来说,对数据损坏有三种层次的安全性:
数据库写入数据时不关心失败,让用户使用数据备份进行数据恢复,或者提供工具尽力重建数据。
数据库系统使用log操作以便在失败场景时能够重新恢复数据
数据库不修改已经写入的数据,而是仅以追加模式工作,所以不会发生数据损坏。
现在,我们了解所有评估数据库系统持久化层可靠性的因素。我们可以看看Redis的表现如何。Redis提供了两种不同的持久化选择,下面依次说明。
快照
Redis快照是最简单的持久化模式。它在某些条件满足时在某个时间点生成快照,如前一次快照2分钟后且至少有100个新的写入操作。这些条件可以通过用户配置文件实现,可以在不重启服务器的条件下修改。快照是一个单个.rdb文件,包含整个数据集。
快照的耐用性只能限定在用户指定的存储点。如果数据集每15分钟后存一次,那么在Redis实例崩溃或者更严重事件发生时,那么15分钟的写入将会丢失。从Redis事务的角度看,快照能够保证MULTI/EXEC事务可以完全写入快照,或者不写入。
RDB文件不会损坏,因为它是由一个子进程以追加的方式生成。新的rdb快照是一个临时文件,生成成功后,使用原子系统调用rename将其修改为最终文件。
Redis快照不能提供好的耐用性保证,因为在两次快照中间的可能多达几分钟的数据丢失是不可接受的,它只对不关注丢失最新数据的应用和场景适用。
然而,即使是使用另外一种更高级持久化模式-AOF时,仍旧建议打开快照功能,因为它提供的一个包含完整数据集的文件在进行数据备份时,将数据发送给其他数据中心进行灾难恢复,或者在重大软件错误严重损坏数据时进行数据回滚益处多多。
需要注意的是,Redis快照Redis使用快照实现主从同步。
仅追加文件(AOF)
仅追加文件或者简称为AOF,是主要的Redis持久化选项。它的工作方式很简单:内存中每一次修改数据集的写操作都会被写入日志。日志的格式和客户端同Redis之间的通信格式相同。因此AOF能够使用netcat经管道传递给另外一个Redis实例,或者说需要时易于解析。Redis重启时,通过重放所有操作重建数据集。
为了说明AOF如何实际工作我们做一个简单的严重,启动一个新的Redis 2.6实例并激活AOF模式:
./redis-server --appendonly yes
现在发送一个写命令给这个Redis实例。
redis 127.0.0.1:6379> set key1 Hello
OK
redis 127.0.0.1:6379> append key1 " World!"
(integer) 12
redis 127.0.0.1:6379> del key1
(integer) 1
redis 127.0.0.1:6379> del non_existing_key
(integer) 0
前三个操作实际修改了数据集,第四个没有修改,它只是试图删除一个不存在的key。下面是AOF文件的内容
$ cat appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
set
$4
key1
$5
Hello
*3
$6
append
$4
key1
$7
World!
*2
$3
del
$4
key1
可以看到,最后一个DEL不存在,因为它没有对数据集进行任何修改。
简而言之,只有实际修改数据集的命令才会记入AOF文件。
Redis AOF仅仅时做追加操作,不会有数据损坏。但是问题是,AOF文件会不断增加,及时数据集已经被删除为空,当文件很大时怎么办呢?