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,简单来说,至少需要20s(4/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文件。
这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。
到这里,我们就解决了对“哪些数据做快照”以及“做快照时数据能否修改”这两大问题:Redis会使用bgsave对当前内存中的所有数据做快照,这个操作是子进程在后台完成的,这就允许主线程同时可以修改数据。
现在,我们再来看另一个问题:多久做一次快照?我们在拍照的时候,还有项技术叫“连拍”,可以记录人或物连续多个瞬间的状态。那么,快照也适合“连拍”吗?
可以每秒做一次快照吗?
对于快照来说,所谓“连拍”就是指连续地做快照。这样一来,快照的间隔时间变得很短,即使某一时刻发生宕机了,因为上一时刻快照刚执行,丢失的数据也不会太多。但是,这其中的快照间隔时间就很关键了。
如下图所示,我们先在T0时刻做了一次快照,然后又在T0+t时刻做了一次快照,在这期间,数据块5和9被修改了。如果在t这段时间内,机器宕机了,那么,只能按照T0时刻的快照进行恢复。此时,数据块5和9的修改值因为没有快照记录,就无法恢复了。
所以,要想尽可能恢复数据,t值就要尽可能小,t越小,就越像“连拍”。那么,t值可以小到什么程度呢,比如说是不是可以每秒做一次快照?毕竟,每次快照都是由bgsave子进程在后台执行,也不会阻塞主线程。
这种想法其实是错误的。虽然bgsave执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销。
一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
另一方面,bgsave子进程需要通过fork操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁fork出bgsave子进程,这就会频繁阻塞主线程了(所以,在Redis中如果有一个bgsave在运行,就不会再启动第二个bgsave子进程)。那么,有什么其他好方法吗?
此时,我们可以做增量快照,所谓增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。
在第一次做完全量快照后,T1和T2时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。但是,这么做的前提是,我们需要记住哪些数据被修改了。你可不要小瞧这个“记住”功能,它需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。如下图所示:
如果我们对每一个键值对的修改,都做个记录,那么,如果有1万个被修改的键值对,我们就需要有1万条额外的记录。而且,有的时候,键值对非常小,比如只有32字节,而记录它被修改的元数据信息,可能就需要8字节,这样的画,为了“记住”修改,引入的额外空间开销比较大。这对于内存资源宝贵的Redis来说,有些得不偿失。
到这里,你可以发现,虽然跟AOF相比,快照的恢复速度快,但是,快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。如果频率太高,又会产生额外开销,那么,还有什么方法既能利用RDB的快速恢复,又能以较小的开销做到尽量少丢数据呢?
Redis 4.0中提出了一个混合使用AOF日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用AOF日志记录这期间的所有命令操作。
这样一来,快照不用很频繁地执行,这就避免了频繁fork对主线程的影响。而且,AOF日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
如下图所示,T1和T2时刻的修改,用AOF日志记录,等到第二次做全量快照时,就可以清空AOF日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。
这个方法既能享受到RDB文件快速恢复的好处,又能享受到AOF只记录操作命令的简单优势,颇有点“鱼和熊掌可以兼得”的感觉,建议你在实践中用起来。
小结
这节课,我们学习了Redis用于避免数据丢失的内存快照方法。这个方法的优势在于,可以快速恢复数据库,也就是只需要把RDB文件直接读入内存,这就避免了AOF需要顺序、逐一重新执行操作命令带来的低效性能问题。
不过,内存快照也有它的局限性。它拍的是一张内存的“大合影”,不可避免地会耗时耗力。虽然,Redis设计了bgsave和写时复制方式,尽可能减少了内存快照对正常读写的影响,但是,频繁快照仍然是不太能接受的。而混合使用RDB和AOF,正好可以取两者之长,避两者之短,以较小的性能开销保证数据可靠性和性能。
最后,关于AOF和RDB的选择问题,我想再给你提三点建议:
- 数据不能丢失时,内存快照和AOF的混合使用是一个很好的选择;
- 如果允许分钟级别的数据丢失,可以只使用RDB;
- 如果只用AOF,优先使用everysec的配置选项,因为它在可靠性和性能之间取了一个平衡。
每课一问
我曾碰到过这么一个场景:我们使用一个2核CPU、4GB内存、500GB磁盘的云主机运行Redis,Redis数据库的数据量大小差不多是2GB,我们使用了RDB做持久化保证。当时Redis的运行负载以修改操作为主,写读比例差不多在8:2左右,也就是说,如果有100个请求,80个请求执行的是修改操作。你觉得,在这个场景下,用RDB做持久化有什么风险吗?你能帮着一起分析分析吗?
到这里,关于持久化我们就讲完了,这块儿内容是熟练掌握Redis的基础,建议你一定好好学习下这两节课。如果你觉得有收获,希望你能帮我分享给更多的人,帮助更多人解决持久化的问题。
- Devin 👍(130) 💬(8)
老师全文中都是使用的“主线程”而不是“主进程”,评论中大家有的用的是“主线程”有的是“主进程”。请问下老师为啥不是用的“主进程”?
2020-08-24 - Kaito 👍(1065) 💬(119)
2核CPU、4GB内存、500G磁盘,Redis实例占用2GB,写读比例为8:2,此时做RDB持久化,产生的风险主要在于 CPU资源 和 内存资源 这2方面: a、内存资源风险:Redis fork子进程做RDB持久化,由于写的比例为80%,那么在持久化过程中,“写实复制”会重新分配整个实例80%的内存副本,大约需要重新分配1.6GB内存空间,这样整个系统的内存使用接近饱和,如果此时父进程又有大量新key写入,很快机器内存就会被吃光,如果机器开启了Swap机制,那么Redis会有一部分数据被换到磁盘上,当Redis访问这部分在磁盘上的数据时,性能会急剧下降,已经达不到高性能的标准(可以理解为武功被废)。如果机器没有开启Swap,会直接触发OOM,父子进程会面临被系统kill掉的风险。 b、CPU资源风险:虽然子进程在做RDB持久化,但生成RDB快照过程会消耗大量的CPU资源,虽然Redis处理处理请求是单线程的,但Redis Server还有其他线程在后台工作,例如AOF每秒刷盘、异步关闭文件描述符这些操作。由于机器只有2核CPU,这也就意味着父进程占用了超过一半的CPU资源,此时子进程做RDB持久化,可能会产生CPU竞争,导致的结果就是父进程处理请求延迟增大,子进程生成RDB快照的时间也会变长,整个Redis Server性能下降。 c、另外,可以再延伸一下,老师的问题没有提到Redis进程是否绑定了CPU,如果绑定了CPU,那么子进程会继承父进程的CPU亲和性属性,子进程必然会与父进程争夺同一个CPU资源,整个Redis Server的性能必然会受到影响!所以如果Redis需要开启定时RDB和AOF重写,进程一定不要绑定CPU。
2020-08-14 - Geek_Lin 👍(91) 💬(14)
文章中写时复制那里,复制的是主线程修改之前的数据还是主线程修改之后的呢?
2020-08-16 - 扩散性百万咸面包 👍(41) 💬(5)
感觉老师这里是不是说的有点问题? 1.fork()本身应该是比较快的吧?因为COW的存在,只需要部分数据(局部变量)的复制。真正阻塞的是bgsave在持久化过程中写RDB的时候,因为同时要服务写请求,所以主线程要复制对应内存。 2.这个复制为什么不能让fork()出来的子线程去做呢?这样不就不阻塞主线程了吗?
2020-08-29 - yyl 👍(24) 💬(2)
解答: 系统的QPS未知,两种情况吧: 1. QPS较低,不会有什么问题 2. QPS较高,首先由于写多读少,造成更多的写时拷贝,导致更多的内存占用。如果采用增量快照,需要增加额外的内存开销;再则,写RDB文件,OS会分配一些Cache用于磁盘写,进一步加剧内存资源的消耗。 由于频繁的写RDB文件,造成较大的磁盘IO开销,消耗CPU
2020-08-15 - 漫步oo0云端 👍(18) 💬(19)
我想提一个傻问题,我作为初学者想问,如果redis服务挂了,备份有什么用?能恢复的前提不是服务还存活吗?难道服务挂了会自动拉起服务?自动还原吗?
2020-08-14 - 吕 👍(9) 💬(1)
老师,请教一下,bgsave命令只能是手动执行么?没配置中只看到了save,没有bgsave的配置
2020-08-29 - 小宇子2B 👍(8) 💬(4)
做RDB期间是写时复制的 2GB的数据 80%都是写请求 也就是大概要复制出来1.6GB数据,加上本身数据2GB ,已经达到3.6GB,去掉操作系统本身的内存占用 机器所剩内存已经不多了 容易发生OOM
2020-08-14 - 周翔在山麓(Xiang Zhou) 👍(7) 💬(5)
这一讲真的很好, aof 相当于数据库的 binlog, rdb 相当于redo log. 知识互相映射, 加强了学习. 我又看了一遍mysql 的数据恢复机制. 同学们还记得吗?
2021-01-01 - Spring4J 👍(2) 💬(2)
由于修改操作占大部分比例,为了尽可能保证宕机时数据的完整性,快照的间隔就不能太长,而间隔太短又会带来很多的性能开销,所以对于这种特点的数据,不适合使用RDB的持久化方式
2020-08-14 - 不负青春不负己🤘 👍(0) 💬(1)
打卡 第二遍
2020-08-18 - 无名十三 👍(0) 💬(1)
简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。 老师,这里是主线程还是主进程?
2020-08-14 - 注定非凡 👍(117) 💬(6)
1,作者讲了什么? 作者在本章讲了redis两种持久化方式中的RDB方式 2,作者是怎么把这事给讲明白的? 为了让大家明白RDB的快照的概念,作者举了拍照片,照合影的例子 3,为了讲明白,作者讲了哪些要点,有哪些亮点? (1)亮点1:作者解释快照使用了拍合影的例子,让我很好的理解快照的概念,以及内存数据大小对快照产生的影响 (2)要点1:RDB快照,将此时内存中的所有的数据写入磁盘 (3)要点2:生成快照有两种方式:sava和bgsava,save是主进程执行,生成时会阻塞redis,只能执行查找。bgsave是由主进程fork出子进程执行, (4)要点3:子进程在被fork处理时,与主进程共享同一份内存,但在生成快照时采取COW机制,确保不会阻塞主进程的数据读写 (5)要点4:RDB的执行频率很重要,这会影响到数据的完整性和Redis的性能稳定性。所以4.0后有了aof和rdb混合的数据持久化机制 4,对于作者所讲,我有哪些发散性思考? 作者开篇提到的两个问题:快照什么数据,快照有何影响,具体的场景,才能讨论出具体的技术方案,我个人认为,脱离场景谈方案是在自嗨 5,将来有哪些场景,我能够使用到它? 我们项目的redis持久化使用的方式就是aof和rdb混合,前一段时间,做过集群升级扩容。把每台8c,30G内存,5主5从,升级改造成为8c,15G内存,15主15从。这样搞主要是因为之前的集群内存占用太高,导致数据持久化失败 6,读者评论的收获: 定这个专栏,真是觉得捡到宝了,大神@Kaito写的评论实在漂亮,每次都要读好几遍,读完都有赏心悦目的愉悦感,期望自己有一天也可像他那样出色
2020-08-15 - Darren 👍(51) 💬(18)
Kaito的回答为啥老让我觉得自己那么菜呢������ 我稍微补充下老师对于 ”混合使用 AOF 日志和内存快照“这块的东西: 在redis4.0以前,redis AOF的重写机制是指令整合(老师上一节课已经说过),但是在redis4.0以后,redis的 AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头,将增量的以指令的方式Append到AOF,这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分就是压缩格式不再是 AOF 格式,可读性较差。Redis服务在读取AOF文件的怎么判断是否AOF文件中是否包含RDB,它会查看是否以 REDIS 开头;人为的看的话,也可以看到以REDIS开头,RDB的文件也打开也是乱码。 可以通过aof-use-rdb-preamble 配置去设置改功能。 # When rewriting the AOF file, Redis is able to use an RDB preamble in the # AOF file for faster rewrites and recoveries. When this option is turned # on the rewritten AOF file is composed of two different stanzas: # # [RDB file][AOF tail] # # When loading Redis recognizes that the AOF file starts with the "REDIS" # string and loads the prefixed RDB file, and continues loading the AOF # tail. aof-use-rdb-preamble yes
2020-08-14 - 曾轼麟 👍(19) 💬(7)
【内存风险】:2 核 CPU、4GB 内存、500GB 磁盘, 2GB数据,在操作系统虚拟内存的支持下,fork出一个子进程会贡献主进程的虚拟内存映射的物理空间,这个是MMU实现的不属于Redis的产物,但是当发生数据修改的时候,MMU会将子进程的物理内存复制独立出来(写时拷贝技术)。在 8:2的独写比例中实际需要的物理内存可能会达到 1.6 +1.6 = 3.2 。假设开启swap的情况下,在物理内存不能满足程序运行的时候,虚拟内存技术会将内存中的数据保存在磁盘上,这样会导致Redis性能下降。 对于绑核问题,我认同Kaito同学的说法。不过我认为的问题是因为云主机的原因:一般大型服务器是使用QPI总线,NMUA架构。NUMA中,虽然内存直接绑定在CPU上,但是由于内存被平均分配在了各个组上。只有当CPU访问自身直接绑定的内存对应的物理地址时,才会有较短的响应时间。而如果需要访问其他CPU 绑定的内存的数据时,就需要通过特殊的通道访问,响应时间就相比之前变慢了(甚至有些服务器宁愿使用swap也不走特殊通道),假如当前云主机比较繁忙的情况下,这样就会导致性能下降。(大部分互联网公司都使用了服务器虚拟化技术)
2020-08-17