Redis 持久化
RDB
RDB 持久化是把当前进程数据生成快照保存到硬盘的过程,触发 RDB 持久化过程分为手动触发和自动触发。
快照原理
Redis 使用操作系统的多进程 COW (Copy On Write)
机制来实现快照持久化。
Redis 在持久化时会调用 glibc
的函数 fork
产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。
子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。
这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。
随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的 2 倍大小。另外一个 Redis 实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页面。每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面。
子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。
触发机制
手动触发分别对应save
和bgsave
命令:
-
save
命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。 -
bgsave
命令:Redis进程执行fork
操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork
阶段,一般时间很短。
除了执行命令手动触发之外,Redis内部还存在自动触发RDB的持久化机制,例如以下场景:
- 1)使用save相关配置,如“
save m n
”。表示m
秒内数据集存在n
次修改时,自动触发bgsave
。 - 2)如果从节点执行全量复制操作,主节点自动执行
bgsave
生成RDB文件并发送给从节点。 - 3)执行debug reload命令重新加载Redis时,也会自动触发save操作。
- 4)默认情况下执行
shutdown
命令时,如果没有开启AOF持久化功能则自动执行bgsave
。
Redis save 命令已经废弃。
- 通过
info stats
命令的latest_fork_usec
可以查看父进程fork
时候阻塞的时间 (微秒)。 - 执行
lastsave
命令,可以查看最后一次生成RDB
的时间,也对应info
命令的rdb_last_save_time
选项。
RDB 文件
RDB文件保存在dir
配置指定的目录下,文件名通过dbfilename
配置指定。可以通过执行config set dir{newDir}
和config set dbfilename{newFileName}
运行期动态执行,当下次运行时RDB文件会保存到新目录。
Redis默认采用LZF
算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数config set rdbcompression{yes|no}
动态修改。
RDB 的优点和缺点
RDB的优点:
- RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全量复制等场景。比如每6小时执行
bgsave
备份,并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。 - Redis加载RDB恢复数据远远快于AOF的方式。
RDB的缺点:
- RDB方式数据没办法做到实时持久化/秒级持久化。因为
bgsave
每次运行都要执行fork
操作创建子进程,属于重量级操作,频繁执行成本过高。 - RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。
AOF (Append only File)
AOF 是 Redis 持久化的主流方式。
开启 AOF 功能需要设置 配置: appendonly yes
, 默认不开启。 AOF 文件名通过 appendfilename
配置设置, 默认文件名是 appendonly.aof
。 保存路径同 RDB 持久化方式一致, 通过 dir
配置指定。指定。 AOF 的工作流程操作: 命令写入(append)、 文件同步(sync)、 文件重写(rewrite)、 重启加载(load),
文件同步
AOF 先把命令追加到 aof_buf
缓冲区中。缓冲区同步到文件中,有多种策略,由参数appendfsync
控制:
- 配置为
always
时,每次写入都要同步 AOF 文件,在一般的 SATA 硬盘上,Redis 只能支持大约几百 TPS 写入,显然跟 Redis 高性能特性背道而驰,不建议配置。 - 配置为
no
,由于操作系统每次同步 AOF 文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但数据安全性无法保证。 - 配置为
everysec
, 是建议的同步策略, 也是默认配置,做到兼顾性能和数据安全性。理论上只有在系统突然宕机的情况下丢失 1 秒 的数据。
文件重写
AOF 文件重写是把 Redis 进程内的数据转化为写命令同步到新 AOF 文件的过程。
重写后的 AOF 文件为什么可以变小?有如下原因:
- 进程内已经超时的数据不再写入
del key1
、hdel key2
、srem key3
等命令只保留最终的就行- 多条命令合并
AOF 重写过程可以手动触发bgrewriteaof
和自动触发(auto-aof-rewrite-min-size
和auto-aof-rewrite-percentage
参数决定)。
auto-aof-rewrite-min-size
:表示运行AOF重写时文件最小体积,默认为64MB。auto-aof-rewrite-percentage
:代表当前AOF文件空间(aof_current_size
)和上一次重写后AOF文件空间(aof_base_size
)的比值。
自动触发时机 = aof_current_size > auto-aof-rewrite-min-size &&
(aof_current_size-aof_base_size)/ aof_base_size >= auto-aof-rewrite-percentage
其中aof_current_size
和aof_base_size
可以在info Persistence
统计信息中查看。
AOF 重写流程:
- 步骤 3.1 和步骤 3.2:
fork
完成后,所有命令继续写入aof_buf
缓冲区,并根据appendfsync
机制同步到硬盘。同时,新数据也会写入到aof
重写缓冲区。 - 步骤 4 :内存数据批量写入硬盘,数据量由
aof-rewrite-incremental-fsync
控制,默认 32 MB,防止刷盘过多对硬盘造成损害。 - 步骤 5.1:子进程发送信号给父进程,我已经写完了。父进程更新统计信息。
- 步骤 5.2:AOF 重新缓冲区数据写入到新的 AOF 文件。
- 步骤 5.3:新 AOF 文件替换老文件,完成重写。
AOF 追加阻塞
常用的同步硬盘的策略是 everysec
,这种方式,Redis 使用另外一条线程每秒执行 fsync
同步硬盘,当系统硬盘资源繁忙的时候,会造成 Redis 主线程阻塞。可以通过 iotop
等工具定位消耗硬盘 IO 的进程。
性能
fork 操作
虽然 fork 创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表。例如对于 10GB 的 Redis 进程,需要复制大约 20MB 的内存页表,因此 fork 操作耗时跟进程总内存量息息相关。
fsync 耗时
通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行。从节点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛。
Redis 4.0 混合持久化
重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。