AOF 重写流程与多部分 AOF
AOF 以追加方式记录写命令,时间一长文件会远大于数据集本身,恢复也随之变慢。重写是 AOF 体系的自我瘦身机制,Redis 7 又把它改造成多部分结构。本文拆解重写的触发条件与完整流程,说明 aof-use-rdb-preamble 在其中的角色。
一、AOF 为什么必须重写
对同一个 key 执行 100 次 INCR,AOF 里就有 100 条命令;某个 key 被删除后,它历史上所有写命令仍然躺在文件里。恢复时逐条重放这些冗余命令,既慢又占磁盘。
BGREWRITEAOF 解决这个问题:根据当前内存中的数据状态,生成一份"最小命令集"——每个 key 只保留恢复它所需的最少命令,已删除的 key 彻底消失。重写完成后,文件体积通常只有原来的一小部分。
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
二、自动触发的两个条件
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
auto-aof-rewrite-percentage 100:当前 AOF 体积比上次重写后增大 100% 时触发;auto-aof-rewrite-min-size 64mb:文件小于 64mb 时即使翻倍也不重写,避免小实例反复 fork。
两个条件同时满足才会自动重写。把 percentage 设为 0 可完全关闭自动重写,只靠手动或定时任务触发。另外,如果 BGSAVE 正在执行,重写会被推迟(INFO persistence 里 aof_rewrite_scheduled 置 1),避免两个子进程同时抢 IO 与内存。
Redis 在 serverCron 里(默认每秒 10 次)检查重写条件,判断所需的数据来自 INFO persistence 的这两个字段:
redis-cli INFO persistence | grep -E "aof_base_size|aof_current_size"
# aof_base_size:67108864
# aof_current_size:184549376
aof_base_size 是上次重写完成时记录的体积,aof_current_size 是当前体积,增长率按 (aof_current_size * 100 / aof_base_size) - 100 计算。本例为 175%,超过 auto-aof-rewrite-percentage 100 的阈值,于是触发自动重写。
要注意比较对象是"上次重写后的体积"而不是"上一次检查时的体积",所以文件增长到恰好两倍时比例才是 100。这解释了一个常见现象:写入量平稳的实例,重写会呈现"按倍数阶梯"发生的节奏,而不是均匀分布。若实例长期没有重写(例如百分比设得很大),aof_base_size 会停留在很早的数值上,此时一次自动重写释放的空间可能远超预期。
三、Redis 7 的重写流程
Redis 7 之前,重写需要父进程维护一块重写缓冲区,把期间的新命令先攒起来,子进程结束后再追加到新文件,流程复杂且内存开销大。7.0 改用多部分 AOF 后,这套机制被整体移除:
- 父进程 fork 出子进程,子进程遍历当前内存,把数据集写成一份 base 临时文件(
aof-use-rdb-preamble yes时是 RDB 格式); - 父进程同时打开一个新的 incr 文件,此后的写命令直接追加到这个新文件,不再需要缓冲区中转;
- 子进程写完 base 后通知父进程,父进程原子更新 manifest,把"新 base + 新 incr"登记为当前生效的文件组合;
- 旧的 incr 文件从 manifest 移除后异步删除。
整个过程中除 fork 瞬间外,命令处理不受阻塞;重写期间唯一需要关注的是磁盘——旧文件与新 base 短暂并存,容量要按约两倍 AOF 预留。
四、aof-use-rdb-preamble:base 文件的格式开关
aof-use-rdb-preamble yes
该参数决定 base 文件的编码:yes 时用 RDB 二进制格式,加载走 RDB 解析路径,速度比重放命令快一个量级;no 时 base 也是 RESP 命令文本。两种形态在数据目录里可以直接区分:
ls /var/lib/redis/appendonlydir/
# appendonly.aof.1.base.rdb ← 混合格式(preamble yes)
# appendonly.aof.1.incr.aof
# appendonly.aof.manifest
aof-use-rdb-preamble no 时 base 文件的后缀是 .base.aof,内容可以直接用文本编辑器打开查看。该参数自 Redis 5.0 起默认为 yes,4.0 引入混合持久化时默认 no。
manifest 本身是一份普通文本,记录了当前生效的文件清单,可以直接查看:
cat /var/lib/redis/appendonlydir/appendonly.aof.manifest
# file appendonly.aof.1.base.rdb seq 1 type b
# file appendonly.aof.1.incr.aof seq 1 type i
三列分别是文件名、序号 seq 与类型 type:b 表示 base,i 表示 incr。恢复时 Redis 先读 base 再按 seq 顺序重放所有 incr。这也是重写能安全切换的原因——子进程只需把新 base 用递增的 seq 写入,再由父进程改写 manifest 这一行文本,切换动作足够小,不会出现"写了一半的 manifest"。
五、一次重写期间发生了什么
把几个观测点串起来,可以把重写理解成下面这条时间线。假设实例有 20gb 数据、AOF 已增长到 25gb:
T0:serverCron发现aof_current_size / aof_base_size超过阈值,调用rewriteAppendOnlyFileBackground;T0+毫秒级:父进程fork,页表复制产生短暂停顿,子进程开始遍历内存;T0~T1:父进程继续处理请求,写命令全部追加到新 incr 文件;子进程把 20gb 数据序列化为 base;T1:子进程完成,向父进程发送信号;父进程在下一个事件循环里改写 manifest;T1+:旧 incr 与新 base 之一被删除,aof_base_size更新为新组合的体积。
这条时间线说明两件事。第一,阻塞只发生在 fork 那一下,之后父进程是自由的;真正的风险在于子进程写盘与父进程追加写盘同时发生,磁盘 IO 饱和会让主线程的 write 调用变慢。第二,aof_base_size 的更新发生在 T1+,如果在 T0~T1 之间再执行一次 BGREWRITEAOF,命令会被忽略(aof_rewrite_in_progress 为 1 时直接返回),不会产生第二个子进程。
六、重写期间的观测与控制
redis-cli INFO persistence | grep -E "aof_rewrite|aof_last"
关键字段:aof_rewrite_in_progress 为 1 表示正在重写;aof_last_bgrewrite_status 记录上次结果;aof_last_rewrite_time_sec 是上次耗时,大实例上可能达到数十秒。
no-appendfsync-on-rewrite yes 会在重写(以及 BGSAVE)期间暂停主 AOF 的 fsync,减少磁盘争抢,代价是这期间宕机会丢失更多数据。磁盘性能不足又要求安全的场景,保持默认 no,把预算花在更快的盘上。
常见坑 / 最佳实践
- 大实例把 min-size 调大。 默认 64mb 在几十 GB 的实例上会导致重写极其频繁、反复 fork;建议按数据量调到 512mb 以上,percentage 相应放宽。
- 磁盘按两倍 AOF 预留。 重写期间旧 incr 与新 base 并存,空间不足会让重写失败并留下垃圾文件。
- 不要用重写"释放内存"。
BGREWRITEAOF只压缩日志文件,不会减少 Redis 占用的内存,两者经常被混淆。 - 监控重写耗时与状态。
aof_last_bgrewrite_status非 ok 或重写时间持续变长,往往是磁盘性能退化或写入量暴涨的信号。 - 重写安排在低峰。 即使参数合理,fork 停顿与 IO 压力也存在,可用定时任务在凌晨执行
BGREWRITEAOF。
小结
- AOF 只增不减,重写根据内存现状生成最小命令集,是控制文件体积与恢复时长的唯一手段。
- Redis 7 的多部分 AOF 让新命令直接写入新 incr 文件,移除了重写缓冲区,流程更简单、内存开销更小。
- manifest 是文件组合的登记表,base 与 incr 的搭配随每次重写轮换。
aof-use-rdb-preamble yes让 base 采用 RDB 格式,恢复速度显著更快,5.0 起默认开启。- 重写期间磁盘占用约翻倍,容量预留、状态监控与低峰执行三件事都要落实。
- 点赞
- 收藏
- 关注作者
评论(0)