Persistence

持久化

Redis提供了2种持久化的方式,「 AOF 日志」和 「 RDB 快照」。

这两种计数都会各用一个日志文件记录信息,但形式的内容不同。

  • AOF文件记录操作命令
  • RDB的文件内容是二进制数据。

AOF日志

AOF全称Append only file,每一条命令以追加形式写入到一个文件。重启Redis时,读取这个文件以达到恢复的作用。注意,只会记录写操作,因为读操作不改变数据。

文件内容

这种保存写操作命令到日志的持久化方式,就是 Redis 里的 AOF(*Append Only File*) 持久化功能,注意只会记录写操作命令,读操作命令是不会被记录的,因为没意义。

在 Redis 中 AOF 持久化功能默认是不开启的,需要我们修改 redis.conf 配置文件中的以下参数:

AOF 日志文件其实就是普通的文本,我们可以通过 cat 命令查看里面的内容,不过里面的内容如果不知道一定的规则的话,可能会看不懂。

我这里以「set name xiaolin」命令作为例子,Redis 执行了这条命令后,记录在 AOF 日志里的内容如下图:

我这里给大家解释下。

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

优点及缺陷

优点:不知道大家注意到没有,Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,这样可以避免检查开销,只有命令执行成功,才会将命令记录到AOF日志。

缺陷:执行操作和记录日志是两个过程,当Redis在这两步中间时宕机了,这个数据就有会有丢失的分险。

落盘策略

日志写入磁盘的过程是在主线程完成的,流程如下。

具体过程为

  1. Redis执行完命令后,会将命令按照前面提到的协议格式,追加到server.aof_buf缓冲区。
  2. 通过write系统调用,将aof_buf缓冲区的数据写入到AOF文件,此时数据并没有写入到磁盘。
  3. 具体写入磁盘的时机由内核决定,也可以主动调用fsync写入。

Redis提供了3种策略,控制fsync的调用时机。在redis.conf的配置文件中appendfsync配置项有以下3种参数可选:

  1. Always,即每次写操作结束,都会主动调用fsync
  2. Everysec,即每秒调用一次fsync,这个过程由一个子线程完成。由主线程唤醒子线程。
  3. No,意味着不主动调用fsync,由操作系统负责写入磁盘的时机。

重写机制

当一个数据被频繁修改时,前面的命令就没有保存的意义了。比如,

127.0.0.1:6379> set key value
OK
127.0.0.1:6379> set key NewValue
OK

前一个命令就没有保存的意义了,所以可以通过重写机制,只记录有效的命令,节省一些空间。

注意,重写时,需要写到新AOF文件之后再覆盖过去,否则可能导致写入到一半的情况,导致AOF文件不可用。

后台重写

由于重写整个文件是比较耗时的,所以**重写 AOF 过程是由后台子进程 bgrewriteaof来完成的**,这么做可以达到两个好处:

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
  • 子进程不会有data race的问题,借助Linux提供的copy on write也可以很大程度上减少数据复制。

Redis设置了一个AOF重写缓冲区,在AOF后台重写期间,Redis会将命令同时写入「AOF 缓冲区」和 「AOF 重写缓冲区」

当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。

主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:

  • 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
  • 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

信号函数执行完后,主进程就可以继续像往常一样处理命令了。

在整个 AOF 后台重写过程中,除了发生写时复制会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主进程。

RDB快照

Redis 提供了两个命令来生成 RDB 文件,分别是 savebgsave,他们的区别就在于是否在「主线程」里执行:

  • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
  • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞

RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。

Redis 还可以通过配置文件(redis.conf)的选项来实现每隔一段时间自动执行一次 bgsave 命令,默认会提供以下配置:

save 900 1
save 300 10
save 60 10000

别看选项名叫 save,实际上执行的是 bgsave 命令,也就是会创建子进程来生成 RDB 快照文件。

只要满足上面条件的任意一个,就会执行 bgsave,它们的意思分别是:

  • 900 秒之内,对数据库进行了至少 1 次修改;
  • 300 秒之内,对数据库进行了至少 10 次修改;
  • 60 秒之内,对数据库进行了至少 10000 次修改。

这里提一点,Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。

所以可以认为,执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。

通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据。

这就是 RDB 快照的缺点,在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少。

后台快照

RDB的快照耗时一般比较长,所以推荐更bgsave,通过fork一个子进程,用到Linux提供的copy-on-write技术。

在生成RDB期间,主线程执行了写操作,那么RDB快照不会包含这个命令,RDB并没有提供AOF那样的一个缓冲区。

AOF+RDB

Redis 配置文件redis.conf将下面这个配置项设置成 yes:

aof-use-rdb-preamble yes

混合持久化工作在 AOF 日志重写过程

当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快

加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失

大Key影响

AOF影响

在AOF3种策略下,有不同的影响

  1. Always,由于是主线程执行fsync,所以会阻塞主线程一段时间。
  2. EverySec,由于是子线程,所以不会阻塞主线程。
  3. No,也不会。

RDB影响

  1. fork创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
  2. 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;

这里额外提一下, 如果 Linux 开启了内存大页,会影响 Redis 的性能的

Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的。

如果采用了内存大页,那么即使客户端请求只修改 100B 的数据,在发生写时复制后,Redis 也需要拷贝 2MB 的大页。相反,如果是常规内存页机制,只用拷贝 4KB。

两者相比,你可以看到,每次写命令引起的复制内存页单位放大了 512 倍,会拖慢写操作的执行时间,最终导致 Redis 性能变慢

那该怎么办呢?很简单,关闭内存大页(默认是关闭的)。

禁用方法如下:

echo never >  /sys/kernel/mm/transparent_hugepage/enabled

其他影响

大 key 除了会影响持久化之外,还会有以下的影响。

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。可以通过unlink来删除,它会唤醒一个子线程,异步删除。
  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。

Persistence
https://messenger1th.github.io/2024/07/24/Redis/Persistence/
作者
Epoch
发布于
2024年7月24日
许可协议