概述
本篇文章为读者介绍 Redis 的持久化机制——RDB & AOF。
- RDB:Redis Database
- AOF:Append Only File
RDB
RDB 持久化是把当前进程数据生成快照保存到硬盘的过程。所谓内存快照,就是指内存中的数据在某一个时刻的状态记录。这就类似于照片,当你给朋友拍照时,一张照片就能把朋友一瞬间的形象完全记下来。RDB 就是 Redis DataBase 的缩写。
Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照。也就是说,把内存中的所有数据都记录到磁盘中。但是,RDB 文件越大,往磁盘上写数据的时间开销就越大。
生成快照命令
Redis 提供了两个手动命令来生成 RDB 文件,分别是 save
和 bgsave
。
- save:在主线程中执行,会导致阻塞。对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
- bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞。这也是 Redis RDB 文件生成的默认配置。
除了执行命令手动触发之外,Redis内部还存在自动触发RDB 的持久化机制,例如以下场景:
- 使用
save
相关配置
save 900 1
save 300 10
save 60 10000
上述配置含义:900s 之内至少一次写操作、300s 之内至少发生 10 次写操作、60s 之内发生至少 10000 次写操作,只要满足任一条件均会触发 bgsave
。
- 如果从节点执行全量复制操作,主节点自动执行
bgsave
生成 RDB 文件并发送给从节点。 - 执行 debug reload 命令重新加载 Redis 时,也会自动触发
save
操作。 - 默认情况下执行
shutdown
命令时,如果没有开启 AOF 持久化功能则自动执行bgsave
。
bgsave
为了快照而暂停写操作,肯定是不能接受的。所以这个时候,Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。
bgsave 子进程是由主进程 fork 生成的,可以共享主进程的所有内存数据。bgsave 子进程运行后,开始读取主进程的内存数据,并把它们写入 RDB 文件。
如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 B),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。
rbd文件
RDB 文件保存在配置指定的目录下,文件名通过 dbfilename 配置指定。
dbfilename dump.rdb
可以通过执行 config set dir ${newDir}
和 config set dbfilename ${newFileName}
运行期动态执行,当下次运行时 RDB 文件会保存到新目录。
rdbcompression yes
Redis 默认采用 LZF 算法对生成的 RDB 文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数 config set rdbcompression ${yes|no}
动态修改。 虽然压缩 RDB 会消耗 CPU,但可大幅降低文件的体积,方便保存到硬盘或通过网维示络发送给从节点,因此线上建议开启。
如果 Redis 加载损坏的 RDB 文件时拒绝启动,并打印如下日志:
Short read or OOM loading DB. Unrecoverable error,aborting now.
这时可以使用 Redis 提供的 redis-check-rdb 工具(老版本是 redis-check-dump)检测 RDB 文件并获取对应的错误报告。
redis-check-rdb dump.rdb
优缺点
优点
RDB 是一个紧凑压缩的二进制文件,代表 Redis 在某个时间点上的数据快照。非常适用于备份,全量复制等场景。
比如每隔几小时执行 bgsave
备份,并把 RDB 文件拷贝到远程机器或者文件系统中,用于灾难恢复。Redis 加载 RDB 恢复数据速度远远快于 AOF 的方式。
缺点
RDB 方式数据没办法做到实时持久化甚至秒级持久化。因为 bgsave
每次运行都要执行 fork 操作创建子进程,属于重量级操作,频繁执行成本过高。
RDB 文件使用特定二进制格式保存,Redis 版本演进过程中有多个格式的 RDB 版本,存在老版本 Redis 服务无法兼容新版 RDB 格式的问题。
数据丢失问题
针对 RDB 不适合实时持久化的问题,Redis 提供了 AOF 持久化方式来解决。
如下图所示,我们先在 时刻做了一次快照(下一次快照是 时刻),然后在 时刻,数据块 5 和 8 被修改了。如果在 时刻,机器宕机了,那么只能按照 时刻的快照进行恢复。此时,数据块 5 和 8 的修改值因为没有快照记录,就无法恢复了。
所以这里可以看出,如果想丢失较少的数据,那么 就要尽可能的小,但是如果频繁地执行全量 快照,也会带来两方面的开销:
-
频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
-
另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然子进程在创建后不会再阻塞主线程,但是 fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。
所以基于这种情况,我们就需要 AOF 的持久化机制。
AOF
AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。理解掌握好 AOF 持久化机制对我们兼顾数据安全性和性能非常有帮助。
开启AOF
开启 AOF 功能需要设置配置:appendonly yes
,默认不开启。
appendonly yes
AOF 文件名通过 appendfilename
配置设置,默认文件名是 appendonly.aof。保存路径同 RDB 持久化方式一致,通过 dir 配置指定。
AOF工作流程
AOF 的工作流程主要是 4 个部分:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)。
命令写入
AOF 命令写入的内容直接是 RESP 文本协议格式。例如 lpush list A B
这条命令,在 AOF 缓冲区会追加如下文本:
*3\r\n$5\r\nlpush\r\n$4\r\list\r\n$3\r\nA B
看看 AOF 日志的内容。其中,*3
表示当前命令有三个部分,每部分都是由 +数字
开头,后面紧跟着具体的命令、键或值。这里“数字”表示这部分中的命令、键或值一共有多少字节。
AOF 为什么直接采用文本协议格式?
文本协议具有很好的兼容性。开启 AOF 后,所有写入命令都包含追加操作,直接采用协议格式,避免了二次处理开销。文本协议具有可读性,方便直接修改和处理。
AOF 为什么把命令追加到 aof_buf 中?
Redis 使用单线程响应命令,如果每次写 AOF 文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区 aof_buf 中。还有另一个好处,Redis 可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。
Redis 提供了多种 AOF 缓冲区同步文件策略,由参数 appendfsync
控制。
appendfsync always
appendfsync everysec
appendfsync no
- always:同步写回。每个写命令执行完,立马同步地将日志写回磁盘。
- everysec:每秒写回。每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘。
- no:操作系统控制的写回。每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘,通常同步周期最长 30s。
很明显,配置为 always 时,每次写入都要同步 AOF 文件,在一般的 SATA 硬盘上,Redis 只能支持大约几百 TPS 写入,显然跟 Redis 高性能特性背道而驰,不建议配置。
配置为 no,由于操作系统每次同步 AOF 文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但数据安全性无法保证。
配置为 everysec,是建议的同步策略,也是默认配置,做到兼顾性能和数据安全性。理论上只有在系统突然宕机的情况下丢失 1s 的数据。
想要获得高性能,就选择 no 策略。如果想要得到高可靠性保证,就选择 always 策略。如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择 everysec 策略。
重写机制
随着命令不断写入 AOF,文件会越来越大。为了解决这个问题,Redis 引入 AOF 重写机制压缩文件体积。AOF 文件重写是把 Redis 进程内的数据转化为写命令同步到新AOF文件的过程。
重写后的 AOF 文件为什么可以变小?有如下原因:
- 进程内已经超时的数据不再写入文件。
- 旧的 AOF 文件含有无效命令。如
set a 111
、set a 222
等。重写使用进程内数据直接生成,这样新的 AOF 文件只保留最终数据的写入命令。 - 多条写命令可以合并为一个。如:
lpush list a
、lpush list b
、lpush list c
可以转化为:lpush list a b c
。为了防止单条命令过大造成客户端缓冲区溢出,对于 list、set、hash、zset 等类型操作,以 64 个元素为界拆分为多条。
AOF 重写降低了文件占用空间。除此之外,另一个目的是:更小的 AOF 文件可以更快地被 Redis 加载。AOF 重写过程可以手动触发和自动触发。
手动触发就是直接调用 bgrewriteaof
命令。
自动触发就是根据 auto-aof-rewrite-min-size
和 auto-aof-rewrite-percentage
参数确定自动触发时机。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
auto-aof-rewrite-min-size
表示运行 AOF 重写时文件最小体积,默认为 64MB。
auto-aof-rewrite-percentage
代表当前 AOF 文件空间(aof_currentsize)大小与上一次重写后 AOF 文件空间(aof_base_size)的增长百分比(如果是 100,那么就代表 100%,比上次 AOF 文件增长一倍)。
假设上次 AOF 重写时文件大小为 100 MB,auto-aof-rewrite-percentage
设置为 50
,当前 AOF 文件大小超过 150 MB(即增长了 50%)时,Redis 会触发自动 AOF 重写。
另外,如果在 Redis 在进行 AOF 重写时,有写入操作,这个操作也会被写到重写日志的缓冲区。这样重写日志也不会丢失最新的操作。
重启加载
AOF 和 RDB 文件都可以用于服务器重启时的数据恢复。Redis 重启时加载 AOF 与 RDB 的顺序是怎么样的呢?
- 当 AOF 和 RDB 文件同时存在时,优先加载 AOF。
- 若关闭了 AOF,加载 RDB 文件。
- 加载 AOF/RDB 成功,Redis 重启成功。
- AOF/RDB 存在错误,启动失败打印错误信息。
文件校验
加载损坏的 AOF 文件时会拒绝启动,对于错误格式的 AOF 文件,先进行备份,然后采用
redis-check-aof --fix
命令进行修复,对比数据的差异,找出丢失的数据,有些可以人工修改补全。
AOF 文件可能存在结尾不完整的情况,比如机器突然掉电导致 AOF 尾部文件命令写入不全。Redis 为我们提供了 aof-load-truncated
配置来兼容这种情况,默认开启。加载 AOF 时当遇到此问题时会忽略并继续启动,同时如下警告日志。
aof-load-truncated yes
# yes:末尾被截断的 AOF 文件将会被加载,并打印日志通知用户。
# no:服务器将报错并拒绝启动,这时用户需要使用 redis-check-aof 工具修复 AOF 文件,再重新启动。
RDB-AOF混合持久化
通过 aof-use-rdb-preamble
配置项可以打开混合开关,yes 则表示开启,no 表示禁用,默认是禁用的,可通过 config set 修改。
aof-use-rdb-preamble yes
该状态开启后,如果执行 bgrewriteaof
命令,则会把当前内存中已有的数据弄成二进程存放在 aof 文件中,这个过程模拟了 rdb 生成的过程,然后 Redis 后面有其他命令,在触发下次重写之前,依然采用 AOF 追加的方式。
持久化相关问题
主进程、子进程和后台线程的区别?
进程和线程的区别
从操作系统的角度来看,进程一般是指资源分配单元,例如一个进程拥有自己的堆、栈、虚存空间(页表)、文件描述符等。
而线程一般是指 CPU 进行调度和执行的实体。
一个进程启动后,没有再创建额外的线程,那么这样的进程一般称为主进程或主线程。
Redis 启动以后,本身就是一个进程,它会接收客户端发送的请求,并处理读写操作请求。而且接收请求和处理请求操作是 Redis 的主要工作。Redis 没有再依赖于其他线程,所以一般把完成这个主要工作的 Redis 进程,称为主进程或主线程。
主线程与子进程
通过 Fork 创建的子进程,一般和主线程会共用同一片内存区域,所以上面就需要使用到写时复制技术确保安全。
后台线程
从 4.0 版本开始,Redis 也开始使用 pthread_create
创建线程,这些线程在创建后,一般会自行执行一些任务,例如执行异步删除任务。
持久化过程中有没有其他潜在的风险?
当 Redis 做 RDB 或 AOF 重写时,一个必不可少的操作就是执行 fork 操作创建子进程,对于大多数操作系统来说 fork 是个重量级错误。虽然 fork 创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表。例如对于 10GB 的 Redis 进程,需要复制大约 20MB 的内存页表,因此 fork 操作耗时跟进程总内存量息息相关,如果使用虚拟化技术,特别是 Xen 虚拟机,fork 操作会更耗时。
fork 耗时问题定位
对于高流量的 Redis 实例 OPS 可达 5 万以上,如果 fork 操作耗时在秒级别将拖慢 Redis 几万条命令执行,对线上应用延迟影响非常明显。正常情况下 fork 耗时应该是每 GB 消耗 20ms 左右。可以在 info stats
统计中查 latest_fork_usec
指标获取最近一次 fork 操作耗时,单位 us。
如何改善 fork 操作的耗时?
- 优先使用物理机或者高效支持 fork 操作的虚拟化技术。
- 控制 Redis 实例最大可用内存,fork 耗时跟内存量成正比,线上建议每个 Redis 实例内存控制在 10GB 以内。
- 降低 fork 操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制等。
为什么主从库的复制不使用AOF?
- RDB 文件是二进制文件,无论是要把 RDB 写入磁盘,还是要通过网络传输 RDB,I/O 效率都比记录和传输 AOF 的高。
- 在从库端进行恢复时,用 RDB 的恢复效率要高于用 AOF。