You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

121 lines
13 KiB
Markdown

2 years ago
# 05 | 内存快照宕机后Redis如何实现快速恢复
你好,我是蒋德钧。
上节课我们学习了Redis避免数据丢失的AOF方法。这个方法的好处是每次执行只需要记录操作命令需要持久化的数据量不大。一般而言只要你采用的不是always的持久化策略就不会对性能造成太大影响。
但是也正因为记录的是操作命令而不是实际的数据所以用AOF方法进行故障恢复的时候需要逐一把操作日志都执行一遍。如果操作日志非常多Redis就会恢复得很缓慢影响到正常使用。这当然不是理想的结果。那么还有没有既可以保证可靠性还能在宕机时实现快速恢复的其他方法呢
当然有了,这就是我们今天要一起学习的另一种持久化方法:**内存快照**。所谓内存快照,就是指内存中的数据在某一个时刻的状态记录。这就类似于照片,当你给朋友拍照时,一张照片就能把朋友一瞬间的形象完全记下来。
对Redis来说它实现类似照片记录效果的方式就是把某一时刻的状态以文件的形式写到磁盘上也就是快照。这样一来即使宕机快照文件也不会丢失数据的可靠性也就得到了保证。这个快照文件就称为RDB文件其中RDB就是Redis DataBase的缩写。
和AOF相比RDB记录的是某一时刻的数据并不是操作所以在做数据恢复时我们可以直接把RDB文件读入内存很快地完成恢复。听起来好像很不错但内存快照也并不是最优选项。为什么这么说呢
我们还要考虑两个关键问题:
* 对哪些数据做快照?这关系到快照的执行效率问题;
* 做快照时数据还能被增删改吗这关系到Redis是否被阻塞能否同时正常处理请求。
这么说可能你还不太好理解,我还是拿拍照片来举例子。我们在拍照时,通常要关注两个问题:
* 如何取景?也就是说,我们打算把哪些人、哪些物拍到照片中;
* 在按快门前,要记着提醒朋友不要乱动,否则拍出来的照片就模糊了。
你看,这两个问题是不是非常重要呢?那么,接下来,我们就来具体地聊一聊。先说“取景”问题,也就是我们对哪些数据做快照。
## 给哪些内存数据做快照?
Redis的数据都在内存中为了提供所有数据的可靠性保证它执行的是**全量快照**也就是说把内存中的所有数据都记录到磁盘中这就类似于给100个人拍合影把每一个人都拍进照片里。这样做的好处是一次性记录了所有数据一个都不少。
当你给一个人拍照时只用协调一个人就够了但是拍100人的大合影却需要协调100个人的位置、状态等等这当然会更费时费力。同样给内存的全量数据做快照把它们全部写入磁盘也会花费很多时间。而且全量数据越多RDB文件就越大往磁盘上写数据的时间开销就越大。
对于Redis而言它的单线程模型就决定了我们要尽量避免所有会阻塞主线程的操作所以针对任何操作我们都会提一个灵魂之问“它会阻塞主线程吗?”RDB文件的生成是否会阻塞主线程这就关系到是否会降低Redis的性能。
Redis提供了两个命令来生成RDB文件分别是save和bgsave。
* save在主线程中执行会导致阻塞
* bgsave创建一个子进程专门用于写入RDB文件避免了主线程的阻塞这也是Redis RDB文件生成的默认配置。
好了这个时候我们就可以通过bgsave命令来执行全量快照这既提供了数据的可靠性保证也避免了对Redis的性能影响。
接下来,我们要关注的问题就是,在对内存数据做快照时,这些数据还能“动”吗? 也就是说,这些数据还能被修改吗? 这个问题非常重要这是因为如果数据能被修改那就意味着Redis还能正常处理写操作。否则所有写操作都得等到快照完了才能执行性能一下子就降低了。
## 快照时数据能修改吗?
在给别人拍照时,一旦对方动了,那么这张照片就拍糊了,我们就需要重拍,所以我们当然希望对方保持不动。对于内存快照而言,我们也不希望数据“动”。
举个例子。我们在时刻t给内存做快照假设内存数据量是4GB磁盘的写入带宽是0.2GB/s简单来说至少需要20s4/0.2 = 20才能做完。如果在时刻t+5s时一个还没有被写入磁盘的内存数据A被修改成了A那么就会破坏快照的完整性因为A不是时刻t时的状态。因此和拍照类似我们在做快照时也不希望数据“动”也就是不能被修改。
但是如果快照执行期间数据不能被修改是会有潜在问题的。对于刚刚的例子来说在做快照的20s时间里如果这4GB的数据都不能被修改Redis就不能处理对这些数据的写操作那无疑就会给业务服务造成巨大的影响。
你可能会想到可以用bgsave避免阻塞啊。这里我就要说到一个常见的误区了**避免阻塞和正常处理写操作并不是一回事**。此时,主线程的确没有阻塞,可以正常接收请求,但是,为了保证快照完整性,它只能处理读操作,因为不能修改正在执行快照的数据。
为了快照而暂停写操作肯定是不能接受的。所以这个时候Redis就会借助操作系统提供的写时复制技术Copy-On-Write, COW在执行快照的同时正常处理写操作。
简单来说bgsave子进程是由主线程fork生成的可以共享主线程的所有内存数据。bgsave子进程运行后开始读取主线程的内存数据并把它们写入RDB文件。
此时如果主线程对这些数据也都是读操作例如图中的键值对A那么主线程和bgsave子进程相互不影响。但是如果主线程要修改一块数据例如图中的键值对C那么这块数据就会被复制一份生成该数据的副本键值对C。然后主线程在这个数据副本上进行修改。同时bgsave子进程可以继续把原来的数据键值对C写入RDB文件。
![](https://static001.geekbang.org/resource/image/a2/58/a2e5a3571e200cb771ed8a1cd14d5558.jpg "写时复制机制保证快照期间数据可修改")
这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。
到这里我们就解决了对“哪些数据做快照”以及“做快照时数据能否修改”这两大问题Redis会使用bgsave对当前内存中的所有数据做快照这个操作是子进程在后台完成的这就允许主线程同时可以修改数据。
现在,我们再来看另一个问题:多久做一次快照?我们在拍照的时候,还有项技术叫“连拍”,可以记录人或物连续多个瞬间的状态。那么,快照也适合“连拍”吗?
## 可以每秒做一次快照吗?
对于快照来说,所谓“连拍”就是指连续地做快照。这样一来,快照的间隔时间变得很短,即使某一时刻发生宕机了,因为上一时刻快照刚执行,丢失的数据也不会太多。但是,这其中的快照间隔时间就很关键了。
如下图所示我们先在T0时刻做了一次快照然后又在T0+t时刻做了一次快照在这期间数据块5和9被修改了。如果在t这段时间内机器宕机了那么只能按照T0时刻的快照进行恢复。此时数据块5和9的修改值因为没有快照记录就无法恢复了。
![](https://static001.geekbang.org/resource/image/71/ab/711c873a61bafde79b25c110735289ab.jpg "快照机制下的数据丢失")
所以要想尽可能恢复数据t值就要尽可能小t越小就越像“连拍”。那么t值可以小到什么程度呢比如说是不是可以每秒做一次快照毕竟每次快照都是由bgsave子进程在后台执行也不会阻塞主线程。
这种想法其实是错误的。虽然bgsave执行时不阻塞主线程但是**如果频繁地执行全量快照,也会带来两方面的开销**。
一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
另一方面bgsave子进程需要通过fork操作从主线程创建出来。虽然子进程在创建后不会再阻塞主线程但是fork这个创建过程本身会阻塞主线程而且主线程的内存越大阻塞时间越长。如果频繁fork出bgsave子进程这就会频繁阻塞主线程了所以在Redis中如果有一个bgsave在运行就不会再启动第二个bgsave子进程。那么有什么其他好方法吗
此时,我们可以做增量快照,所谓增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。
在第一次做完全量快照后T1和T2时刻如果再做快照我们只需要将被修改的数据写入快照文件就行。但是这么做的前提是**我们需要记住哪些数据被修改了**。你可不要小瞧这个“记住”功能,它需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。如下图所示:
![](https://static001.geekbang.org/resource/image/8a/a5/8a1d515269cd23595ee1813e8dff28a5.jpg "增量快照示意图")
如果我们对每一个键值对的修改都做个记录那么如果有1万个被修改的键值对我们就需要有1万条额外的记录。而且有的时候键值对非常小比如只有32字节而记录它被修改的元数据信息可能就需要8字节这样的画为了“记住”修改引入的额外空间开销比较大。这对于内存资源宝贵的Redis来说有些得不偿失。
到这里你可以发现虽然跟AOF相比快照的恢复速度快但是快照的频率不好把握如果频率太低两次快照间一旦宕机就可能有比较多的数据丢失。如果频率太高又会产生额外开销那么还有什么方法既能利用RDB的快速恢复又能以较小的开销做到尽量少丢数据呢
Redis 4.0中提出了一个**混合使用AOF日志和内存快照**的方法。简单来说内存快照以一定的频率执行在两次快照之间使用AOF日志记录这期间的所有命令操作。
这样一来快照不用很频繁地执行这就避免了频繁fork对主线程的影响。而且AOF日志也只用记录两次快照间的操作也就是说不需要记录所有操作了因此就不会出现文件过大的情况了也可以避免重写开销。
如下图所示T1和T2时刻的修改用AOF日志记录等到第二次做全量快照时就可以清空AOF日志因为此时的修改都已经记录到快照中了恢复时就不再用日志了。
![](https://static001.geekbang.org/resource/image/e4/20/e4c5846616c19fe03dbf528437beb320.jpg "内存快照和AOF混合使用")
这个方法既能享受到RDB文件快速恢复的好处又能享受到AOF只记录操作命令的简单优势颇有点“鱼和熊掌可以兼得”的感觉建议你在实践中用起来。
## 小结
这节课我们学习了Redis用于避免数据丢失的内存快照方法。这个方法的优势在于可以快速恢复数据库也就是只需要把RDB文件直接读入内存这就避免了AOF需要顺序、逐一重新执行操作命令带来的低效性能问题。
不过内存快照也有它的局限性。它拍的是一张内存的“大合影”不可避免地会耗时耗力。虽然Redis设计了bgsave和写时复制方式尽可能减少了内存快照对正常读写的影响但是频繁快照仍然是不太能接受的。而混合使用RDB和AOF正好可以取两者之长避两者之短以较小的性能开销保证数据可靠性和性能。
最后关于AOF和RDB的选择问题我想再给你提三点建议
* 数据不能丢失时内存快照和AOF的混合使用是一个很好的选择
* 如果允许分钟级别的数据丢失可以只使用RDB
* 如果只用AOF优先使用everysec的配置选项因为它在可靠性和性能之间取了一个平衡。
## 每课一问
我曾碰到过这么一个场景我们使用一个2核CPU、4GB内存、500GB磁盘的云主机运行RedisRedis数据库的数据量大小差不多是2GB我们使用了RDB做持久化保证。当时Redis的运行负载以修改操作为主写读比例差不多在8:2左右也就是说如果有100个请求80个请求执行的是修改操作。你觉得在这个场景下用RDB做持久化有什么风险吗你能帮着一起分析分析吗
到这里关于持久化我们就讲完了这块儿内容是熟练掌握Redis的基础建议你一定好好学习下这两节课。如果你觉得有收获希望你能帮我分享给更多的人帮助更多人解决持久化的问题。