Redis日志之持久化

535 阅读8分钟

关键词

AOFaofredisrdbaof日志重写fork写时复制

AOF(Append of File)日志

MySQL中的写前日志(write ahead log,WAL)不同,redisAOF日志正好相反,它是写后日志,即Redis先执行命令,把数据写到内存中,然后才记录日志。 增量 img

AOF日志的优点与缺点

优点

  • 不阻塞当前的写操作
  • 执行完成后才写日志,说明不会记录语法出错的命令。

缺点

  • 虽然不阻塞当前的写操作,但是可能阻塞后面的操作,因为写AOF日志也是在主线程中执行的。
  • 执行完命令后,可能会宕机,此时还没有写日志。丢失数据的问题。

实际上,写后日志的缺点都是和AOF日志写回磁盘的时机相关的

写入策略

写入策略:执行写操作命令之后是马上写入磁盘还是等会儿写入磁盘?关于这一点redis有自己的配置选项,如下

  • everysec:每秒写入磁盘一次
  • no:由操作系统来控制,每个写命令执行完,只是把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写会磁盘
  • xx:马上写入磁盘:每个写命令执行完,立马同步的将日志写会磁盘

其实这就是主线程阻塞(性能问题)和日志落盘(可靠性问题)之间的一个取舍!

AOF日志重写

为何需要日志重写

因为AOF会随着redis的运行变大。文件变大,本身对文件系统是一个负担、其次往大文件里写入较为耗时、最后使用大容量AOF进行恢复redis比较耗时 一句话总结:AOF日志重写就是遍历redis的内存,创建一个新的AOF文件,与此同时主线程的最新操作记录也记录AOF,最后合并这两个日志。 称之为一处拷贝,两个日志。 redis首先fork一个子进程,使用bgrewriteaof指令将主线程拷贝给子进程的内存进行aof重写,具体为读取数据库中的每一对键值对,然后对每一键值对用一条命令记录它的写入。因为主线程没有阻塞,仍然可以处理新来的操作。redis将新来的操作写到它的缓冲区。最后合并两处AOF日志,组成最新的AOF日志。

RDB(Redis DataBase)日志

快照 - 记录是某一时刻的数据。

快照的方式

  • save 命令:会阻塞主线程
  • bgsave 命令:不会阻塞主线程,是redis默认的生成RDB文件的配置 对当前redis内存中的数据做一个全量备份,fork子进程,不阻塞主线程(但是fork时本身是必定会阻塞主线程的),写时复制 img 图中只是示意图,主线程修改一对键值对,并不会只复制这一对键值对到子进程的内存中,而是一个基本单位-页,这么大的内存。详见下面的思考题

关于AOF与RDB的使用建议

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

思考题

Redis采用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生成RDBAOF重写完成后,都可以在Redis log中看到父进程重新申请了多大的内存空间。

2CPU4GB内存、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每秒刷盘、异步关闭文件描述符这些操作。由于机器只有2CPU,这也就意味着父进程占用了超过一半的CPU资源,此时子进程做RDB持久化,可能会产生CPU竞争,导致的结果就是父进程处理请求延迟增大,子进程生成RDB快照的时间也会变长,整个Redis Server性能下降。

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

上面两个问题均来自极客时间课程下面的留言,respect

总结

  • rdb日志是快照日志,aof是增量日志,两者可以组合使用
  • 为了不阻塞redis的主线程,aof日志重写与rdb日志生成时采用的都是fork子进程,然后采用写时复制技术。但是请注意,fork子进程也不是万无一失的,可能也会阻塞主线程。
  • 写实拷贝导致内存占用更多,继而打爆内存。
  • Redis机器上需要关闭Huge Page机制
  • Redis需要开启定时RDBAOF重写,进程一定不要绑定CPU

参考

  • 极客时间《Redis核心技术与实战》