5.RDB内存快照
上节课,我们学习了 Redis 避免数据丢失的 AOF 方法。这个方法的好处,是每次执行只需要记录操作命令,需要持久化的数据量不大。
一般而言,只要你采用的不是 always 的持久化策略,就不会对性能造成太大影响。
但是,也正因为记录的是操作命令,而不是实际的数据,所以,用 AOF 方法进行故障恢复的时候,需要逐一把操作日志都执行一遍。如果操作日志非常多,Redis 就会恢复得很缓慢,影响到正常使用。这当然不是理想的结果。那么,还有没有既可以保证可靠性,还能在宕机时实现快速恢复的其他方法呢?
当然有了,这就是我们今天要一起学习的另一种持久化方法:内存快照。
所谓内存快照,就是指内存中的数据在某一个时刻的状态记录。这就类似于照片,当你给朋友拍照时,一张照片就能把朋友一瞬间的形象完全记下来。
对 Redis 来说,它实现类似照片记录效果的方式,就是把某一时刻的状态以文件的形式写到磁盘上,也就是快照。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件就称为 RDB 文件,其中,RDB 就是 Redis DataBase 的缩写。
和 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以在做数据恢复时,我们可以直接把 RDB 文件读入内存,很快地完成恢复。
听起来好像很不错,但内存快照也并不是最优选项,为什么这么说呢?
我们还要考虑两个关键问题:
对哪些数据做快照?这关系到快照的执行效率问题。
做快照时,数据还能被增删改吗?
这关系到 Redis 是否被阻塞,能否同时正常处理请求。
★RDB全量快照:bgsave:fork(会阻塞主线程)
Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中,这就类似于给 100 个人拍合影,把每一个人都拍进照片里。这样做的好处是,一次性记录了所有数据,一个都不少。
给内存的全量数据做快照,把它们全部写入磁盘也会花费很多时间。而且,全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。
对于Redis而言,它的单线程模型就决定了,我们要尽量避免所有会阻塞主线程的操作。所以针对任何操作我们都会提一个灵魂之问:"它会阻塞主线程吗?"
RDB 文件的生成是否会阻塞主线程,这就关系到是否会降低 Redis 的性能。
Redis 提供了两种命令来生成 RDB 文件,分别是 save 和 bgsave。
save:在主线程中执行,会导致主线程阻塞;已经被废弃。
bgsave:fork创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,虽然fork操作会阻塞主线程,但是时间很短。
bgsave也是 Redis RDB 文件生成的默认配置。
save 60 1000
上面的命令指的是60秒内,有1000次更改的话就会执行bgsave;
★RDB快照持久化时数据可以修改:写时复制技术
如果快照执行期间数据不能被修改,Redis 就不能处理对这些数据的写操作,那无疑就会给业务服务造成巨大的影响。
你可能会想bgsave不是可以避免阻塞吗?
这里我就要说到一个常见的误区了,避免阻塞和正常处理写操作并不是一回事。
此时,主线程的确没有阻塞,可以正常接收请求,但是,为了保证快照完整性,它只能处理读操作,因为不能修改正在执行快照的数据。
为了快照而暂停写操作,肯定是不能接受的。
所以这个时候,Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。
简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。
bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。
此时,如果主线程对这些数据也都是读操作,例如图中的键值对 A,那么,主线程和 bgsave 子进程相互不影响。
但是,如果主线程要修改一块数据,例如图中的键值对 C,那么这个数据所在的页就会被复制一份作为该数据的副本。
然后修改副本的数据即可。后续等整个RDB文件写入完成,将所有的副本数据写入RDB文件即可。
由于这一块没有找到相关资料,所以加粗的地方待确认。
这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。
到这里,我们就解决了对"哪些数据做快照"以及"做快照时数据能否修改"这两大问题:Redis 会使用 bgsave 对当前内存中的所有数据做快照,这个操作是子进程在后台完成的,这就允许主线程同时可以修改数据。
Fork函数与写时复制
核心思路:fork一个子进程,只有在父进程发生写操作修改内存数据时,才会真正去分配内存空间,并复制内存数据,而且也只是复制被修改的内存页中的数据,并不是全部内存数据。
- Redis中执行BGSAVE命令生成RDB文件时,本质就是调用Linux中的fork()命令,Linux下的fork()系统调用实现了copy-on-write写时复制;
- fork()是类Unix操作系统上创建线程的主要方法,fork用于创建子进程(等同于当前进程的副本);
- 传统的 普通进程复制,会直接将父进程的数据拷贝到子进程中,拷贝完成后,父进程和子进程之间的数据段 和堆栈是相互独立的;
- copy-on-write技术,在fork出子进程后,与父进程共享内存空间,两者只是虚拟空间不同,但是其对应的物理空间是同一个;
在 Redis 中,Fork 函数被用于创建子进程。Redis 的使用场景中通常有大量的读操作和较少的写操作,而 Fork 函数调用了Linux 操作系统的写时复制(Copy On Write,即 COW)机制,让父子进程共享内存,从而减少内存占用,并且避免了没有必要的数据复制。
我们可以使用 Linux下的 man fork
命令来查看下fork函数的说明文档。翻译如下:
在Linux下,fork()是使用写时复制的页实现的,所以它唯一的代价是复制父进程的页表以及为子进程创建独特的任务结构所需的时间和内存。
简单来说就是 fork()
函数会复制父进程的地址空间到子进程中,复制的是指针,而不是数据,所以速度很快。
在 Redis 中,当执行 RDB 持久化操作时,Redis 会调用 fork 函数创建子进程,然后由子进程负责将数据写入到磁盘中。为了避免父子进程同时对内存中的数据进行修改导致数据不一致。Redis 会启用写时复制机制。
这样,当父进程修改内存中的数据时, Linux 内核会将该部分内存复制一份给子进程使用,从而保证父子进程间的数据互相独立。
当没有发生写的时候,子进程和父进程指向地址是一样的。
当发生写的时候,就会拷贝出一块新的内存区域,实现父子进程隔离。
通过使用 fork 函数和写时复制机制,Redis 可以高效地执行 RDB 持久化操作,并且不会对 Redis 运行过程中的性能造成太大的影响。
同时,这种方式也提供了一种简单有效的机制来保护 Redis 数据的一致性和可靠性。
不过,需要注意的是:
fork的这个过程主进程是阻塞的,fork完之后不阻塞。
RDB 需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能会导致Redis在一些毫秒级内不能响应客户端的请求,数据集很大的时候,fork过程可能会持续数秒。
可能会因为数据量大而导致主进程长时间被挂起,造成Redis服务不可用。因此,在设计时应尽可能减少数据量或者优化fork的调用频率。
写时复制的思考
上述写时复制流程貌似有个问题:
比如,有个键值对 k1 a 。此时Redis正在bgsave
。这时客户端发来一个请求,主进程发生写操作set k1 b,由于写时复制,此时子进程里k1的值还是a。最终持久化的也是a。
为什么不直接持久化新值而持久化旧值?写时复制的意义是什么?
基于上面的问题,可以给出的解释主要有两点:
- 其实Redis为了性能考虑,内存的持久化是一个顺序写的操作。子进程备份RDB是一个顺序写的过程,如果主进程的所有写入请求都随时记录到RDB文件中,那么理论更新的key可能在任何位置出现,就会变为随机写,性能低。 [个人感觉是不会的,内存不同于磁盘存在随机IO和顺序IO]
- 其次如果主进程一直在写入更新key的话,那么这次RDB备份一直都在写主进程写入的新值,永远不会停止。
好了,这个时候,我们就可以通过 bgsave 命令来执行全量快照,这既提供了数据的可靠性保证,也避免了对 Redis 的性能影响。
接下来,我们要关注的问题就是,在对内存数据做快照时,这些数据还能"动"吗?
也就是说,这些数据还能被修改吗?
这个问题非常重要,这是因为如果数据能被修改,那就意味着Redis还能正常处理写操作。否则,所有写操作都得等到快照完了才能执行,性能一下子就降低了。
Copy On Write优/缺点
优点:
- COW技术可减少分配和复制大量资源时带来的瞬间延时。
- COW技术可减少不必要的资源分配。比如fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。
缺点:
- 如果在fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误(页异常中断page-fault),这样就得不偿失。
- Redis在持久化时,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那Redis会fork出一个子进程来读取数据,从而写到磁盘中。如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
- 总体来看,Redis还是读操作比较多。如果子进程存在期间,发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断page-fault),这样就得耗费不少性能在复制上。
- 而在rehash阶段上,写操作是无法避免的。所以Redis在fork出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。
RDB快照频率不能太快
对于快照来说,所谓"连拍"就是指连续地做快照。这样一来,快照的间隔时间变得很短,即使某一时刻发生宕机了,因为上一时刻快照刚执行,丢失的数据也不会太多。但是,这其中的快照间隔时间就很关键了。
如下图所示,我们先在 T0 时刻做了一次快照,然后又在 T0+t 时刻做了一次快照,在这期间,数据块 5 和 9 被修改了。如果在 t 这段时间内,机器宕机了,那么,只能按照 T0 时刻的快照进行恢复。此时,数据块 5 和 9 的修改值因为没有快照记录,就无法恢复了。
所以,要想尽可能恢复数据,t 值就要尽可能小,t 越小,就越像"连拍"。那么,t 值可以小到什么程度呢,比如说是不是可以每秒做一次快照?毕竟,每次快照都是由 bgsave 子进程在后台执行,也不会阻塞主线程。这种想法其实是错误的。虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销。
一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。
到这里,你可以发现,虽然跟 AOF 相比,快照的恢复速度快,但是,快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。如果频率太高,又会产生额外开销,那么,还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?
那么,有什么其他好方法吗?
★AOF增量快照
此时,我们可以做增量快照,所谓增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。
在第一次做完全量快照后,T1 和 T2 时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。但是,这么做的前提是,我们需要记住哪些数据被修改了。你可不要小瞧这个"记住"功能,它需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。如下图所示:
如果我们对每一个键值对的修改,都做个记录,那么,如果有 1 万个被修改的键值对,我们就需要有 1 万条额外的记录。而且,有的时候,键值对非常小,比如只有 32 字节,而记录它被修改的元数据信息,可能就需要 8 字节,这样的画,为了"记住"修改,引入的额外空间开销比较大。这对于内存资源宝贵的 Redis 来说,有些得不偿失。
混合快照:RDB全量1<-AOF增量->RDB全量2
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,RDB内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
如下图所示,T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。
这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势,颇有点"鱼和熊掌可以兼得"的感觉,建议你在实践中用起来。
混合模式配置
config
appendonly yes
aof-use-rdb-preamble yes
混合 aof 加载
开启混合存储模式后 aof 文件加载的流程如下:
- aof 文件开头是 rdb 的格式, 先加载 rdb 内容再加载剩余的 aof
- aof 文件开头不是 rdb 的格式,直接以 aof 格式加载整个文件
判断 aof 文件的前面部分是否为 rdb 格式,只需要判断前 5 个字符是否是 REDIS。
这个是因为 rdb 持久化开头就是 REDIS, 同时 aof 命令开头一定不会是 REDIS(命令开头都是 *)。
小结
Redis 用于避免数据丢失的RDB内存快照方法,它的优势在于可以快速恢复数据库,也就是只需要把 RDB 文件直接读入内存,这就避免了 AOF 需要顺序、逐一重新执行操作命令带来的低效性能问题。
内存快照也有它的局限性。它拍的是一张内存的"大合影",不可避免地会耗时耗力。并且不支持实时持久化。
虽然,Redis 设计了 bgsave 和写时复制方式,尽可能减少了内存快照对正常读写的影响,但是,频繁快照仍然是不太能接受的。
而混合使用 RDB 和 AOF,正好可以取两者之长,避两者之短,以较小的性能开销保证数据可靠性和性能。
最后,关于 AOF 和 RDB 的选择问题,我想再给你提三点建议:
如果允许分钟级别的数据丢失,可以只使用 RDB;
如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。
数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
参考: blog.csdn.net/hellozhxy/a... blog.csdn.net/qq_45422703... zhuanlan.zhihu.com/p/661183511 blog.csdn.net/weixin_4838... download.csdn.net/blog/column...