
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_bufAOF写入缓冲里写数据fork之后主进程除了继续写aof_buf还会把每一条新命令同时追加到server.aof_rewrite_bufAOF重写缓冲。也就是说重写期间产生的所有写入会被同时记到两个地方旧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设为64MBpercentage设为100。第一次重写完成时aof_base_size是80MB之后随着业务写入当前文件涨到160MB此时差值比例是100%触发重写。重写完成后aof_base_size更新为新的体积比如85MB。如果后续写入量不大文件一直没超过170MB就不会再次触发。很多同学配了自动重写但发现它不触发原因多半是这几点min-size设太高比如配成了5GB小实例永远够不到门槛刚做过手动重写base_size被刷新得很小百分比差没到触发线Redis 7.0里如果开启了多部分AOFaof_base_size的含义变成了base文件的大小需要结合manifest看。3.3 fork瞬间为什么会出现毫秒级卡顿重写过程中唯一需要主进程同步等待的地方就是fork。Linux fork需要复制页表页表大小跟进程占用内存RSS成正比。一个内存占用10GB的Redis实例fork复制页表可能要几百毫秒如果机器内存带宽又紧张这个时间还会更长。这就是为什么我从来不建议在业务高峰期手动触发重写——你点一下bgrewriteaof可能就要用几百毫秒的延迟尖刺来买单。缓解办法有三个方向。一是控制单实例内存尽量不超过8~12GBfork开销可控二是把自动重写的阈值调大降低频繁fork的概率三是如果Redis实例确实很大考虑改造为主从架构在从节点上做重写把fork压力转移到从机。第三种方案需要额外验证从节点重写后的文件是否完全一致但思路是成立的。3.4 自动重写不触发的排查思路我之前在另一个项目里被问过一个问题我们Redis配置了自动重写为什么AOF都涨到2GB了还不触发排查链路是这样的第一步执行redis-cli info persistence看aof_base_size和aof_current_size各是多少。如果base_size已经是1.9GBcurrent_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 everysecRedis每秒执行一次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拒绝启动然后人工介入检查备份和做数据恢复而不是默默丢掉尾部。我见过一个案例某团队配置了yesAOF尾部损坏后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体积和重写耗时的告警比背多少理论知识点都管用。先把监控铺好再谈调优这条顺序一定不要反过来。