背景
Redis 是内存数据库,一旦宕机,内存中的数据将会全部丢失。因此,Redis 提供持久化机制,将内存中的数据以文件的形式存储到硬盘上, redis 重启时加载持久化文件来恢复原来的数据。
Redis 持久化的方式
- AOF 日志 (append only file):每执行一条写操作命令,就把该命令追加到日志文件里。
- RDB 快照 (redis database):将某一时刻的内存数据,以二进制的方式写入磁盘。
- 混合持久化方式:AOF(增量方式) + RDB(全量方式)
AOF
Redis 每执行一条写操作命令,将命令以追加的方式写入 AOF 日志文件。Redis 重启时,通过重放 AOF 日志中的命令序列,来进行数据恢复。
AOF 文件以文本格式存储,aof 日志只记录对内存修改的指令记录。例如:记录命令 set key value
配置
修改 redis.conf
配置,开启 AOF 日志。
# 开启 aof appendonly yes # 保存的aof文件都以这个为前缀 appendfilename "appendonly.aof" # dir后面指的是redis的持久化文件的生成路径 dir /home/xuheding/redis-data/default # 关闭 aof复写 auto-aof-rewrite-percentage 0 # 关闭 混合持久化 aof-use-rdb-preamble no # 关闭 rdb save ""
AOF写回策略
linux 提供 fsync 函数可以将指定文件的内容强制从内存缓冲区刷到磁盘,主动刷盘。
#include <unistd.h> int fsync(int fd);
AOF 不同写回策略的差异在于:fsync 的调用时机
- always:同步写回。主线程每次写命令执行后,调用 fsync,同步将 AOF 日志数据写回磁盘。保证数据不丢失,但是 redis 性能降低至传统关系数据库的水平。(这个是在主线程中进行写回的)
- everysec:每秒写回。线程
bio-fsync-of
,每秒调用 1 次 fsync。宕机时最多只会丢失 1s 内产生的命令数据,是一种兼顾性能和安全性的折中方案,是 redis 默认策略。 - no:Redis 不调用 fsync,由系统决定刷盘时机。性能高,但是宕机时丢失的数据多。
修改 redis.conf
对应的写回策略
# 1. 每条写命令刷盘 appendfsync always # 2. 每秒刷盘,默认 appendfsync everysec # 3. 交由系统刷盘 appendfsync no
缺点
日志文件大,冗余数据,全量文件重放,数据恢复速度慢。
AOF-Rewrite
随着执行的写命令增多,AOF 日志文件过大, 全量数据重放导致数据恢复过慢。因此,为减少冗余数据,提升数据恢复速度,需要定期对 AOF 重写,压缩 AOF 文件。
AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到新的 AOF 文件,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
原理:fork 进程,根据当前内存 Redis 的数据重写新的 AOF 日志,序列化完毕后再将持久化期间发生的增量 AOF日志追加到新的 AOF 日志。
写时复制COW
写时复制(Copy on Write, COW)
:fork 调用时页表复制,并且将两个进程的每个页面标记为只读,父子进程共用一块物理内存。当有进程试图写操作时,触发缺页中断,从而进行物理内存的复制,并更新其页表项指向这个新的物理内存,然后恢复这个页面的可写权限。总之,谁修改,谁拷贝内存,子进程指向这块新内存。
写时复制的最充分地使用了稀有的物理内存,只有在发生写操作的时候,系统才会去复制物理内存,从而避免物理内存的复制过程导致进程长时间阻塞。
AOF增量
主线程 fork 子进程,主进程正常处理命令请求,子进程执行重写操作,避免主进程阻塞。子进程共享主进程副本数据,以只读的方式读取 Redis 数据,并逐一把内存数据的键值对转换成相应的命令,再将命令写入重写日志(新的 AOF 文件)。之所以使用进程而不是线程,是因为父子进程以只读的方式共享内存数据,当父子进程任意一方修改该共享内存,就会发生写时复制,于是父子进行就有了独立的数据副本,就不用加锁来保证线程安全。
当子进程完成 AOF 重写工作后,向主进程发送一个信号。主进程收到信号后,调用信号处理函数,将 AOF 重写期间的增量 AOF(AOF 重写缓冲区的数据)追加到新的 AOF 文件中。接着,将新的 AOF 文件重命名,覆盖现有的 AOF 文件。
此过程中,只有信号处理函数调用期间会阻塞主进程,从而降低了重写操作对性能造成的影响。
配置
# 开启 aof appendonly yes # 保存的aof文件都以这个为前缀 appendfilename "appendonly.aof" # dir后面指的是redis的持久化文件的生成路径 dir /home/xuheding/redis-data/default # 开启 aof复写 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 关闭 混合持久化 aof-use-rdb-preamble no # 关闭 rdb save ""
策略
这里设置了最小aof-rewrite内存大小,当大于等于64mb的时候就会进行一次复写
而aof-rewrite-percentage 100表示,下一次复写就是原来两倍大小,比如第一次aof文件大小64mb复写,第二次就是128mb,第三次256mb,以此类推
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb
缺点
虽然减少了 AOF 文件的数据量,但是 AOF 全量复写的数据量很大,数据恢复速度慢。
RDB
为解决 AOF 或 AOF 复写文件过大的问题,以快照方式将内存数据写入 RDB 文件。
RDB 快照就是记录某一瞬间的内存数据,记录的是二进制数据(二进制数据在回放的时候效率是最高的),而 AOF 记录的是文本数据。
原理:fork 进程,基于内存对象编码直接持久化。
fork 进程,子进程将内存中的所有数据按照存储方式写入新的 RDB 文件。若在期间内,主线程执行写操作,发生写时复制,被修改的文件生成副本,子进程把副本数据写入新的 RDB 文件,主进程仍可以直接修改原来的数据。快照完成后,子进程退出并通知父进程,父进程用新的 RDB 文件替换旧的 RDB 文件。这种数据持久化方式称为时间点快照,缺点是系统在宕机时将丢失最后一次持久化后的所有数据。
配置
在使用rdb的时候要把aof,和aof-rewrite都关闭
要开启rdb,首先要把save ”“
注释掉
# 关闭 aof 同时也关闭了 aof复写 appendonly no # 关闭 aof复写 auto-aof-rewrite-percentage 0 # 关闭 混合持久化 aof-use-rdb-preamble no # 开启 rdb 也就是注释 save "" # save "" save 3600 1 save 300 100 save 60 10000
策略
save 3600 1
表示3600秒(1小时)内有1次修改数据
save 300 100
表示300秒内有100次修改数据
save 60 10000
表示60秒内有10000次修改数据
上述save语句中,满足任意一个就fork子进程进行rdb写
缺点
内存中所有数据持久化,一旦数据丢失,则会丢失该时间段内的全部数据。此外,数据量大的时候,fork 页表复制和持久化时的写时复制时间过长。
RDB-AOF
RDB-AOF 混合持久化:全量 RDB + 增量 AOF
两种持久化方式的特点
- RDB 文件小,数据恢复快,但是数据丢失多。
- AOF 数据丢失少,但是文件大,数据恢复慢。
因此,汲取两者的优点,在 AOF 重写基础上进行 RDB 优化,既减小了文件大小,提高数据恢复速度,又避免了持久化期间数据的丢失,
当开启混合持久化时,在 AOF 重写日志时,fork 出来的子进程先根据与主线程共享的内存数据以 RDB 的方式写入新的 AOF 文件。主线程对 Redis 的写操作会记录到 AOF 重写缓冲区。当 RDB 持久化结束后,重写缓冲区里的增量命令以 AOF 的形式追加到新的 AOF 文件。写入完成后,子线程通知主线程将新的的 AOF 文件替换旧的 AOF 文件。这样生成的 AOF 文件是: RDB 格式的全量数据 + AOF 格式的增量数据。
这样,Redis 重启时,先加载 RDB 的内容,然后再重放增量 AOF 日志,提升了效率,减少数据丢失。缺点在于 RDB 格式导致 AOF 文件可读性变差。
配置
# 开启 aof appendonly yes # 开启 aof复写 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 开启 混合持久化 aof-use-rdb-preamble yes # 关闭 rdb save "" # save 3600 1 # save 300 100 # save 60 10000
大key问题
大 key:Key 对应的 Value 很大,占用大量的空间。例如 Value 是元素个数超过 5000 个的 ZSet或者hash等待。这样的对象在扩容或缩容时, 会出现内存卡顿。
大 key 对持久化的影响
- fsync:压力大 。AOF 写回策略 always + no,阻塞主进程
- fork:时间长。fork 页表复制和写时复制
fork 对 redis 性能的影响,见 redis 源码:
// 1、定时器 rehash 的时候 void databasesCron(void) { /* Perform hash tables rehashing if needed, but only if there are no * other processes saving the DB on disk. Otherwise rehashing is bad * as will cause a lot of copy-on-write of memory pages. */ // 是否有子进程 if (!hasActiveChildProcess()) { /* Rehash */ } } // 2、dict 扩容 void updateDictResizePolicy(void) { if (!hasActiveChildProcess()) dictEnableResize(); else dictDisableResize(); } void dictDisableResize(void) { dict_can_resize = 0; } static int _dictExpandIfNeeded(dict *d) { ... /* If we reached the 1:1 ratio, and we are allowed to resize the hash * table (global setting) or we should avoid it but the ratio between * elements/buckets is over the "safe" threshold, we resize doubling * the number of buckets. */ // 当负载因子达到1以上且没有fork or 负载因子超过5(索引效率极低,立即扩容) if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { // 扩容翻倍 return dictExpand(d, d->ht[0].used*2); } ... }
小结
AOF vs RDB
AOF 可靠性高,效率低
- 优点:数据可靠,丢失较少,持久化过程代价低(aof是一个命令一个命令的进行持久化)。
- 缺点:aof 文件过大,数据恢复慢。
RDB 效率高,可靠性低
- 优点:rdb 文件小,数据恢复速度快
- 缺点:丢失较多,持久化过程代价比较高(记录内存所有数据)。
这就需要我们在可靠性与效率取得平衡,RDB - AOF 混用则是汲取了两者的优点,但对服务器性能要求高。
通常 Redis 主节点不会进行持久化操作,持久化操作主要在从节点进行。因为从节点没有客户端的请求压力,操作系统资源比较充沛。但是若主节点宕机,数据就会丢失,主从不一致。
应用
- MySQL 缓存方案中,redis 不开启持久化,redis 只存储热点数据,数据的依据来源于
MySQL;若某些数据经常访问需要开启持久化,此时可以选择 rdb 持久化方案,也就是允许丢失一段时间数据; - 对数据可靠性要求高,在机器性能,内存也安全 (fork 写时复制 最差的情况下 96G)的情况
下,可以让 redis 同时开启 aof 和 rdb,注意此时不是混合持久化;redis 重启优先从 aof 加载数据,理论上 aof 包含更多最新数据;如果只开启一种,那么使用混合持久化; - 在允许丢失的情况下,亦可采用主redis不持久化(96G 90G),从redis进行持久化(因为从节点没有客户端的请求压力,操作系统资源比较充沛);
- 伪装从库;
数据安全策略
数据安全要考虑两个问题:
- 节点宕机
- 磁盘故障
上述持久化方式只考虑到了节点宕机问题,但若磁盘故障,则无法恢复数据。因此,需要定期将持久化文件拷贝到其他地方。
拷贝持久化文件是安全的。持久化文件一旦被创建, 就不会进行任何修改。 当服务器要创建一个新的持久化文件时, 它先将文件的内容保存在一个临时文件里面, 当临时文件写入完毕时, 程序才使用 rename(2)
原子地用临时文件替换原来的持久化文件。
具体措施
- 创建一个定期任务 (cron job), 每小时将一个 RDB 文件备份到一个文件夹, 并且每天将一个 RDB 文件备份到另一个文件夹。
- 确保快照的备份都带有相应的日期和时间信息, 每次执行定期任务脚本时, 使用 find 命令来删除过期的快照:
- 至少每天一次, 将 RDB 备份到数据中心之外,或者至少是备份到运行 redis 服务器的物理机器之外。