总所周知,Redis 绝大部分事业场景是当做缓存使用,但是有个问题是绝对不能忽略的:一旦服务器宕机,内存中的数据将全部丢失。虽然从后端数据库可以恢复这些数据,但是这种方式存在两个问题:一是需要频繁访问数据库,给数据库带来巨大压力;二是这些数据是从慢速数据库中读取出来的,性能肯定比不上从 Redis 中读取,导致使用这些数据的应用程序响应变慢。所以,对于 Redis 来说,实现数据持久化,避免从后端数据库中进行恢复,是至关重要的。
目前,Redis 的持久化主要有两大机制,及 AOF 日志和 RDB 快照。
我们熟悉的是数据库的写前日志(Write Ahead Log,WAL),也就是在实际写数据前,先把修改的数据记录到日志文件中,以便故障时进行恢复。而 AOF 正好相反,它是写后日志,也就是先执行命令,把数据写入内存,然后才记录日志。
为什么 AOF 要先执行命令再写日志呢?
我们以 Redis 收到 “set testkey testvalue” 命令后记录的日志为例,先看看 AOF 文件内容。
*3
表示当前命令有三个部分,每个部分都是由 $数字
开头,后面紧跟着具体的命令、键、值。$数字
中的数字
表示这部分中的命令、键、值一共有多少字节。$3set
,表示这部分有3个字节,也就是 set
命令。不过,AOF 也有两个潜在风险:
仔细分析,你会发现,这两个风险都是和 AOF 写回磁盘的时机相关。
AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendsync 的三个可选值。
针对避免主线程阻塞和减少数据丢失问题,这三种写回策略都无法做到两全其美,我们分析下:
配置项 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 可靠性高,数据基本不缺失 | 每个命令都要落盘,性能影响较大 |
Everysec | 每秒写回 | 性能适中 | 宕机时丢失1秒内的数据 |
No | 操作系统控制的写回 | 性能好 | 宕机时丢失数据较多 |
但是,并不是按照性能需求和数据可靠性需求选定了 AOF 的写回策略,就“高枕无忧”了。随着接收的命令越来越多,AOF 文件会越来越大。我们要小心 AOF 文件过大带来的性能问题:一是,文件系统本身对文件大小有限制;二是,如果文件太大,之后再往里追加命令记录的话,效率也变低;三是,如果宕机,AOF 中记录的命令要一个个的被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程会非常慢,这就会影响到 Redis 的正常使用。
AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件。读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。比如说,当读取了键值对“testkey”: “testvalue”之后,重写机制会记录 set testkey testvalue 这条命令。这样,当需要恢复时,可以重新执行该命令,实现“testkey”: “testvalue”的写入。
为什么重写机制可以把日志文件变小?实际上,重写机制具有“多变一”功能。也就是将就日志文件中的多条命令,在重写后的新日志文件中变成一条命令。
AOF 文件是以追加方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。但是,在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。
不过,虽然 AOF 重写后,日志文件会缩小,但是,要把整个数据库最新数据的操作日志都写回磁盘,仍是一个非常耗时的过程。这时,我们要关注重写会不会阻塞主线程。
和 AOF 日志由主线程写回不同,重写过程是由后台子进程 bgrewriteaof
来完成的,这也是为了避免阻塞主线程。
重写的过程可总结为“一处拷贝,两处日志”。
“一个拷贝”是指每次重写时,主线程 fork 出后台的 bgrewriteaof
子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof
子进程,这里包含了最新数据。然后,bgrewriteaof
子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
“两处日志”是什么?
AOF 方法进行故障恢复的时候,需要逐一把操作日志都执行一遍。如果操作日志非常多,Redis 就会恢复的很缓慢,影响到正常功能。那么有没有既可以保证可靠,还能再宕机时实现快速恢复的其他方法呢?
当然有了,这就是 Redis 的另一种持久化方法: 内存快照。所谓内存快照,就是指内存中的数据在某一个时刻的状态记录。
对于 Redis 来说,它实现类似照片记录效果的方式,即把某一时刻的状态以文件的形式写到磁盘上。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件称为 RDB 文件。
与 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以在做数据恢复时直接把 RDB 文件读入内存,很快地完成恢复。
听起来这好像不错,但内存快照也并不是最优选项,我们还要考虑两个关键问题:
Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中。给内存的权力数据做快照,把他们全部写入磁盘也会花费很多时间。而且,全量数据越多,RDB 文件越大,往磁盘上写数据的时间开销越大。
对 Redis 来说,单线程模型就决定了,我们要尽量避免会阻塞主线程的操作,所以针对任何操作,我们都会提出一个灵魂拷问:“它会阻塞主线程吗?”。同理,RDB 文件的的生成是否会阻塞主线程,会关系到是否会降低 Redis 的性能。
Redis 提供了两个命令来生产 RDB 文件,分别是 save 和 bgsave。
接下里,要关注的问题是在对内存数据做快照时,这些数据还能“动”吗?也就是说这些数据还能被修改吗?
Redis 借助操作系统提供的写时复制技术(Copy-On-Write,COW),在执行快照的同时,正常处理些操作。
简单来说, bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。
此时,如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么主线程和 bgsave 子进程相互不影响。但是,若主线程要修改一块数据(例如图中的 C),那么,这块数据就会被赋值一份,生成该数据的副本(键值对 C’)。然后主线程在这个数据副本上进行修改。同时 bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。
这样既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。
到这里,我们就解决了对“哪些数据做快照”以及“做快照时数据能否修改”这两大问题:Redis 会使用 bgsave 对当前内存中的所有数据做快照,这个操作是子进程完成的,这就允许主线程同时可以修改数据。
再来看另一个问题:多久做一次快照?我们在拍照的时候,还有项技术叫“连拍”,可以记录人或物连续多个瞬间的状态。那么,快照也适合“连拍”吗?
对于快照来说,所谓“连拍”就是指连续地做快照。这样一来,快照的间隔时间变得很短,及时某一刻发生宕机了,因为上一时刻快照刚刚执行完,丢失的数据也不会太多。但是这其中的快照间隔时间就很关键了。
如下所示,我们在 T0 时刻做了一次快照,然后又在 T0 + t 时刻做了一次快照,在这期间,数据库 5 和 9 被修改了。 如果在 t 这段时间内,机器宕机了,那么,只能按照 T0 时刻的快照进行恢复。此时,数据 5 和 9 的修改值就无法恢复了。
所以,要想尽可能恢复数据, t 值就要尽可能小,t 越小,就越像“连拍”。那么 t 值可以小到什么程度呢,是不是每秒可以做一次快照?
这种想法其实是错误的。虽然 bgsave 执行时不阻塞主线程,但是,如果频繁的执行全量快照,也会带来两方面的开销。
有没有什么好的办法?
我们可以做增量快照,即做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样就可以避免每次全量快照的开销。
在昨晚全量快照后, T1 和 T2 时刻如果再做快照,我们只需将被修改的数据接入快照文件就行。但是,这么做的前提是要记住哪些数据被修改了。“记住”这个功能,需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。如下图所示:
想象一下,我们对每个键值对的修改都做记录,那么若有1万个被修改的键值对,我们就需要有1万条额外的记录。
并且有的时候键值对非常小,比如只有 32 字节,而记录它被修改的元数据信息,可能需要 8 字节。这样的话,为了“记住”修改这个功能,引入的额外空间开销比较大。这对于内存资源宝贵的 redis 来说,得不偿失。
讲到这,我们发现,虽然根 AOF 相比,快照的恢复速度快,但是快照的频率不好把我,如果太低,两次快照间一旦宕机,就可能会丢失较多的数据。如果频率太高,又产生额外开销。有没有什么办法既能利用 RDB 的快速恢复,又能已较小的开销做到尽量少丢数据呢?
Redis 4.0 中提出了混合使用 AOF 日志和内存快照的方法。即,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有操作。
这样一来,快照不需要很频繁的执行,避免了 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,不需要记录所有的操作,就不会出现文件过大的问题,也可以避开重写开销。
如下图所示, T1 和 T1 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了。
这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势,颇有点“鱼和熊掌可以兼得”的感觉。
最后,关于 AOF 和 RDB 的选择问题,我想再给你提三点建议: