☰
Redis AOF重写原理与性能调优:从文件膨胀到触发机制
2026/10/6 3:09:09 网站建设 项目流程

1. AOF文件膨胀的真实代价:为什么重写是必然刚需

1.1 AOF的写入链路决定了文件只会越用越大

我在生产环境维护过不少Redis实例,每次看到appendonly.aof的体积数据往上涨,心里都会咯噔一下。AOF的原理很多人张口就来:把每个写操作追加到日志文件末尾。但仔细想想,这个词的重点在"追加"——不做回写、不覆盖、不清空。这意味着每个写命令的完整历史都会永久留在文件里。

比如同一个keyuser:9527,业务侧一天内反复set了500次,AOF里就躺着500条set命令。再比如某个列表,先rpush了10万条数据,后来业务下线又把整个key删了,文件里依然留着那10万条rpush记录。我见过一个极端案例:实例当前数据量其实只有2GB,但AOF文件已经膨胀到18GB——因为上游一个批量导入脚本反复写同一个大集合,每次全量覆盖。

膨胀的本质是:AOF体积跟"累计写操作总量"走,而不是跟"当前数据集大小"走。这就是为什么重写不是可选优化,而是AOF长期运行下去必须解决的问题。

1.2 膨胀带来的代价远不止磁盘多占几GB

先看看磁盘,18GB和2GB差出16GB,如果机器磁盘总容量有限,这一项就够喝一壶。更麻烦的是下面这几个连锁反应:

  • 启动恢复变慢。Redis用AOF恢复数据的逻辑是"按顺序重放所有命令"。文件翻倍,恢复时间基本也翻倍。一次意外重启,业务方可能等十几分钟才能恢复,这在线上是无法接受的。
  • 每个写入周期都更吃力。AOF写入虽然用的是write系统调用追加,但文件越大,涉及的文件元数据、页缓存、伙伴系统层面的成本都会增加,磁盘IO压力也更大。
  • 排查问题更难。文件大了以后,想手动翻AOF确认某个key的历史变更基本不可能,只能靠脚本离线分析。
  • 主从复制也可能被拖累。虽然主从同步走的是RDB和复制流,但如果实例频繁重启,全量同步时RDB或AOF的处理压力会传导到从节点。

一句话总结:AOF膨胀不是"多花点磁盘钱"的小事,它会直接拉低整个Redis集群的可用性。

1.3 重写到底删掉了哪些"垃圾"

初次接触AOF重写的人,很容易把它理解成"拿旧AOF文件做一遍压缩去重"。实际上这是误解,而且这个误解会让你在排查问题时走弯路。AOF重写完全不读旧AOF文件,它是直接读当前内存里的数据,根据现有key和value重新生成一套最小化的写命令集合。

那它到底清掉了什么?我列一下:

  • 同一个key的重复写入只保留最后一条。前面500次set,最后只留一条。
  • 已经过期或已删除的key直接消失。重写时Redis遍历所有数据库,发现key不存在了,自然一条命令都不生成。
  • 多个Redis命令被批量合并。同一个hash的所有字段合并成一条hmset,同一个list的所有元素合并成一条rpush,同一个set的所有成员合并成一条sadd。命令条数从几万降到几百。
  • 带有过期时间的key会重新计算TTL,过期时间点不变,但记录方式更精简。

重写出来的文件体积,理论上约等于"当前数据集的最小完整副本"。数据本身越紧凑、碎片越少,重写后的文件就越小。

提示:正因为重写基于内存快照,所以如果旧AOF里曾经发生过命令覆盖(比如某个key后来被del了),重写后这条key就不会出现在新文件里。这跟"备份还原"的直觉不一样,但逻辑是完全正确的。

2. 重写的执行内核:fork子进程、快照和双缓冲机制

2.1 为什么必须fork子进程来处理

重写整个数据集是个大工程,如果让主进程一条条遍历处理,业务请求全被卡死,这是绝对不可接受的。Redis的解法是fork出一个子进程来干重活。

fork瞬间,子进程获得一份父进程内存页表的副本。父进程继续接收客户端请求、处理命令、写AOF;子进程则拿到fork那一刻数据集的内存镜像,开始遍历生成新AOF文件。由于Linux的写时复制(Copy On Write)机制,父子进程在fork后共享物理内存页,只有父进程后续修改到的内存页才会被复制,因此fork本身的开销大约等于复制页表的时间,而不是复制全部内存的时间。

这里有个很关键的直觉:子进程拿到的是fork时刻的"快照",而不是"最新数据"。重写期间新写入的命令,主进程需要另想办法兜住,这就是下面的双缓冲机制要解决的问题。

2.2 子进程如何生成最小命令集合

子进程遍历完所有数据库后,会对每个key根据类型做序列化输出。如果aof-use-rdb-preamble是开启的(Redis 4.0之后默认开启),子进程会先用RDB格式把当前数据集的核心内容写进临时文件——这一步就像做了一轮紧凑快照,然后才把重写期间差量命令(来自重写缓冲区)以AOF格式追加到文件末尾。Redis 7.0里这个机制更自然,基础文件直接就是RDB格式的内容。

这种"RDB打底 + AOF增量"的混合设计,好处显而易见:RDB格式加载速度远快于逐条重放命令,文件体积也远小于全量命令集。实测下来,同样一份数据,混合格式的AOF加载耗时可以比纯命令格式少一个数量级。代价是格式不再是纯文本,不能再用cat直接看内容,但对运维来说这个代价完全值得。

2.3 双缓冲机制:重写期间的新写入怎么接住

这是AOF重写最容易讲不清楚、也最值得吃透的一块。场景是:fork完成、子进程开始动手之后,Redis还在持续处理线上写入。假设没有额外的缓冲,那么子进程生成的新AOF文件里就不包含这些新命令,重写完成后一切换,数据就丢了。这肯定不行。

Redis的解决办法是双缓冲。在fork子进程之前,主进程往server.aof_buf(AOF写入缓冲)里写数据;fork之后,主进程除了继续写aof_buf,还会把每一条新命令同时追加到server.aof_rewrite_buf(AOF重写缓冲)。也就是说,重写期间产生的所有写入,会被同时记到两个地方:

  • 旧AOF文件继续按原逻辑追加,保证当前持久化状态完好;
  • 新AOF临时文件之外,主进程本地额外攒一份"重写期间增量"。

等子进程完成新文件的生成,主进程会收到一个信号,然后把aof_rewrite_buf里的全部内容追加到临时文件末尾,最后执行原子改名,完成新旧文件切换。

你可能会问:那如果重写期间有大量写入,aof_rewrite_buf会不会撑爆?会。这也是我在监控里特别关注aof_rewrite_buffer_length字段的原因。如果这个缓冲区一直涨得很快,说明写入压力非常大,重写完成时追补的数据量也不小,甚至可能导致持久化延迟。

2.4 文件切换的原子替换与崩溃安全

最后一步替换动作,Redis用的是rename,这是文件系统层面的原子操作。子进程从头到尾写的都是临时文件(比如temp-rewriteaof-bg-xxx.aof),只有等它完整写完、并且主进程把重写缓冲区的数据补进去之后,这个临时文件才会被正式改名为appendonly.aof。

这个设计保证了:即使重写过程中主进程或子进程崩溃,旧AOF文件始终完整可用。崩溃恢复时最多丢失一小段aof_rewrite_buf里的数据,而这一段数据本身就还没写入旧AOF,恢复时旧AOF重放的结果是"重写开始前"的状态,语境上是自洽的。

所以从数据安全角度看,AOF重写不是"边写边覆盖",而是"全部写完再一步切换"。我在帮同事review配置时,经常看到有人担心"重写会不会把正在写的文件写坏",其实完全不会,你可以放心。

3. 触发机制拆解:手动bgrewriteaof与自动阈值的完整链路

3.1 手动触发前后这一秒内部发生了什么

执行bgrewriteaof命令的那一瞬,Redis会先做几个检查:当前是否已经有另一个AOF重写子进程在跑?如果有,直接返回错误,提示Background append only file rewriting already in progress。还会检查是否正在生成RDB快照,如果正在生成,重写请求会被标记为scheduled,等RDB子进程跑完再自动启动。

检查通过后,主进程调用fork创建子进程,然后立即返回字符串Background append only file rewriting started。这里要注意:命令返回"started"不代表重写完成,它只是告诉你子进程已经起来了。很多运维第一次看到这个输出以为完事了,结果一查info persistence发现aof_rewrite_in_progress还是1,就误以为自己搞错了。实际上,重写需要持续十几秒到几分钟都很正常,取决于数据集大小和磁盘速度。

手动触发最适合的场景是:已知接下来有低峰期,或者系统刚部署完、准备让AOF从初始状态就开始保持精简。我习惯在大促前主动做一次重写,把文件压到最小,给后面的写入留出空间和时间。

3.2 auto-aof-rewrite-percentage的计算逻辑

自动重写有两个开关,auto-aof-rewrite-percentage和auto-aof-rewrite-min-size,默认是100和64mb。Redis判定是否需要重写的逻辑是两条同时满足:

  • 当前AOF体积aof_current_size大于等于auto-aof-rewrite-min-size;
  • (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage / 100。

这里有个容易忽略的变量:aof_base_size是上次重写完成之后的基础大小,它其实记录了"上次重写结束时的文件体积"。所以percentage的含义是:相对上次重写的结果,文件又增长了百分之多少。

举一个具体例子。假设你把min-size设为64MB,percentage设为100。第一次重写完成时aof_base_size是80MB,之后随着业务写入,当前文件涨到160MB,此时差值比例是100%,触发重写。重写完成后aof_base_size更新为新的体积(比如85MB)。如果后续写入量不大,文件一直没超过170MB,就不会再次触发。

很多同学配了自动重写但发现"它不触发",原因多半是这几点:

  • min-size设太高,比如配成了5GB,小实例永远够不到门槛;
  • 刚做过手动重写,base_size被刷新得很小,百分比差没到触发线;
  • Redis 7.0里如果开启了多部分AOF,aof_base_size的含义变成了base文件的大小,需要结合manifest看。

3.3 fork瞬间为什么会出现毫秒级卡顿

重写过程中唯一需要主进程"同步等待"的地方就是fork。Linux fork需要复制页表,页表大小跟进程占用内存(RSS)成正比。一个内存占用10GB的Redis实例,fork复制页表可能要几百毫秒;如果机器内存带宽又紧张,这个时间还会更长。这就是为什么我从来不建议在业务高峰期手动触发重写——你点一下bgrewriteaof,可能就要用几百毫秒的延迟尖刺来买单。

缓解办法有三个方向。一是控制单实例内存,尽量不超过8~12GB,fork开销可控;二是把自动重写的阈值调大,降低频繁fork的概率;三是如果Redis实例确实很大,考虑改造为主从架构,在从节点上做重写,把fork压力转移到从机。第三种方案需要额外验证从节点重写后的文件是否完全一致,但思路是成立的。

3.4 自动重写不触发的排查思路

我之前在另一个项目里被问过一个问题:"我们Redis配置了自动重写,为什么AOF都涨到2GB了还不触发?"排查链路是这样的:

第一步,执行redis-cli info persistence,看aof_base_size和aof_current_size各是多少。如果base_size已经是1.9GB,current_size是2GB,那增长比例只有大约5%,离100%差得远,自然不触发。第二步,确认auto-aof-rewrite-min-size配置,如果设了1GB,那算法第二个条件虽然满足,但第一个条件可能不满足。第三步,检查aof_rewrite_scheduled字段是否为1,如果之前有RDB在做,重写一直在排队。第四步,确认appendonly确实是yes,不要只看redis.conf,要看运行时config get appendonly。

这套排查思路基本能覆盖90%的"不触发"场景。剩下10%大概率是版本差异——Redis 7.0的多部分AOF机制里,base文件和增量文件是分离的,aof_current_size的含义不同,必须对着manifest看。

4. 重写期间最容易踩的性能坑与参数调优

4.1 三大阻塞点:fork阻塞、fsync阻塞、write阻塞

重写期间主进程不是完全无感,它有至少三个地方可能出现延迟尖刺。

第一个是fork阻塞,上面已经说过,页表复制耗时跟内存大小强相关。第二个是fsync阻塞,即使配置了appendfsync everysec,Redis每秒执行一次fsync把缓冲刷到磁盘,碰上磁盘性能差或者系统IO繁忙,一次fsync可能卡几十毫秒甚至更久。第三个是write阻塞,主进程写AOF文件要走系统调用,如果磁盘队列塞满,write一样会卡。

真正影响体验的组合场景是:重写子进程正在疯狂写磁盘,同时主进程每秒还要fsync旧AOF,两边抢磁盘带宽。我见过一个现象:开启自动重写后,本来平均延迟0.5ms的实例,重写那几十秒内P99直接飙到几十毫秒。这不是重写逻辑有bug,而是"重写子进程和主进程同时抢同一块磁盘"导致的资源竞争。

4.2 appendfsync与no-appendfsync-on-rewrite的组合方案

这里有个配置值得单独拎出来:no-appendfsync-on-rewrite。默认值是no,翻译成操作行为就是:即使正在重写,主进程依然严格按照appendfsync策略来刷盘。

如果把它设为yes,那么在整个AOF重写期间,主进程会暂停主动fsync,把刷盘动作完全交给操作系统。这个配置的收益和风险都很清晰:

配置组合重写期间延迟断电丢失窗口适用场景
appendfsync everysec + no-appendfsync-on-rewrite no重写时延迟可能升高最多丢1秒默认安全配置
appendfsync everysec + no-appendfsync-on-rewrite yes延迟明显更平稳最多丢1秒,但极端情况下可能更多磁盘性能弱的机器
appendfsync always不允许丢失任何已确认写入几乎不丢对持久化要求极高的场景
appendfsync no延迟最优,操作系统定刷可能丢数秒对丢数据容忍度高的缓存场景

我个人的倾向是:如果没有严格的"每秒最多丢多少数据"的SLA,重写期间把no-appendfsync-on-rewrite设为yes是合算的,因为重写本来就是要花时间做磁盘密集操作,没必要让主进程在这段时间里跟子进程抢fsync。但如果你的业务方明确要求"最多丢1秒以内",那就要保持no,并且要把磁盘性能提前压测好。

4.3 大key与写时复制导致的内存翻倍风险

这是重写相关故障里最容易出大问题的点,但常规资料很少讲透。前面说过fork后父子进程共享物理页,父进程修改的内存页会被复制。问题在于:如果父进程持续修改一个很大的key,比如一个包含百万成员的hash,业务侧高频更新它的局部字段,那么每次更新涉及的hash底层结构页都会触发COW复制。大数据量大key在场时,COW复制的内存量可能远超预期,极端情况下实例RSS会在重写期间接近翻倍。

为了防止这个坑,我在运维规范里会做几件事:

  • 监控重写期间的内存指标,观察RSS曲线有没有异常爬升;
  • 上线大key拆分规范,单个value超过10MB的key一律拆成多个分区key;
  • 如果发现某个大key是重写期间内存飙升的元凶,先跟业务方协调把写入频率降下来,再触发重写。

内存翻倍的后果不只是服务器压力大,还可能是cgroup OOM Kill直接把Redis进程干掉。这个风险必须认真对待。

4.4 一套实测下来比较省心的参数组合

分享一组我在中小型实例(数据量约6GB)上长期使用的组合:

appendonly yes appendfilename "appendonly.aof" appendfsync everysec auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 1gb no-appendfsync-on-rewrite yes aof-use-rdb-preamble yes aof-load-truncated yes

为什么min-size设到1GB而不是默认的64MB?因为小于1GB的AOF文件重写收益不高,频繁小规模重写反而会引入更多fork和磁盘IO。当文件真正涨到GB级别,做一次重写的性价比就很高。percent保持100表示翻倍才重写,这样大部分实例一天最多触发一两次,甚至一两天一次,频率完全可控。

注意:如果你管理的实例写入量极大,比如每秒几万次写,重写缓冲区可能会持续增长。这种情况下建议调高内存水位告警,并在架构层面考虑给Redis开启AOF重写专用的-remove-on-fail等保护机制,尽量避免重写失败。

5. 用日志和INFO字段监控重写健康状况

5.1 INFO persistence里每个关键字段都在告诉你什么

redis-cli info persistence是观察AOF重写的第一入口。我每次排查AOF问题,上来就刷这个命令。核心字段我整理成了一张表:

字段含义重点关注场景
aof_enabledAOF功能是否开启确认开关没被误关
aof_rewrite_in_progress当前是否正在重写长期为1且不结束要警惕
aof_rewrite_scheduled是否有重写在排队RDB子进程拖后腿时会看到
aof_last_rewrite_time_sec上次重写总耗时突然暴涨说明磁盘异常或数据膨胀
aof_current_rewrite_time_sec当前这轮重写已耗时超过last字段多倍即为异常
aof_last_bgrewrite_status上次重写结果非ok要立刻查日志
aof_current_size当前AOF体积跟base_size对比判断触发时机
aof_base_size上次重写后的基础体积重写后会自动更新
aof_rewrite_buffer_length重写缓冲区当前积压字节持续上升是性能隐患
aof_pending_bio_fsync等待后台刷盘的请求数积压说明磁盘IO吃紧
aof_delayed_fsync主进程延迟fsync的次数数值一直涨说明磁盘跟不上

5.2 从日志读懂一次重写的完整生命周期

重写期间Redis的日志会输出几条关键记录,我用一个模拟示例说明:

M ... * Background append only file rewriting started by pid 19431 M ... * Fork CoW for AOF rewrite: 512 MB M ... * Background AOF rewrite finished successfully

第一条是fork前的预告,告诉你子进程pid。第二条是关键:Fork期间写时复制了多少内存。这个数字如果接近实例内存总量,说明父进程在fork后写入了大量页,COW压力大。第三条是成功标志。如果失败,你会看到Background AOF rewrite terminated with error,同时日志里一般会附带具体错误码,比如磁盘已满、权限不对、文件系统只读等。

我还会特别留意中间那句的耗时差异。正常情况下,CoW的数据量跟重写期间实际写入的数据量正相关;但如果相同的写入量下CoW明显变大,就要检查是否出现了大量随机覆盖写,这会让内存页复制成倍增加。

5.3 结合latency监控确认重写没有拖垮延迟

日志只能看到结果,看不到用户请求是否被卡顿。我自己有个习惯:在重写测试或者故障演练时,开一个独立的窗口跑redis-cli --latency,观察实时延迟分布。

实际操作示例:

redis-cli --latency -h 127.0.0.1 -p 6379 -i 1

然后在另一个终端手动触发:

redis-cli bgrewriteaof

观察--latency输出中是否出现明显尖刺。正常情况下,如果实例数据量不大,尖刺就一个点(对应fork),后续保持平稳;如果尖刺持续存在,说明磁盘IO竞争或COW内存复制对主线程产生了持续影响,那时再回头调no-appendfsync-on-rewrite等参数就更有针对性了。

另外可以在配置里开启延迟监控:

latency-monitor-threshold 100

然后通过redis-cli latency history command查看耗时超过100ms的事件分布,确认重写是否上榜。

6. 重写故障的大坑复盘:从卡死到AOF损坏恢复

6.1 磁盘满导致重写失败:排查链路

这是我真实踩过的一个坑。某次自动重写触发后,实例日志开始疯狂报No space left on device,重写子进程反复失败。我当时先看了info persistence,发现aof_last_bgrewrite_status是err,然后翻日志确认是磁盘已满。排查链路其实很直接:

  • 第一步,df -h看磁盘分区使用率,确认是否真的满了;
  • 第二步,du -sh /data/redis/appendonly*看AOF相关文件占用;
  • 第三步,检查临时文件是否残留。重写失败或中断会留下类似temp-rewriteaof-bg-*.aof的文件,占用不少空间。

处理上我是这么干的:先清理掉残留的临时文件,再跟业务方确认是否可以清理旧备份,最后调大auto-aof-rewrite-min-size避免它频繁重写,等磁盘空间稳定后再手动做一次完整重写。

这里提醒一下:AOF重写需要临时磁盘空间大概是"新文件体积 + 旧文件体积"的量级。因为重写完成前新旧文件是同时存在的,所以磁盘剩余空间最好留够旧AOF体积的2倍,否则重写大概率在中途失败。

6.2 AOF文件尾部截断的恢复:redis-check-aof实测

AOF文件因为崩溃或者意外断电,文件尾部可能出现截断或损坏。Redis启动时会做load检查,如果尾部数据不完整,默认策略(aof-load-truncated yes)是忽略尾部错误、只加载完整部分,然后正常启动。

如果aof-load-truncated是no,或者你想手工修复文件,可以用Redis自带的工具:

# 先备份原文件 cp appendonly.aof appendonly.aof.bak # 执行修复,--fix 会自动把损坏/截断的部分截掉 redis-check-aof --fix appendonly.aof

工具执行完成后会告诉你修复了多少字节。但我要强调:修复意味着"丢失损坏之后的那部分数据",不是一个完全无损的操作。所以在修复前一定要确认,你更在意的是"尽快让Redis可用",还是"尽可能找回最后一段数据"。如果磁盘空间充足,可以先把原文件完整备份再修复,给自己留后悔药。

6.3 aof-load-truncated的取舍

这个参数很多人不重视,但它决定了Redis遇到损坏AOF时是"直接罢工"还是"带伤启动"。生产环境的取舍我建议分场景:

  • 如果这台Redis是缓存性质,丢几秒数据无所谓,aof-load-truncated yes是最省心的;
  • 如果是核心数据存储,任何一段数据都不愿意丢,那最好保持no,让Redis拒绝启动,然后人工介入检查备份和做数据恢复,而不是默默丢掉尾部。

我见过一个案例:某团队配置了yes,AOF尾部损坏后Redis启动后静默丢了最后几秒钟的关键订单写入,业务方一直到第二天对账才发现数据对不上。所以这个配置本质上是一个"可用性优先"还是"一致性优先"的选择题,没有绝对正确答案,但一定要是团队里明确讨论过的决定。

6.4 Redis 7.0多部分AOF机制带来的变化

Redis 7.0把AOF从"单一文件"改成了"多个文件+清单文件"的结构。简单说,现在AOF由三类文件组成:

  • base文件:存储基础快照数据,通常是RDB格式;
  • incr文件:存储增量命令日志;
  • manifest清单文件:记录当前有哪些base文件和incr文件、它们的顺序和状态。

AOF重写在这个新结构下的行为是:生成一个新的base文件,同时把旧的incr文件清空或轮替掉,manifest同步更新。好处是文件管理和恢复逻辑更清晰,加载时可以并行处理多个文件,速度更快;代价是排查问题时不能只盯一个appendonly.aof,要去appendonlydir目录下看多个文件。

如果你还在用Redis 6.x,升级到7.0后要注意:老版本的单个AOF文件会被迁移为多部分结构,auto-aof-rewrite-min-size的语义仍然是全局的,但aof_base_size现在指的是base文件的大小,不再是整个AOF目录的体积。网上有些旧教程里的字段解读,放到新版本上会失真。

关于重写这块,我最后想说的一个体会是:AOF重写本身不是越频繁越好,也不是一直不触发就好,它本质上是在"文件体积"和"重写成本"之间找平衡。真正考验运维功力的不是会用bgrewriteaof,而是能根据实例的内存大小、写入吞吐、磁盘性能、业务低峰期,把阈值和监控调到刚好合适。我在实际运维里踩过不少跟磁盘、COW、缓冲区、截断相关的坑之后,最大的心得就是:给每个Redis实例都配上AOF体积和重写耗时的告警,比背多少理论知识点都管用。先把监控铺好,再谈调优,这条顺序一定不要反过来。

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

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

立即咨询