Redis 持久化


Redis 持久化

一、持久化的作用

持久化是什么? 为什么需要持久化?

  1. 持久化是将内存中的数据以特定的策略写入到文件系统

  2. 持久化的作用是通过存储和读取文件系统中特定格式的数据文件,避免进程挂掉后的内存数据的丢失,下次重启后利用之前的数据文件即可恢复数据

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...

工作流程

大致流程

步骤说明

  1. 手动/触发 RDB 持久化执行 bgsave
  2. redis 父进程得知到需要执行 RDB 持久化
  3. redis 父进程检查是否存在子进程正在执行 RDB 持久化
  4. 如果有子进程正在执行持久化直接返回
  5. 没有子进程的话 fork 子进程,fork 操作期间,父进程会阻塞,可以通过info stats中的 latest_fork_usec 查看到最近的 fork 阻塞时间,单位是微秒,子进程 fork 创建完毕后,子进程向父进程返回Background saving started,父进程收到信息后不再阻塞,父进程更新 info stats 中的 latest_fork_usec,下面是几个相关参数
  6. 父进程接收新的命令
  7. 子进程执行 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 标准序列化后的命令么

工作流程

  1. redis 将 RESP 命令序列写入到 aof_buf 缓冲区中
  2. AOF 缓冲区根据策略将缓冲区的数据追加到 $dir/$appendfilename 配置命名的文件中
  3. Redis 会通过重写 AOF 文件以达到压缩的目的
  4. Redis 启动时会读取 AOF 实现数据的恢复

流程图

流程说明

  1. 主进程接收到 bgrewrite 指令
  2. 主进程 fork 子进程,fork 期间阻塞,fork 成功由子进程执行重写,主进程接收新命令
  3. 为了保证数据的完整性,新的写入命令会写到 aof_buf 中,由于子进程只能获取到父进程 fork 时的数据,如果仅基于那时的数据进行重写,肯定会丢失重写期间的写入命令,所以新的写入命令除了写到 aof_buf,还会写到 aof_rewrite_buf
  4. 子进程基于 fork 时的内存数据进行重写
  5. 子进程通知父进程已经将数据以 RESP 重写到新的 aof 文件中,更新 info 中 aof_* 信息
  6. 父进程将重写期间的写入命令追加到新的 aof 文件
  7. 使用新 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 写入命令,文件重写有两个好处

  1. 缩小数据文件体积:重写是基于当前内存中的最新数据,自动清理数据文件中的已删除的无效数据,压缩命令,如

    lpush key3 value1 
    lpush key3 value2
    ->
    lpush key3 value1 value2
    # PS: list、hash、set、zset 合并时以 64 个元素为界
  2. 加快启动载入速度:体积变小,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,仅追加)

数据载入顺序

img

流程比较简单,补充说明下

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刷盘流程

img

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 的进程迁移到其他服务器上


文章作者: Da
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Da !
  目录