Administrator
发布于 2019-04-26 / 3413 阅读
69

Redis 持久化 RDB 与 AOF 该怎么选

宿主机重启,Redis 丢了 5 分钟的数据

4 月 20 号凌晨,云厂商的一台宿主机异常重启,我们的 Redis 主节点正好在上面。进程恢复之后,运营来反馈:03:12 到 03:17 之间领取的优惠券,用户端显示"未领取",但业务库里记录是有的。

核对下来丢了 2347 条。我们 Redis 5.0.4 只开了 RDB,当时我还不太理解"丢了 5 分钟"这个数字是怎么来的,查了一遍配置才明白。

为什么会丢:RDB 是定时快照

RDB 的思路是"把某一刻内存里的数据完整地拍一张照片存成二进制文件"。触发方式有三种:

  • 配置文件里的 save 规则(定时 + 变更次数)
  • 手动执行 SAVE(阻塞主线程)或 BGSAVE(fork 子进程)
  • 主从复制时主节点自动触发

我们的配置是 Redis 的默认模板:

save 900 1          # 900 秒内至少 1 个 key 变化
save 300 10         # 300 秒内至少 10 个 key 变化
save 60 10000       # 60 秒内至少 10000 个 key 变化
dbfilename dump.rdb
dir /data/redis

翻事故时的 Redis 日志:

2186:M 20 Apr 2019 03:12:41.107 * 10000 changes in 60 seconds. Saving...
2186:M 20 Apr 2019 03:12:41.112 * Background saving started by pid 31842
31842:C 20 Apr 2019 03:12:44.883 * DB saved on disk
2186:M 20 Apr 2019 03:12:44.951 * Background saving terminated with success
# 03:17:12 进程被杀,中间再没有任何保存记录

最后一次落盘是 03:12:44,进程 03:17:12 挂掉,中间这 4 分 28 秒的写入全部在内存里,没来得及写盘。RDB 的丢失窗口就是"两次快照之间的时间间隔",配置里最短的 save 60 10000 意味着最坏情况下丢一分钟的数据,我们运气不好,正好赶上写入量没到 10000 的阈值,规则一直没被触发。

RDB 还有个容易被忽略的开销:BGSAVE 要 fork 子进程,用的 copy-on-write 机制。子进程共享父进程的内存页,父进程修改某个页时才复制一份。理论上复制量不大,但 fork 系统调用本身在内存大时耗时可观。用这个命令能看最近一次 fork 的耗时(微秒):

127.0.0.1:6379> INFO stats | grep latest_fork_usec
latest_fork_usec:124318

我们 Redis 内存 8.2G,fork 花了 124 毫秒。这 124 毫秒里主线程是阻塞的,P99 延迟会有一个明显的尖刺。这也是为什么不建议把 save 配得太密。

AOF:把写命令记下来

AOF(Append Only File)的思路完全不同:不存数据快照,而是把每一条会修改数据的命令以文本协议格式追加到文件末尾。恢复的时候,把文件里的命令从头到尾重放一遍。

看一眼 AOF 文件的内容,就是 Redis 协议本身:

$ redis-cli set coupon:1001:user:88 1
$ tail -5 appendonly.aof
*3
$3
SET
$22
coupon:1001:user:88
$1
1

AOF 的写入分两步:命令先写进 aof_buf 缓冲区,再由 flushAppendOnlyFile 决定什么时候 write 到文件、什么时候 fsync 到磁盘。真正决定丢多少数据的是 appendfsync 这个参数:

appendfsync行为丢失窗口适用场景
always每条命令都 fsync只丢当前这条金融级,性能很差
everysec(默认)后台线程每秒 fsync 一次最多 1 秒绝大多数场景
no只 write,由操作系统决定 fsync 时机最多一个页缓存周期,通常 30 秒不推荐

everysec 还有个细节:如果后台 fsync 超过 2 秒还没完成,Redis 主线程会阻塞等待(因为它怕继续写会让缓冲区膨胀得更厉害)。这个在磁盘 IO 打满的时候会发生,我们监控过一次,当时另一个进程在做大量日志压缩。

实测:三种配置的性能差别

在测试机上用 redis-benchmark 跑了对比(8 核 16G,SSD,50 并发,10 万次请求):

redis-benchmark -n 100000 -c 50 -t set,get -q
配置SET QPSGET QPSP99 延迟
完全关闭持久化118,300121,9000.42 ms
RDB(save 60 10000)96,100105,2001.15 ms
AOF everysec88,700101,4001.32 ms
AOF always12,50097,8008.24 ms

always 的 SET 性能掉到十分之一,因为每条命令都要等一次磁盘 fsync。SSD 上单次 fsync 大约 0.08ms,看起来不多,但它把 Redis 的单线程优势全废了。我们没用这个模式。

从 RDB 换成 AOF everysec,SET 吞吐大概掉 8%,换来的是丢失窗口从"最多 60 秒"变成"最多 1 秒"。这笔交易我认为非常划算。

混合持久化

AOF 有个明显缺点:文件会一直变大。同样是 8000 万个 key,RDB 是压缩二进制,AOF 是命令文本,能差好几倍。而且 Redis 重启时重放 AOF 特别慢。

我们测过 8GB 数据集的启动加载时间:

方式文件大小加载耗时
RDB2.1 GB42 秒
纯 AOF7.4 GB3 分 20 秒
混合持久化2.3 GB55 秒

Redis 4.0 引入的混合持久化把两者的优点合在了一起:执行 BGREWRITEAOF 时,新的 AOF 文件前半段是 RDB 格式的二进制数据,后面追加的是重写期间的增量命令。文件内容长这样:

$ head -c 100 appendonly.aof | xxd | head -3
00000000: 5245 4449 5330 3030 39fa 0972 6564 6973  REDIS0009..redis
00000010: 2d76 6572 0535 2e30 2e34 fa0a 7265 6469  -ver.5.0.4..redi
...
# 前半段是 RDB,往后才是 *3\r\n$3\r\nSET\r\n 这种命令格式

开启只需要一行,Redis 5.0 里已经默认打开了:

appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
auto-aof-rewrite-percentage 100      # 比上次重写后大 100% 就触发
auto-aof-rewrite-min-size 64mb       # 且不小于 64MB
aof-use-rdb-preamble yes             # 混合持久化,4.0+ 支持,5.0 默认开启

我们最终的主节点配置就是这个。恢复速度接近 RDB,数据安全接近 AOF。

AOF 的日常运维

几个实际会用到的命令和操作。

手动触发重写(别用 BGREWRITEAOF 之外的方式):

127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started

如果 AOF 文件在写入过程中机器宕机,最后一条命令可能只写了一半,Redis 启动时会报错拒绝加载:

Bad file format reading the append only file: make a backup, then use
./redis-check-aof --fix <filename>

按提示修复,它会扫描文件并截断最后那条不完整的命令:

$ redis-check-aof --fix appendonly.aof
0x         1a4f3c2: Expected prefix '*', got: '$'
AOF analyzed: size=27483921, ok_up_to=27483885, diff=36
This will shrink the AOF from 27483921 bytes to 27483885 bytes
Continue? [y/N]: y
Successfully truncated AOF

最多丢最后一条命令,可以接受。修复前一定先备份一份 AOF 文件,我有同事直接跑 fix 结果把文件搞坏了,还好之前做了 cp

我们最后的方案

事故之后做了这几件事:

  • 主节点开 AOF(everysec + 混合持久化),从节点保留 RDB 用于做冷备和快速拉起
  • latest_fork_usecaof_delayed_fsync(fsync 被延迟的次数)加进了监控,超过阈值告警
  • 优惠券领取这种"不能丢"的数据,改成先写 MySQL 再同步到 Redis,用 Canal 订阅 binlog 补偿,Redis 只当缓存用
  • 写了个每月一次的恢复演练脚本,把 RDB 拉到测试机恢复一遍,确认备份是有效的

最后一条是 DBA 提醒我的:备份不做恢复演练,等于没有备份。我们第一次演练就发现,其中一个从节点的 RDB 文件三周前就停止更新了,磁盘满了没人知道。

小结

  • RDB 的丢失窗口是"两次快照之间",AOF everysec 的丢失窗口是 1 秒。前者适合做冷备和快速恢复,后者保证数据安全。
  • 不要二选一,开混合持久化(aof-use-rdb-preamble yes)两个都要:恢复速度接近 RDB,数据安全接近 AOF。
  • appendfsync always 会让写性能掉到十分之一,除非业务真的不能丢一条数据,否则用 everysec。
  • 持久化不是备份。RDB 文件要定期拉到别的机器,还要定期演练恢复。

最后补一句,我们这次丢数据还暴露出另一个问题:主从之间是异步复制,主节点挂掉时如果数据没同步到从节点,即使从节点有 RDB 也救不回来。持久化解决的是"进程重启丢数据",复制解决的是"机器挂了丢数据",两件事要分开考虑。

参考