栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

《轩轩Redis学习笔记》——持久化

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

《轩轩Redis学习笔记》——持久化

文章目录
  • 题引开篇
  • AOF持久化
    • “ 写后日志 ” 的风险
    • 写回策略
    • “大日志文件”的性能问题
    • AOF重写机制
      • 配置重写机制
      • 重写机制的详细过程
  • RDB持久化
    • 全量快照
    • RDB阻塞情况
    • bgsave命令的RDB生成过程
    • 增量快照
      • 增量快照生成过程
      • 优化增量快照
    • AOF与RDB的选择问题
  • 小结
  • 思考题

参考资料:蒋德钧老师的《Redis核心技术与实战》、《redis设计与实现 第二版》、《Redis开发与运维》

题引开篇

你知道Redis里的持久化机制吗?

AOF持久化

AOF,Append only File,追加文件,记录的是 Redis 收到的每一条写命令,这些写命令是以文本形式保存的。

AOF是“ 写后日志 ”,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志。

留个问题:为什么要先执行再记录日志呢?

为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。

除此之外,AOF 还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操作。

“ 写后日志 ” 的风险

风险主要有两个:

  • 如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。

如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。

  • AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。
写回策略

Redis通过配置配置项appendfsync来配置写回策略。

AOF日志的写回策略有如下三种:

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

留个问题:这三种策略能解决上面的两种风险吗?

答案是不能的。“ 同步写回 ”就有可能阻塞,“ 操作系统控制写回 ”就有可能丢失,“ 每秒写回 ”就折中了一下而已。

小结一下:

“大日志文件”的性能问题

AOF是以向文件追加的方式接收所有的写命令,必然会面临日志文件越来越大的问题。主要问题有以下三个方面:

  • 文件系统无法保存过大的文件;
  • 如果文件太大,导致追加效率也会变低;

这里我还不清楚AOF文件里是怎样追加的,讲道理记录一个偏移指针,不是一直可以追加末尾吗,那样追加效率没问题吧?这个不太懂,先放一放。

  • 如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用。
AOF重写机制

“ 重写 “的意思是当AOF文件大到一定程度的时候,直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。

重写机制可以解决 “大日志文件 ”的问题。

留个问题:为什么会想到这种策略呢?

Redis主要是用在缓存、数据库存储的场景,里面会大量存着一个数据多个操作版本的值,而重写机制就是根据这个数据的最新状态,将多个操作组合一下,生成对应的写入命令。这样一行顶替原来的多行,就可以解决大日志文件的问题。

配置重写机制

AOF 文件重写机制通过 redis.conf 配置文件中的 auto-aof-rewrite-percentage以及auto-aof-rewrite-min-size配置:

  • auto-aof-rewrite-percentage:默认100
  • auto-aof-rewrite-min-size:默认64M

这意味着,也就是说Redis会记录上次重写时的AOF大小,当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。

重写机制的详细过程

我们通常称重写过程为“ 一个拷贝、两处日志 ”。

bgrewriteaof,bg rewrite aof,后台重写AOF线程,来完成整个重写过程,避免阻塞主线程,导致性能下降。

  • 一个拷贝
    • 每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。
    • 然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
  • 两处日志
    • 因为主线程未阻塞,仍然可以处理新来的操作。此时,如果有写操作,第一处日志就是指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。
    • 第二处日志指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。

看看下面这张图,图文结合一下:

尽管有重写机制,但在恢复过程中,AOF日志本身的记录写命令的特性,一条条恢复终究是太慢。Redis提供一个新的持久化机制,RDB来解决这个问题。

RDB持久化

RDB,Redis Database的缩写,指的是Redis的内存快照文件。

内存快照:就是指内存中的数据在某一个时刻的状态记录。

在RDB持久化方面,除了研究经典的阻塞问题,我们还得研究另一个问题,对哪些数据进行快照?多久做一次快照?

全量快照

全量快照,指的是对内存中的所有数据都做一个快照,也就是说把某个时刻的所有记录存在磁盘中。

Redis中主要是进行全量快照。

量多就考虑时间问题,我们来看看全量快照时的阻塞情况。

RDB阻塞情况

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。

  • save:在主线程中执行,会导致阻塞;
  • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。

学了那么久发现,很多解决主线程阻塞的情况就是通过子线程来处理,这也是多线程应用得最多的地方。但此时就会带来线程维护问题,很多应用的线程维护都采取线程池的方式。关于线程方面,复习Java的时候再探讨,这里只是突发奇想。

这里注意一个误区,我们说的RDB阻塞是指阻塞主线程读请求的情况,写请求必然会阻塞,都不用讨论,否则会导致快照数据前后不一致的。

Redis通过写时复制技术来解决bgsave命令下阻塞写操作的问题,我们一起来看看。

bgsave命令的RDB生成过程

写时复制技术,Copy On Write,COW,指的是主线程的写操作的对象实际上是一个副本。

简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。

这里和AOF线程不一样,AOF线程是拷贝一份内存数据。

此时,如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。

但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本(键值对 C’)。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。

留个问题:C‘副本该如何处理?里面有数据的最新状态呀

这个书本和专栏都没提到,我也不是很了解。先放一放,以后遇到再补充。

快照的内容和阻塞情况已经解决了。我们来看看下一个问题:快照的间隔时间。

总的来说,快照的间隔时间太大大小都会出问题

  • 间隔太大,数据太旧,很可能导致新数据丢失;
  • 间隔太小,频繁的全量快照频繁写入磁盘,IO开销非常大。

因此Redis采用一种新的策略——增量快照来解决这个问题。

增量快照

所谓增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。

我们来看看增量快照的生成过程。

增量快照生成过程

结合图中的例子说一下:

在第一次做完全量快照后,T1 和 T2 时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。

留个问题:右边元数据表是如何维护的?会产生怎么的问题?

怎么维护我不太了解,但维护这样一个表肯定会占用Redis大量的内存,有点得不偿失。

优化增量快照

为了解决“ 增量元数据表 ”占用Redis内存的问题,Redis 4.0 将这个表持久化,放入AOF日志中。

当然,用了AOF日志就得考虑IO开销的问题,所以实际中对于AOF和RDB,并不总是一起使用。

AOF与RDB的选择问题

考虑具体的业务场景,AOF与RDB并不一定总是需要一起使用:

  • 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
  • 如果允许分钟级别的数据丢失,可以只使用 RDB;
  • 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。

具体策略根据实际情况使用即可。

小结

这节课,我向你介绍了 Redis 用于避免数据丢失的 AOF 方法。这个方法通过逐一记录操作命令,在恢复时再逐一执行命令的方式,保证了数据的可靠性。

这个方法看似“简单”,但也是充分考虑了对 Redis 性能的影响。总结来说,它提供了 AOF 日志的三种写回策略,分别是 Always、Everysec 和 No,这三种策略在可靠性上是从高到低,而在性能上则是从低到高。

此外,为了避免日志文件过大,Redis 还提供了 AOF 重写机制,直接根据数据库里数据的最新状态,生成这些数据的插入命令,作为新日志。这个过程通过后台线程完成,避免了对主线程的阻塞。

其中,三种写回策略体现了系统设计中的一个重要原则 ,即 trade-off,或者称为“取舍”,指的就是在性能和可靠性保证之间做取舍。我认为,这是做系统设计和开发的一个关键哲学,我也非常希望,你能充分地理解这个原则,并在日常开发中加以应用。

不过,你可能也注意到了,落盘时机和重写机制都是在“记日志”这一过程中发挥作用的。例如,落盘时机的选择可以避免记日志时阻塞主线程,重写可以避免日志文件过大。

但是,在“用日志”的过程中,也就是使用 AOF 进行故障恢复时,我们仍然需要把所有的操作记录都运行一遍。再加上 Redis 的单线程设计,这些命令操作只能一条一条按顺序执行,这个“重放”的过程就会很慢了。


我们学习了 Redis 用于避免数据丢失的内存快照方法。这个方法的优势在于,可以快速恢复数据库,也就是只需要把 RDB 文件直接读入内存,这就避免了 AOF 需要顺序、逐一重新执行操作命令带来的低效性能问题。

不过,内存快照也有它的局限性。它拍的是一张内存的“大合影”,不可避免地会耗时耗力。虽然,Redis 设计了 bgsave 和写时复制方式,尽可能减少了内存快照对正常读写的影响,但是,频繁快照仍然是不太能接受的。而混合使用 RDB 和 AOF,正好可以取两者之长,避两者之短,以较小的性能开销保证数据可靠性和性能。

思考题

AOF 日志重写的时候,是由 bgrewriteaof 子进程来完成的,不用主线程参与,我们今天说的非阻塞也是指子进程的执行不阻塞主线程。但是,你觉得,这个重写过程有没有其他潜在的阻塞风险呢?如果有的话,会在哪里阻塞?

Redis采用fork子进程重写AOF文件时,潜在的阻塞风险包括:fork子进程 和 AOF重写过程中父进程产生写入的场景,下面依次介绍:

  1. fork子进程,fork这个瞬间一定是会阻塞主线程的(注意,fork时并不会一次性拷贝所有内存数据给子进程,老师文章写的是拷贝所有内存数据给子进程,我个人认为是有歧义的),fork采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。那什么时候父子进程才会真正内存分离呢?“写实复制”顾名思义,就是在写发生时,才真正拷贝内存真正的数据,这个过程中,父进程也可能会产生阻塞的风险,就是下面介绍的场景。
  2. fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。因为内存分配是以页为单位进行分配的,默认4k,如果父进程此时操作的是一个bigkey,重新申请大块内存耗时会变长,可能会产阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page,页面大小2M),那么父进程申请内存时阻塞的概率将会大大提高,所以在Redis机器上需要关闭Huge Page机制。Redis每次fork生成RDB或AOF重写完成后,都可以在Redis log中看到父进程重新申请了多大的内存空间。

AOF 重写也有一个重写日志,为什么它不共享使用 AOF 本身的日志?

AOF重写不复用AOF本身的日志有两个原因:

  1. 一个原因是父子进程写同一个文件必然会产生竞争问题,控制竞争就意味着会影响父进程的性能。
  2. 二是如果AOF重写过程中失败了,那么原本的AOF文件相当于被污染了,无法做恢复使用。所以Redis AOF重写一个新文件,重写失败的话,直接删除这个文件就好了,不会对原先的AOF文件产生影响。等重写完成之后,直接替换旧文件即可。

我曾碰到过这么一个场景:我们使用一个 2 核 CPU、4GB 内存、500GB 磁盘的云主机运行 Redis,Redis 数据库的数据量大小差不多是 2GB,我们使用了 RDB 做持久化保证。当时 Redis 的运行负载以修改操作为主,写读比例差不多在 8:2 左右,也就是说,如果有 100 个请求,80 个请求执行的是修改操作。你觉得,在这个场景下,用 RDB 做持久化有什么风险吗?你能帮着一起分析分析吗?

  1. 内存资源风险:Redis fork子进程做RDB持久化,由于写的比例为80%,那么在持久化过程中,“写实复制”会重新分配整个实例80%的内存副本,大约需要重新分配1.6GB内存空间,这样整个系统的内存使用接近饱和,如果此时父进程又有大量新key写入,很快机器内存就会被吃光,如果机器开启了Swap机制,那么Redis会有一部分数据被换到磁盘上,当Redis访问这部分在磁盘上的数据时,性能会急剧下降,已经达不到高性能的标准(可以理解为武功被废)。如果机器没有开启Swap,会直接触发OOM,父子进程会面临被系统kill掉的风险。

  2. CPU资源风险:虽然子进程在做RDB持久化,但生成RDB快照过程会消耗大量的CPU资源,虽然Redis处理处理请求是单线程的,但Redis Server还有其他线程在后台工作,例如AOF每秒刷盘、异步关闭文件描述符这些操作。由于机器只有2核CPU,这也就意味着父进程占用了超过一半的CPU资源,此时子进程做RDB持久化,可能会产生CPU竞争,导致的结果就是父进程处理请求延迟增大,子进程生成RDB快照的时间也会变长,整个Redis Server性能下降。

  3. CPU资源风险:虽然子进程在做RDB持久化,但生成RDB快照过程会消耗大量的CPU资源,虽然Redis处理处理请求是单线程的,但Redis Server还有其他线程在后台工作,例如AOF每秒刷盘、异步关闭文件描述符这些操作。由于机器只有2核CPU,这也就意味着父进程占用了超过一半的CPU资源,此时子进程做RDB持久化,可能会产生CPU竞争,导致的结果就是父进程处理请求延迟增大,子进程生成RDB快照的时间也会变长,整个Redis Server性能下降。

  4. 另外,可以再延伸一下,老师的问题没有提到Redis进程是否绑定了CPU,如果绑定了CPU,那么子进程会继承父进程的CPU亲和性属性,子进程必然会与父进程争夺同一个CPU资源,整个Redis Server的性能必然会受到影响!所以如果Redis需要开启定时RDB和AOF重写,进程一定不要绑定CPU。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/631444.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号