持久化

持久化 #

Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证 Redis 的数据不会因为故障而丢失, 这种机制就是 Redis 的持久化机制。

Redis 的持久化机制有两种:

  • 快照,快照是一次全量备份。
  • AOF 日志,AOF 日志是连续的增量备份。

RDB #

快照(RDB)是内存数据的二进制序列化形式,在存储上非常紧凑

Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写。内存快照要求 Redis 必须进行文件 IO 操作, 可文件 IO 操作是不能使用多路复用 API。

这意味着单线程同时在服务线上的请求还要进行文件 IO 操作,文件 IO 操作会严重拖垮服务器请求的性能。

还有个重要的问题是为了不阻塞线上的业务,就需要边持久化边响应客户端请求。持久化的同时,内存数据结构还在改变,比 如一个大型的 hash 字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完呢,这尼玛要怎么搞?

Redis 使用操作系统的多进程写时复制(Copy On Write,简称 COW) 机制来实现快照持久化。

SAVE 和 BGSAVE #

SAVEBGSAVE 指令可以用来生成 rdb 文件,区别在于 SAVE 会阻塞 Redis 服务器进程,客户端的所有请求都会被拒绝,直到 SAVE 执行 完成。而 BGSAVE 指令会派生一个子进程,由子进程负责创建 rdb 文件,父进程继续处理客户端请求。

创建 rdb 文件实际上是调用了 rdb.c/rdbSave 函数,SAVEBGSAVE 指令会以不同的方式调用这个函数:

def SAVE():
  # 创建 rdb 文件
  rdbSave()
def BGDAVE():
  # 创建子进程
  pid = fork()

  if pid == 0:
    # 子进程创建 rdb 文件
    rdbSave()

    # 完成后想父进程发送信号
    singnal_parent()
  elif pid > 0:
    # 父进程继续处理命令 并轮询等待子进程的信号
    handle_request_and_wait_signal()
  else:
    handle_fork_error()

BGSAVE 指令执行期间要注意

  • 客户端的 SAVE 指令会被拒绝。SAVEBGSAVE 指令禁止同时调用,避免父子进程同时执行两个 rdbSave 调用,防止竞争条件。
  • 客户端的 BGSAVE 指令会被拒绝。
  • BGREWRITEAOFBGSAVE 不能同时执行:
    • 如果 BGSAVE 正在执行,客户端的 BGREWRITEAOF 请求会被延迟,直到 BGSAVE 执行完毕。
    • 如果 BGREWRITEAOF 正在执行,客户端的 BGSAVE 请求会被拒绝。

多进程 #

Redis 在执行 BGSAVE 持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端 请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这是 Linux 操作系统的机制,为了节约内存资源,所以尽可能让它们 共享起来。在进程分离的一瞬间,内存的增长几乎没有明显变化。

子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服 务客户端请求,然后对内存数据结构进行不间断的修改。

这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数 据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬 间的数据

子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫快照的原因。

AOF 日志 #

AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录

假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例顺序执行所有的指令,也就是重放, 来恢复 Redis 当前实例的内存数据结构的状态。

Redis 会在收到客户端修改指令后,进行参数校验进行逻辑处理后,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先执行指令再 将日志存盘。这点不同于 leveldb、hbase 等存储引擎,它们都是先存储日志再做逻辑处理。

AOF 日志在长期的运行过程中会变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。所以需要对 AOF 日志瘦身。

AOF 重写 #

Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令(比如 添加一个 key,但是这时已经失效了,就不需要再添加到 AOF 日志中区),序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的 增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

fsync #

AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核 会异步将脏数据刷回到磁盘的。

这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失。那该怎么办?

Linux 的 glibc 提供了fsync(int fd) 函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要 Redis 进程实时调用 fsync 函数就 可以保证 aof 日志不丢失。但是 fsync 是一个磁盘 IO 操作,它很慢!

所以在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,这个时间是可以配置的。这是在数据安全性和性能之间做了一个折 中,在保持高性能的同时,尽可能使得数据少丢失。

Redis 同样也提供了另外两种策略:

  • 永不 fsync,由操作系统来决定何时同步磁盘,不安全。
  • 执行一个指令就 fsync 一次,这是非常慢的操作。生产环境下不应该使用。

运维 #

  • 快照遍历整个内存,大块写磁盘会加重系统负载
  • AOF 的 fsync 是一个耗时的 IO 操作,它会降低 Redis 性能,同时也会增加系统 IO 负担

所以通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行。从节点是备份节点,没有来自客户端请求的压力,它的操作系统 资源往往比较充沛。

但是如果出现网络分区,从节点长期连不上主节点,就会出现数据不一致的问题,特别是在网络分区出现的情况下又不小心主节点宕机了,那么数据就 会丢失,所以在生产环境要做好实时监控工作,保证网络畅通或者能快速修复。另外还应该再增加一个从节点以降低网络分区的概率,只要有一个从节 点数据同步正常,数据也就不会轻易丢失。

混合持久化 #

重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说 要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。

Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日 志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。

于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。