Redis 持久化
一、持久化的作用
持久化是什么? 为什么需要持久化?
持久化是将内存中的数据以特定的策略写入到文件系统
持久化的作用是通过存储和读取文件系统中特定格式的数据文件,避免进程挂掉后的内存数据的丢失,下次重启后利用之前的数据文件即可恢复数据
Redis中支持两种持久化方案,RDB、AOF
二、RDB 快照
RDB 持久化是指 Redis 进程把数据生成快照保存到文件系统的过程,触发 RDB 持久化的方式分为两种,手动触发 和 自动触发
手动触发
如何手动触发RDB持久化? 手动持久化的有什么问题?
手动触发持久化命令
save: 阻塞当前 Redis 服务,直到 save 持久化完成,不建议在访问量较大的生产中使用
1983:M 03 Jan 08:56:32.199 * DB saved on disk
bgsave: fork 创建子进程执行 save 持久化,阻塞只发生 fork 阶段,通常时间非常短,通过日志可以看到 redis 默认内部使用的 bgsave
1983:M 03 Jan 08:59:08.960 * Background saving started by pid 13078 13078:C 03 Jan 08:59:08.971 * DB saved on disk 13078:C 03 Jan 08:59:08.991 * RDB: 0 MB of memory used by copy-on-write 1983:M 03 Jan 08:59:09.006 * Background saving terminated with success
自动触发
1. 参数配置
默认 redis.conf 配置,save m n
,指m
秒内有 n
次修改的话,自动触发 RDB 持久化执行 bgsave
# 900秒内有1次修改则触发
save 900 1
# 300秒内有10次修改则触发
save 300 10
# 60秒内有10000次修改则触发
save 60 10000
通过日志也可以看到对应触发的情况
1983:M 02 Jan 09:49:41.052 * 10 changes in 300 seconds. Saving...
1983:M 02 Jan 09:49:41.052 * Background saving started by pid 30144
30144:C 02 Jan 09:49:41.060 * DB saved on disk
30144:C 02 Jan 09:49:41.061 * RDB: 0 MB of memory used by copy-on-write
1983:M 02 Jan 09:49:41.153 * Background saving terminated with success
1983:M 02 Jan 09:57:45.945 * DB saved on disk
1983:M 02 Jan 10:12:46.034 * 1 changes in 900 seconds. Saving...
1983:M 02 Jan 10:12:46.034 * Background saving started by pid 30546
30546:C 02 Jan 10:12:46.043 * DB saved on disk
30546:C 02 Jan 10:12:46.043 * RDB: 0 MB of memory used by copy-on-write
1983:M 02 Jan 10:12:46.134 * Background saving terminated with success
2. 从节点同步数据
当 slave 节点执行全量复制操作,master 节点则自动触发 RDB 持久化执行 bgsave 并将数据文件发送给 slave 节点以同步数据
3. 调试重置
执行 debug reload 命令时则会触发 RDB 持久化执行 save
127.0.0.1:6379> debug reload
OK
日志输出
$ tail -0f data/redis.log
1983:M 03 Jan 09:12:38.905 * DB saved on disk
1983:M 03 Jan 09:12:38.905 # DB reloaded by DEBUG RELOAD
4. 实例关闭
执行 shutdown 关闭实例,如果没有开启 AOF 持久化功能,也会触发 RDB 持久化执行 bgsave
命令执行
$ redis-cli shutdown
日志输出
1983:M 03 Jan 09:14:24.998 # User requested shutdown...
1983:M 03 Jan 09:14:24.998 * Saving the final RDB snapshot before exiting.
1983:M 03 Jan 09:14:25.011 * DB saved on disk
1983:M 03 Jan 09:14:25.011 * Removing the pid file.
1983:M 03 Jan 09:14:25.012 # Redis is now ready to exit, bye bye...
工作流程
大致流程
步骤说明
- 手动/触发 RDB 持久化执行 bgsave
- redis 父进程得知到需要执行 RDB 持久化
- redis 父进程检查是否存在子进程正在执行 RDB 持久化
- 如果有子进程正在执行持久化直接返回
- 没有子进程的话 fork 子进程,fork 操作期间,父进程会阻塞,可以通过info stats中的 latest_fork_usec 查看到最近的 fork 阻塞时间,单位是微秒,子进程 fork 创建完毕后,子进程向父进程返回
Background saving started
,父进程收到信息后不再阻塞,父进程更新 info stats 中的 latest_fork_usec,下面是几个相关参数 - 父进程接收新的命令
- 子进程执行 RDB 持久化,首先创建 rdb 数据文件,根据父进程的内存数据生成快照,然后对原有数据文件进行原子替换,根据持久化执行情况更改 info 中的信息
- rdb_changes_since_last_save: 上次持久化后新的修改数
- rdb_bgsave_in_progress: 标识是否有子进程正在执行持久化
- rdb_last_save_time: 上次 RDB 持久化成功的时间戳
- rdb_last_bgsave_status: 上次 RDB 持久化的状态
- rdb_last_bgsave_time_sec: 上次 RDB 持久化花费的时间(秒)
- rdb_current_bgsave_time_sec: 如果有持久化子进程,子进程当前正在执行的 RDB 持久化的秒级时间
数据文件
数据存储配置
RDB 持久化出来的文件,存在 dir 配置下以 dbfilename 配置命名,例如
获取 dir 和 dbfilename 配置
127.0.0.1:6379> config get dir
1) "dir"
2) "/opt/redis-3.0.7/data"
127.0.0.1:6379> config get dbfilename
1) "dbfilename"
2) "dump.rdb"
列出文件
root@iZ947mgy3c5Z:/opt/redis# ls -al /opt/redis-3.0.7/data/dump.rdb
-rw-r--r-- 1 root root 42 Jan 3 09:59 /opt/redis-3.0.7/data/dump.rdb
上面两个参数支持动态修改,当磁盘出现问题,可以通过动态调整数据文件路径避免持久化失败或者重启 redis 配置的问题
创建新数据目录
$ mkdir -p /data/redis_6379/data
修改配置
$ redis-cli
127.0.0.1:6379> config set dir "/data/redis_6379/data"
OK
127.0.0.1:6379> config set dbfilename "redis_6379.rdb"
OK
127.0.0.1:6379> config get dir
1) "dir"
2) "/data/redis_6379/data"
127.0.0.1:6379> config get dbfilename
1) "dbfilename"
2) "redis_6379.rdb"
127.0.0.1:6379> bgsave
Background saving started
列出文件
$ ls -al /data/redis_6379/data/redis_6379.rdb
-rw-r--r-- 1 root root 62 Jan 3 10:13 /data/redis_6379/data/redis_6379.rdb
数据文件检查
$ redis-check-dump /data/redis_6379/data/redis_6379.rdb
==== Processed 6 valid opcodes (in 45 bytes) ===================================
CRC64 checksum is OK
总结
优点
数据文件体积小,非常适合定期备份,全量复制,例如每 6 个小时备份一次,然后将数据文件同步到远程主机,用作灾难恢复
加载速度相较于 aof 快很多
缺点
无法实现实时备份/秒级备份,而且 fork 在访问量较高的场景属于成本比较高的操作
rdb 数据文件在 redis 新老版本之间存在不兼容的问题
补充
原子替换
RDB 文件一旦被创建,就不会进行任何修改。当服务器要创建一个新的 RDB 文件时,它先将文件的内容保存在一个临时文件里,当临时文件写入完毕时,程序才原子地使用临时文件替换原来的 RDB 文件。即,无论何时,复制 RDB 文件都是绝对安全的
三、AOF 方案
开启 AOF
默认 redis 持久化方案是 RDB,而 aof 是没有开启,所以需要手动启用配置
appendonly yes
或者动态配置开启
$ redis-cli
127.0.0.1:6379> config get appendonly
1) "appendonly"
2) "no"
127.0.0.1:6379> config set appendonly yes
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set name da
OK
127.0.0.1:6379>
$ more data/appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
set
$4
name
$2
da
讲道理看到这我笑出声来了,咋这么眼熟,这不是就是 Redis 通信协议中 RESP 标准序列化后的命令么
工作流程
- redis 将 RESP 命令序列写入到 aof_buf 缓冲区中
- AOF 缓冲区根据策略将缓冲区的数据追加到
$dir/$appendfilename
配置命名的文件中 - Redis 会通过重写 AOF 文件以达到压缩的目的
- Redis 启动时会读取 AOF 实现数据的恢复
流程图
流程说明
- 主进程接收到 bgrewrite 指令
- 主进程 fork 子进程,fork 期间阻塞,fork 成功由子进程执行重写,主进程接收新命令
- 为了保证数据的完整性,新的写入命令会写到
aof_buf
中,由于子进程只能获取到父进程 fork 时的数据,如果仅基于那时的数据进行重写,肯定会丢失重写期间的写入命令,所以新的写入命令除了写到aof_buf
,还会写到aof_rewrite_buf
- 子进程基于 fork 时的内存数据进行重写
- 子进程通知父进程已经将数据以 RESP 重写到新的 aof 文件中,更新 info 中
aof_*
信息 - 父进程将重写期间的写入命令追加到新的 aof 文件
- 使用新 aof 替换老的 aof 文件,整个
bgrewriteaof
完成
为什么要先写到 aof_buf 缓冲区中?
如果没有 aof_buf 缓冲区,每次发生修改操作都直接写入到文件系统中,以 redis 是单线程的进程结构,这不意味着 redis 的性能完全受限于硬盘的性能
所以,为了避免这个问题,Redis 使用 aof_buf 缓冲区作为内存到磁盘的一个过渡,然后根据一定的刷盘策略,进行数据持久化
解决了什么问题?带来了什么问题?
使用 aof_buf 缓冲区虽然解决了性能的问题,但是也带来了一些问题,其中一个不可忽视的就是 “数据一致性” 问题
针对这个问题,我们可以调整 刷盘策略 来解决,aof_buf 缓冲区
的刷盘策略对应的配置参数是appendfsync
,一共有 3 种策略
always
: 每次发生修改操作都直接通过系统调用fsync进行刷盘,最安全,性能影响最大,因为fsync的期间是阻塞的,不建议配置,因为和redis的特定背道而驰everysec
: 将数据写入到aof_buf
后,通过write
系统调用操作,write
调用会触发延迟写的机制,此时数据写到了系统的页缓冲中,Linux 内核中通过页缓冲来提高磁盘IO的性能,具体页缓冲什么时候刷盘取决于特定的策略,例如达到特定的大小或者间隔,aof_buf
的 fsync 是由操作系统专门的线程进程刷盘的,推荐配置也是默认配置no
: 由操作系统进行刷盘,一般情况下最长 30 秒,由于时间间隔太长数据安全性无法保证,所以通常不建议配置,除非不关心数据安全性
文件重写
AOF 文件重写简单来说就是,将内存中的数据重新生成一份 RESP 写入命令,文件重写有两个好处
缩小数据文件体积:重写是基于当前内存中的最新数据,自动清理数据文件中的已删除的无效数据,压缩命令,如
lpush key3 value1 lpush key3 value2 -> lpush key3 value1 value2 # PS: list、hash、set、zset 合并时以 64 个元素为界
加快启动载入速度:体积变小,Redis 启动恢复数据时要载入的写入命令也就减少了,从而启动载入数据的速度更快了
文件重写同样分为 手动触发、自动触发
手动触发
重写前,name 由 da 修改为 yo
$ cat data/appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
SET
$4
name
$2
da
*2
$6
SELECT
$1
0
*3
$3
set
$4
name
$2
yo*2
$6
SELECT
$1
0
*3
$3
SET
$4
name
$2
yo
执行 bgrewriteaof
命令手动触发文件重写
127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started
重写后,直接基于当前最新的数据生成的写入命令
$ cat data/appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
SET
$4
name
$2
yo
自动触发
自动触发 AOF 文件重写有两个关键的条件参数
auto-aof-rewrite-min-size
:当 aof 数据文件最小达到该值时auto-aof-rewrite-percentage
:当前 aof 空间和上次 aof 重写后的空间的百分比值
修复检验
校验
$ redis-check-aof data/appendonly.aof
AOF analyzed: size=54, ok_up_to=54, diff=0
AOF is valid
修复
$ redis-check-aof --fix data/appendonly.aof
AOF analyzed: size=99, ok_up_to=77, diff=22
This will shrink the AOF from 99 bytes, with 22 bytes, to 77 bytes
Continue? [y/N]: y
Successfully truncated AOF
手动破坏了 aof 文件里的值以测试修复功能,感觉没那么智能,并不像我以为的那样,只 truncated 掉有问题的部分
例如,当前存在 a、b、c 三个键,手动破坏了 b 键,实际影响可能是 abc 三个键,而且最后恢复回来的值也可能是错误的
四、RDB vs AOF
持久化方案对比
类型 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
文件体积 | 小 | 大 |
恢复速度 | 块 | 慢 |
数据安全性 | 丢数据 | 取决于策略 |
系统影响 | 重 | 轻(不考虑rewrite,仅追加) |
数据载入顺序
流程比较简单,补充说明下
RDB:通过载入 RDB 文件 启动,日志输出
30721:M 04 Jan 11:48:52.938 * DB loaded from disk: 0.000 seconds
30721:M 04 Jan 11:48:52.938 * The server is now ready to accept connections on port 6379
AOF:通过载入 AOF 文件 启动,日志输出
30763:M 04 Jan 11:49:38.196 * DB loaded from append only file: 0.014 seconds
30763:M 04 Jan 11:49:38.196 * The server is now ready to accept connections on port 6379
无持久化文件:如果没有 rbd、aof 数据文件,则日志中不会有任何 load 数据说明,只有服务端在指定的端口准备接收连接
31755:M 04 Jan 13:21:53.196 * The server is now ready to accept connections on port 6379
推荐配置
RDB 持久化
“关”,或者控制 save 频率
远程存储,集中管理
AOF 持久化
- 开或不开,取决于应用场景,两个判断条件,1. 数据是否重要,2. 数据回溯代价大不大
- 远程存储,集中管理
- everysec
五、问题定位分析
fork 耗时问题
在 fork 时子进程会拷贝父进程的页数据,所以跟服务器内存量有关,如果是虚拟化平台,特别是 Xen,可能会更耗时
阿里云一部分 ECS 就是 Xen 虚拟机,如下
$ dmidecode -q -t system
System Information
Manufacturer: Xen
Product Name: HVM domU
Version: 4.0.1
Serial Number: cf2cd85d-645d-4fa4-b8c1-a5eb7d822607
UUID: CF2CD85D-645D-4FA4-B8C1-A5EB7D822607
Wake-up Type: Power Switch
SKU Number: Not Specified
Family: Not Specified
System Boot Information
Status: No errors detected
解决思路:
- 避免使用 Xen 平台虚拟机
- 控制 redis 最大可用内存,fork 耗时和内存容量成正比,建议 10G 以下
- 调整内核参数
sysctl vm.overcommit_memory=1
,避免低内存情况下 save 失败 - 通过调整触发 fork 策略以降低 fork 频率,避免全量复制
子进程开销优化
CPU
开销分析
数据写入文件,属于 CPU 型工作?书里提到 子进程负责把进程内的数据分批写到文件,这里难道不是IO密集型工作?
仔细思考了下,这里指的CPU密集可能是说将进程内的数据转为特定格式的二进制数据并写入到文件,其中设计到了大量的数据”转码(不知道这么形容对不对)”,而且数据并不是一次性写入文件,而是分批写入,所以属于 CPU 密集
优化思路
- 避免 redis 进程绑定 CPU,主进程会和子进程抢 CPU
- 避免和其他 CPU 密集型进程(服务)放在一起,减轻 CPU 争抢的问题
- 多实例情况下尽量保证只有一个实例在执行重写
内存
开销分析
因为要复制父进程的内存数据,理论上需要多一倍的内存空间,但是 Linux 提供了写时复制,父子进程共享相同的物理空间页,父进程只需要开辟一小块内存空间用以存放被修改区域的数据
32012:C 04 Jan 13:29:29.462 * AOF rewrite: 0 MB of memory used by copy-on-write
32266:C 04 Jan 13:46:04.029 * RDB: 0 MB of memory used by copy-on-write
COW(Copy On Write) 写时复制
写入时复制,是一种计算机程序设计领域的优化策略,核心思想是,如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)
此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源
所以总的来说,开销情况如下
- RDB 内存开销:
COW
内存开销 - AOF 内存开销:
COW
内存开销 +aof_rewrite_buf
开销
优化思路
- 多实例情况下尽量保证只有一个实例在执行重写
- 避免在磁盘大量写入情况下重写,因为这会导致 COW 大量副本,从而带来内存消耗
- 关闭巨页
磁盘
开销分析
这里其实就是数据真正落地了,RDB文件应该开销较低一点,以为是二进制并且体积较小,而AOF为文本体积较大,所以开销相较于RDB会大一些
优化思路
- 不同实例数据目录设置到不同的磁盘,以分摊IO压力
- 避免和IO密集型的工作(服务)放到一起
追加阻塞问题
首先理解下流程
everysec刷盘流程
everysec丢失多少数据?
最初说是丢 1s 数据,但是后来又说丢 2s 数据,带着疑惑在源码 ./src/aof.c
中看到,它是 2 秒判断的,如果小于 2 秒,直接 return,大于两秒则开始阻塞直至同步线程完成写入,所以最极端的情况可能是会丢 2s 的数据
./src/aof.c:293: } else if (server.unixtime - server.aof_flush_postponed_start < 2) {
如何定位问题是否来自aof追加阻塞?
- 日志关键字:aof fsync is talking too long(disk is busy?)
- 发生阻塞时info persistence中aof_delayed_fsync指标会累加
如何避免追加阻塞?
aof 最多允许两秒的阻塞,如果发生阻塞说明当前磁盘写 IO 性能被占用
此时,使用 iotop 分析什么进程在使用IO?将类似占用大量 IO 的进程迁移到其他服务器上