☰
Redis AOF重写机制全解析:从膨胀原理到性能调优实践
2026/10/1 14:51:03 网站建设 项目流程

1. AOF文件膨胀的根源与重写机制的设计初衷

1.1 从一次线上事故说起:AOF文件是怎么悄悄变大的

先讲一个我实际遇到过的场景。某个业务服务用Redis做计数器,记录用户每天的行为次数,核心操作就是INCR和EXPIRE。上线跑了几个月,数据量其实没多大,内存才用了2GB,但我发现磁盘上AOF文件已经涨到了15GB。最头疼的是每次重启Redis,加载这个AOF文件要花将近二十分钟,期间服务完全不可用。

这就是AOF持久化的典型问题:它记录的是"操作过程",不是"最终结果"。每执行一次INCR key,AOF里就追加一条INCR key的命令。同一把钥匙被加了一万次,AOF里就有一万条命令,而内存里其实只有一个最终值。文件里记录的无效历史操作越来越多,文件体积自然失控。

Redis持久化一共两条路:RDB快照和AOF日志。RDB是定期把内存里的数据整体拍一张"照片",恢复快但可能丢数据;AOF是每步操作都记"流水账",数据安全但文件容易膨胀。AOF文件重写要解决的,就是把这个"流水账"压缩成一份"最终账单"——把一万条INCR合并成一条SET key 10000。

1.2 重写的本质:用一条命令代替一百条命令

理解AOF重写,就要抓住一个核心思想:重写不是修改旧文件,而是从零生成一份新的、体积最小的AOF。Redis会遍历当前内存中的全部键值对,针对每个键生成一条能够重建数据的最简命令。比如一个String类型的键,最终值是10000,重写后只有一条SET key 10000;一个Hash类型的键,哪怕之前有过一千次HSET,重写后也只剩一条HMSET。

这个设计有几个关键优势。第一,重写过程不需要暂停服务,用户请求照常处理,这是AOF重写能落地的前提。第二,新文件只包含当前有效数据,和AOF里堆积了多少历史命令无关。第三,重写期间新产生的写命令会被额外缓冲,不会丢失。

可能有朋友会问,为什么不直接在原文件上做裁剪,非要另起炉灶?答案很简单:直接修改一个正在被持续追加写入的文件,既无法保证原子性,又可能在中间出问题导致整个AOF损坏。另建新文件,最后做一个原子替换,才是最稳妥的方案。理解了这一点,后面看整个重写流程就会很顺。

2. 触发AOF重写的两个途径与配置细节

2.1 手动触发:BGREWRITEAOF命令怎么用最保险

手动触发很简单,在redis-cli里执行一条命令:

> BGREWRITEAOF

命令名字里的BG是background的意思,表示这个动作会在后台异步执行,不会阻塞主线程。Redis会立刻返回Background append only file rewriting started,然后你通过INFO persistence可以看到重写状态。

手动触发适合什么场景?我通常在下面几种情况下会主动来一发:

  • 刚部署完一套新的持久化策略,想让AOF文件一次性瘦身到位。
  • 大版本升级后结构发生变化,比如从老的数据类型迁移到新的编码方式。
  • 监控发现aof_current_size增长曲线明显异常,比如一晚上涨了几倍,想马上压缩。
  • 要做备份迁移,希望先让磁盘上的AOF文件尽可能小,降低传输成本。

有一个细节容易忽略:手动触发时,即使appendonly参数没有开启,执行BGREWRITEAOF也会生效。Redis 7.0之后有个appendonly动态开关的改进,但手动重写始终是一个独立可用的运维动作。我建议手动触发之前先看一眼INFO stats里的aof_rewrites计数,如果上一次重写才过去几十秒,最好等一会儿再触发,避免无意义的fork开销。

2.2 自动触发:两个参数如何配合

自动触发主要看两个参数,默认配置是这样的:

auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb

这两个参数什么含义?用大白话说:AOF文件相比上一次重写后的基准大小增长了100%,而且当前文件已经超过64MB,就自动触发下一次重写。

auto-aof-rewrite-percentage说的是百分比,倍数关系;auto-aof-rewrite-min-size说的是绝对下限,防止文件还很小的时候就频繁重写。很多入门资料只解释参数意思,不讲怎么配合,导致有人把percentage调到10、把min-size调到1mb,结果高负载下每几分钟就重写一次,把磁盘I/O和CPU都打爆了。

我建议按这个思路去取值。先观察正常业务下AOF文件的日均增长量,假设一天涨300MB。如果希望一天只重写一次,那min-size可以设到512MB,percentage设到200,也就是文件涨到基准大小的两倍才重写。如果业务写入波动大,不想等太久,min-size设128MB、percentage设100,折中方案。下面这张表是我常用的几组配置对照:

业务写入特点auto-aof-rewrite-percentageauto-aof-rewrite-min-size预期触发频率
写入平稳、内存数据量稳定100256mb每天1-2次
写入有洪峰、波动大200512mb每天1次或更少
写入频繁、实时性要求高5064mb每几小时一次
测试环境、无所谓开销101mb高频测试

每次自动重写完成后,Redis会把当前AOF文件大小记录为新的基准值,下一次增长就基于这个新基准重新计算。如果你手动执行BGREWRITEAOF,它也会重置基准值。另外,如果AOF文件因为这个那个原因被清空或重建,Redis会把aof_rewrite_base_size重新设为当前大小,判断逻辑不会出乱子。

需要特别提醒:auto-aof-rewrite-min-size这个门槛在Redis 7.0之后有细微变化。7.0引入了Multi Part AOF机制,AOF文件被拆成多个分片,aof_rewrite_base_size的统计口径是总的逻辑大小,而不是单个文件的物理大小。实际排查时如果发现文件明明不到64MB却触发了重写,别慌,先查看INFO persistence里的aof_base_size字段,以它为准。

3. 重写流程的完整运作步骤与关键细节

3.1 从fork到替换:重写全程的四阶段拆解

AOF重写虽然一条命令就触发,但背后是一整套精细的动作序列。我把它拆成四个阶段讲,方便记忆也方便排错。

阶段一:fork子进程。Redis主进程调用fork()创建出一个子进程,子进程会复制一份主进程的页表,也就是拿到一份内存数据的快照视图。这就是"重写期间的AOF基于fork时的内存状态"这句话的由来。这里有个关键的知识点:fork本身很轻,但如果内存里有很多脏页,或者操作系统内存不足,fork的耗时可能从几毫秒变成几百毫秒,极端情况下会阻塞主线程。所以监控里看到latest_fork_usec数值飙升,就要意识到重写对主进程产生影响的可能性。

阶段二:子进程生成新AOF文件。子进程遍历内存中的每个数据库、每个键,按照数据类型的最简编码方式生成命令,写入一个临时文件。这个阶段完全不碰主进程的原始AOF文件,所以主进程可以继续正常追加写操作。子进程生成的临时文件命名通常带有temp-rewriteaof-bg-<pid>.aof这样的标记,在Redis配置的AOF目录下能看到。

阶段三:主进程缓冲增量写命令。从fork那一刻起,主进程把新收到的所有写命令同时追加到两块地方:一块是旧的AOF文件,保证旧文件持续完整;另一块是AOF重写缓冲区,这块内存专门为这次重写服务,记录fork之后产生的所有写操作,确保子进程生成的新文件不会缺少这些增量。

阶段四:子进程完成并通知主进程。子进程写完临时文件后向主进程发送信号。主进程收到信号后,把重写缓冲区里的增量命令追写到临时文件末尾,然后调用fsync确保数据落盘,最后用rename原子性地把临时文件替换成正式的AOF文件,同时关闭旧的AOF文件句柄,开始往新文件追加写操作。

3.2 重写缓冲的"双写"机制:为什么重写期间数据不会丢

这个"双写"机制是AOF重写最精华的地方,很多人面试或者理解时栽在这里。简单说,fork之后,新产生的写命令走两个管道:旧文件管道和新缓冲管道。

假设时间线是这样:T0时刻fork,T1时刻执行了SET name zhangsan,T2时刻执行了DEL name。子进程在T0之后读取的内存状态里还没有这两个操作,所以它生成的临时文件不包含它们。如果主进程只往旧AOF文件里写,不往临时文件里补,最终替换完成后这两个操作就丢了。

所以Redis专门开辟了aof_rewrite_buf缓冲区。从T0到子进程完成之间,所有写命令都会进这个缓冲区。当子进程完成临时文件生成,主进程把缓冲区里的命令一股脑追加到临时文件末尾,这样新文件就包含了T0之后的所有命令,一笔不漏。

有人问,为什么不在fork之后直接停止接收写命令,等重写完再恢复?那样当然简单,但服务会中断,与AOF重写"不打断服务"的设计目标相悖。所以缓冲区的存在,本质上是拿内存换性能、拿空间换正确性。缓冲区大小如果长期居高不下,说明重写期间写入量特别大,后面第五章我会讲怎么处理这个隐患。

3.3 重写收尾的原子替换:一个rename解决所有一致性问题

阶段四里最关键的原子操作就是rename(2)。当临时文件被重命名为正式的AOF文件名时,操作系统保证了这一动作的原子性——要么旧文件继续生效,要么新文件立刻生效,绝对不会出现两个文件同时存在或者文件内容混乱的中间态。

这里要提醒一个容易踩的坑:执行rename之后,主进程不会立刻把写句柄从旧文件切换到新文件,而是先完成缓冲区数据追加,再切换。如果期间发生了崩溃,重启时Redis会加载哪一个文件?答案是新文件,因为rename已经是最终结果,旧文件已经被替换掉了。Redis启动时如果发现AOF文件不完整,还会用redis-check-aof逻辑尝试修复,但这些都属于异常恢复路径,正常情况下新文件是完整且可用的。

我在一次生产实践里观察过,重写完成瞬间的INFO persistence里aof_rewrite_in_progress会从1迅速变成0,aof_current_size会大幅下降,同时aof_last_rewrite_time_sec显示本次重写耗时。这些指标加起来,就是判断重写是否健康完成的完整证据链。

4. 重写过程中的性能开销与调优策略

4.1 内存和CPU开销怎么评估:别让重写拖垮主服务

AOF重写能后台执行,但代价并不是零。主要开销集中在三块:

第一块是fork产生的内存压力。Linux的fork采用写时复制技术,主进程内存页只有在被修改时才会复制。所以重写期间主进程写的命令越多,复制的内存页越多,内存瞬时占用会上升。如果Redis本身已经占了机器80%以上的内存,再叠加fork带来的页复制,就可能触发swap或者OOM。我的经验值:内存占用超过机器物理内存60%时,要谨慎对待自动重写,必要时错峰执行。

第二块是子进程遍历内存的CPU开销。子进程需要完整扫描所有键,生成命令并写临时文件。数据量大时,这个遍历过程对CPU的占用是实实在在的。虽然子进程不和主进程共享执行线程,但CPU总资源有限,重写期间主进程的读写延迟出现轻微波动是正常现象。

第三块是磁盘I/O。生成新文件和写缓冲都会消耗磁盘带宽。如果Redis所在磁盘本身还承担着RDB快照的写入任务,两个重型操作叠加,磁盘很可能成为瓶颈。

有一个我反复用的监控组合,分享出来:fork耗时、aof_last_rewrite_time_sec、aof_rewrite_buffer_length、instantaneous_ops_per_sec。前三个从INFO persistence里直接拿,第四个从INFO stats里看。如果重写期间瞬时QPS下降超过20%,说明fork或磁盘影响了主流程,需要从配置层面做调整。

4.2 三个关键参数:从fsync到no-appendfsync的取舍

Redis配置里有几个参数直接决定重写期间的行为,改对了能有效降低风险。

aof-rewrite-incremental-fsync,默认值是yes。这个参数控制子进程生成新AOF文件时,是否每写入32MB数据就执行一次fsync。与它相对的是一次性在文件末尾做一次大fsync。增量fsync的优点是避免在最后阶段一次性flush大量脏数据导致的长阻塞,缺点是多几次fsync调用,稍微增加I/O次数。默认值yes不用动,除非你的磁盘天生对fsync过敏。

no-appendfsync-on-rewrite,默认no。这个参数很有意思,它控制重写期间主进程往旧AOF文件执行fsync的时机。注意,主进程在重写期间仍然在往旧AOF文件追加数据。如果这个参数设成yes,意味着重写期间主进程跳过每次写操作后的fsync,改为由操作系统决定何时落盘,这样能大幅降低重写期间的磁盘同步压力。但代价是,如果这段时间机器崩溃,最多可能丢失最后一次fsync之前的所有AOF追加内容。我的建议是:appendfsync配置为everysec时,no-appendfsync-on-rewrite可以设yes;如果appendfsync是always,千万别动这个参数,否则崩溃丢失数据的窗口会扩大到秒级。

aof-load-truncated,默认yes。这个参数说的是启动加载AOF时发现文件末尾不完整(比如正好碰到断电),Redis会忽略尾部不完整数据并正常启动。重写过程中如果异常崩溃,临时文件可能不完整,这个参数负责在下次启动时兜底。生产环境我建议保留yes,配合日志告警人工判断即可。

4.3 与RDB快照同时运行会怎样

有些运维同学习惯同时开启RDB和AOF,两者并行触发时会互相抢资源。RDB快照的fork本质上和AOF重写的fork是同一套机制,同一时刻两个fork叠加,内存和CPU压力直接翻倍。

虽然没有硬性机制禁止两者同时执行,但很值得在运维层面错开。我见过一种情况:定时任务每整点触发一次RDB的BGSAVE,而AOF自动重写恰好也在同一分钟内触发,结果fork耗时从几毫秒涨到200多毫秒,主进程命令延迟从0.2ms飙到3ms,线上出现明显毛刺。

解法也很直接:在RDB定时任务脚本里,先通过INFO persistence检查aof_rewrite_in_progress,如果正在重写则跳过本次RDB;反过来,在触发BGREWRITEAOF前检查rdb_bgsave_in_progress。两个重型fork错开哪怕几十秒,对服务稳定性的帮助都很明显。

5. 常见问题与排查技巧实录

5.1 aof_rewrite_in_progress长期为1怎么办

正常情况下,一次重写少则几秒,多则几十秒。如果你发现INFO persistence里的aof_rewrite_in_progress长时间显示1,说明重写卡住了。最典型的卡住原因是子进程在生成文件阶段磁盘写入极慢,或者临时文件所在磁盘空间不足。

排查步骤我一般按这个顺序来:

  1. 看INFO persistence里的aof_last_rewrite_time_sec,判断上次成功重写用了多长时间,有参照才好判断这次是不是异常。
  2. ps -ef | grep redis看那个redis-rdb-bgsave或者redis-aof-rewrite进程是不是还在跑,如果进程存在但CPU占用一直为0,大概率是在等I/O。
  3. 用iostat -x 1看磁盘的%util,如果接近100%,基本就是磁盘触顶了。
  4. 检查磁盘剩余空间:df -h,临时文件生成的过程如果磁盘满了,子进程会长期处于写不进去的状态。

找到方向后针对处理。磁盘满就清理磁盘;磁盘慢就考虑把AOF临时文件目录放到更快的磁盘上。临时文件默认和AOF文件在同一个目录,如果当前目录所在磁盘性能差,可以通过dir配置指向一块SSD。改动dir要重启生效,生产环境建议提前规划好目录。

5.2 重写后磁盘占用没降下来,优先级最高的三个排查点

有一次重写完成后,我发现aof_current_size显示从10GB降到了1GB,但磁盘上的AOF文件实际还是10GB。这就很迷惑了。后来排查发现原来主进程还在向旧文件的句柄继续写入,而新文件的替换由于某些原因没有真正完成,系统里同时存在两个AOF相关的文件。

这个现象最常出现在文件系统层面。rename替换顺利完成时,旧文件会被释放。但如果你用了奇怪的目录挂载,或者旧文件被某个进程占用没释放,就可能出现新文件已生成、旧文件还占着空间的情况。排查方法很简单:ls -lh看AOF目录下面的文件名和大小,查看是否有temp-rewriteaof-*这样的残留文件;如果发现正式AOF大小和INFO persistence里报告的aof_current_size不一致,先看是不是有多个AOF文件、有没有被硬链接占用。

另一个重要原因是Redis 7.0以后的Multi Part AOF机制。老版本的AOF只有一个appendonly.aof,新版本会拆成appendonly.aof.1.base.rdb、appendonly.aof.1.incr.aof这样的多个分片。虽然逻辑上是一体的,但物理文件会同时存在。重写后看到的aof_current_size是所有分片的总逻辑大小,而ls -lh看到的是多个文件各自的物理大小。理解这个差异,就不会被数字不一致吓到。

5.3 频繁自动重写如何止损

自动重写触发过于频繁,通常有两个原因。第一个是auto-aof-rewrite-min-size设得太小,比如小于实际数据量,那么每写入一点点数据就超过100%增长,于是不断重写。第二个原因是业务写入模式特殊——大量键在短时间被更新,比如秒杀场景下多个热键被高频SET,每次重写之后数据还是有大量变更,很快又到了触发线。

止损思路有两种。一种是把auto-aof-rewrite-percentage调大,比如从100调到300,让触发频率降下来;另一种是把auto-aof-rewrite-min-size调大,比如从64MB调到1GB,让重写只在高水位时发生。两个方向都行,实际我建议优先调min-size,它对触发频率的控制更直观,也不容易让文件长时间不重写。

还有一种更精细的策略:如果你知道热键集中在哪里,可以把这些热键拆到独立的Redis实例上,让AOF重写只发生在小数据量的实例上,这样每次重写都很快,触发频率再高也能接受。这属于架构层面的优化,比较适合数据隔离明确、容量规划清晰的团队。

5.4 一个容易被忽略的坑:重写缓冲过大导致的内存暴涨

前面说到重写期间主进程会把新写命令缓冲到aof_rewrite_buf,如果这个缓冲增长失控,内存占用会直线上升。我在一次大促复盘时看到过,aof_rewrite_buffer_length一度达到好几百MB,直接把实例内存推向峰值。

为什么会这样?因为重写期间恰好是大促写入高峰,子进程还没写完新文件,主进程积压的增量命令越来越多。此时INFO persistence里的aof_rewrite_buffer_length数值可以看作是缓冲队列的积压量。

碰到这种情况怎么办?优先看写入趋势,如果高峰期只持续几分钟,一般能扛过去。如果缓冲增长没有收敛迹象,我建议主动干预:先临时调高auto-aof-rewrite-min-size防止下一次自动触发,然后手动执行一次BGREWRITEAOF把当前文件重写一遍,清掉积压的缓冲。之所以这样处理,是因为重写结束后缓冲会自然归零,新的基线也同步变小,相当于给AOF系统做了一次"重置"。

另外,可以观察aof_pending_bio_fsync和aof_delayed_fsync两个计数,前者表示等待后台fsync的字节数,后者表示因为fsync延迟导致的写入阻塞次数。两者如果都在上涨,说明磁盘同步已经跟不上写入速度,需要尽快检查磁盘I/O或者调低写入压力。

最后再分享一个我自己的习惯

我在日常运维里看AOF重写是不是干得漂亮,从来不看单次指标,我维护了一张简单的趋势表。每天固定时间记录aof_current_size、aof_base_size、aof_rewrite_buffer_length、aof_last_rewrite_time_sec,连续记录一到两周之后,这套数据会清楚告诉你写入增长规律、重写频率是否合理、是否存在异常的缓冲堆积。

比如你会发现某个业务每天凌晨有定时任务大量写数据,那AOF重写多半也在那个时段触发,于是你就有意识地把自动重写参数调得宽松一点,或者在定时任务脚本里主动避开这个时段。这种基于时序数据的判断,远比临时看到一次报警就瞎调参数靠谱得多。AOF重写本身不复杂,但能不能把它调教得又快又稳,拼的就是对细节的观察和对规律的把握。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询