Nemo
Nemo
Redis的AOF与RDB
Redis的AOF与RDB

有一个绝对不能忽略的问题:一旦服务器宕机,内存中的数据将全部丢失

Redis 的持久化主要有两大机制,即 AOF 日志RDB 快照

小结

  • AOF 方法:这个方法通过逐一记录操作命令,在恢复时再逐一执行命令的方式,保证了数据的可靠性;
  • AOF 日志的三种写回策略: Always、Everysec 和 No;
  • 为避免日志文件过大,Redis 还提供了 AOF 重写机制:直接根据数据库里数据的最新状态,生成这些数据的插入命令,作为新日志。这个过程通过后台线程完成,避免了对主线程的阻塞。
  • 三种写回策略体现了系统设计中的一个重要原则 ,即 trade-off,或者称为“取舍”,指的就是在性能和可靠性保证之间做取舍。

AOF 日志的实现

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

407f2686083afc37351cfd9107319a1f.jpg

AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。

以 Redis 收到set testkey testvalue命令后记录的日志为例看看 AOF 日志的内容:

4d120bee623642e75fdf1c0700623a9f.jpg

*3表示当前命令有三个部分,每部分都是由$+数字开头,后面紧跟着具体的命令、键或值。
这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,$3 set表示这部分有 3 个字节,也就是“set”命令。

优点:

  • 避免出现记录错误命令的情况;
  • 不会阻塞当前的写操作。

缺点:

  • Redis 是直接用作数据库,没有来得及记日志就宕机,这个命令和相应的数据就有丢失的风险;
  • AOF 日志也是在主线程中执行,可能会给下一个操作带来阻塞风险。

三种写回策略

AOF 配置项 appendfsync 的三个可选值:

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

72f547f18dbac788c7d11yy167d7ebf8.jpg

AOF 重写机制

解决日志文件过大造成的性能问题。

AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件:读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。

重写机制把日志文件变小,是因为重写机制具有“多变一”功能。所谓的“多变一”,也就是说,旧日志文件中的多条命令,在重写后的新日志中变成了一条命令

6528c699fdcf40b404af57040bb8d208.jpg

AOF 重写不会阻塞

重写过程是由后台线程bgrewriteaof 来完成的。

把重写的过程总结为“一个拷贝,两处日志”:

  • 一个拷贝:每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程;
  • 两处日志:第一处日志就是指正在使用的 AOF 日志,第二处日志,就是指新的 AOF 重写日志。

5770a4f81fb0469656fef2b35d354fe1.jpg

总结来说,每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的线程进行数据重写,所以,这个过程并不会阻塞主线程。

思考:

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

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

a、fork子进程,fork这个瞬间一定是会阻塞主线程的(注意,fork时并不会一次性拷贝所有内存数据给子进程,老师文章写的是拷贝所有内存数据给子进程,我个人认为是有歧义的),fork采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。那什么时候父子进程才会真正内存分离呢?“写实复制”顾名思义,就是在写发生时,才真正拷贝内存真正的数据,这个过程中,父进程也可能会产生阻塞的风险,就是下面介绍的场景。

b、fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。因为内存分配是以页为单位进行分配的,默认4k,如果父进程此时操作的是一个bigkey,重新申请大块内存耗时会变长,可能会产阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page,页面大小2M),那么父进程申请内存时阻塞的概率将会大大提高,所以在Redis机器上需要关闭Huge Page机制。Redis每次fork生成RDB或AOF重写完成后,都可以在Redis log中看到父进程重新申请了多大的内存空间。

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

建议

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

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

  • Redis把某一时刻的状态以文件的形式写到磁盘上,它执行的是全局快照

  • 快照文件就称为 RDB 文件,其中,RDB 就是 Redis DataBase 的缩写。

给哪些内存数据做快照?

针对任何操作,都会提一个灵魂之问:“它会阻塞主线程吗?”

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

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

快照时数据能修改吗?

Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。

如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

4dc5fb99a1c94f70957cce1ffef419cc.jpg

可以每秒做一次快照吗?

在 T0 时刻做了一次快照,然后又在 T0+t 时刻做了一次快照,在这期间,数据块 5 和 9 被修改了。如果在 t 这段时间内,机器宕机了,那么,只能按照 T0 时刻的快照进行恢复。此时,数据块 5 和 9 的修改值因为没有快照记录,就无法恢复了。

711c873a61bafde79b25c110735289ab.jpg

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

8a1d515269cd23595ee1813e8dff28a5.jpg

Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。

T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了:

e4c5846616c19fe03dbf528437beb320.jpg

问题

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

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。

赞赏
Nemo版权所有丨如未注明,均为原创丨本网站采用BY-NC-SA协议进行授权,转载请注明转自:https://kanghaov.com/670.html

kanghaov

文章作者

发表评论

textsms
account_circle
email

Nemo

Redis的AOF与RDB
有一个绝对不能忽略的问题:一旦服务器宕机,内存中的数据将全部丢失。 Redis 的持久化主要有两大机制,即 AOF 日志和 RDB 快照。 小结 AOF 方法:这个方法通过逐一记录操作命令,在…
扫描二维码继续阅读
2020-08-14