
1. AOF文件膨胀的根源与重写机制的设计初衷1.1 从一次线上事故说起AOF文件是怎么悄悄变大的先讲一个我实际遇到过的场景。某个业务服务用Redis做计数器记录用户每天的行为次数核心操作就是INCR和EXPIRE。上线跑了几个月数据量其实没多大内存才用了2GB但我发现磁盘上AOF文件已经涨到了15GB。最头疼的是每次重启Redis加载这个AOF文件要花将近二十分钟期间服务完全不可用。这就是AOF持久化的典型问题它记录的是操作过程不是最终结果。每执行一次INCR keyAOF里就追加一条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可以设到512MBpercentage设到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时刻forkT1时刻执行了SET name zhangsanT2时刻执行了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迅速变成0aof_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说明重写卡住了。最典型的卡住原因是子进程在生成文件阶段磁盘写入极慢或者临时文件所在磁盘空间不足。排查步骤我一般按这个顺序来看INFO persistence里的aof_last_rewrite_time_sec判断上次成功重写用了多长时间有参照才好判断这次是不是异常。ps -ef | grep redis看那个redis-rdb-bgsave或者redis-aof-rewrite进程是不是还在跑如果进程存在但CPU占用一直为0大概率是在等I/O。用iostat -x 1看磁盘的%util如果接近100%基本就是磁盘触顶了。检查磁盘剩余空间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重写本身不复杂但能不能把它调教得又快又稳拼的就是对细节的观察和对规律的把握。