ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Redis混合持久化深度拆解:RDB前缀+AOF增量的恢复机制与实战

Redis混合持久化深度拆解:RDB前缀+AOF增量的恢复机制与实战 先从一个实际场景说起。我之前维护过一套 Redis 集群数据量到了十几个 G业务端一直走的纯 AOF 持久化。有一回机房断电后重启Redis 在那安安静静地重放了大概二十多分钟的写命令期间所有请求全部超时值班同事急得直找我。后来我把持久化方案改成混合持久化同样是断电恢复重启时间从二十分钟缩短到一分钟以内。这个差异的关键就在于 Redis 加载的持久化文件结构不一样了——AOF 文件前半段是 RDB 快照后半段才是增量命令。很多朋友听说过“混合持久化”这个词也知道大概是把 RDB 和 AOF 拼在一起但问到“Redis 到底怎么识别这个文件、怎么把两种格式衔接起来”能讲清楚的人其实不多。这篇文章我就从混合持久化文件的组织结构入手把前段 RDB、后段 AOF 的边界、Redis 的启动加载流程、我实际验证过的一些手段以及这几年踩过的坑完整拆一遍。1. 什么场景逼出了混合持久化先复盘 RDB 和 AOF 两个老方案1.1 纯 RDB数据全量加载快但丢数据的窗口太大先梳理一下背景。Redis 最早只有 RDB 持久化它做的事情很简单定期把内存里的全量数据做一次二进制快照写到 dump.rdb 文件里。这东西最大的优点是加载速度快因为文件里存的就是内存数据结构序列化之后的结果Redis 启动时只需要把这份快照反序列化回内存就行。对一个大几 G 的实例来说RDB 恢复通常几十秒内可以完成。缺点也很明显RDB 的触发方式是“快照”两次快照之间如果宕机了中间所有写入的数据就全部丢失。默认配置是 900 秒内至少 1 次写操作、300 秒内至少 10 次、60 秒内至少 10000 次才触发一次快照某些流量小的业务或者还没到触发阈值的时候最坏情况下可能要丢十几分钟的数据。这在很多核心业务里是根本没法接受的。1.2 纯 AOF命令攒着不丢但重放命令慢、文件容易膨胀为了缓解丢数据的问题Redis 后来加入了 AOF。它的思路是把每一条写命令以 RESP 协议格式追加到 appendonly.aof 文件末尾。只要配置了 appendfsync everysec最多只会丢 1 秒的数据。理论上数据安全度比 RDB 高很多。可是纯 AOF 有个致命的毛病启动恢复时要逐条重放命令。假如线上实例写入了 5000 万条写命令恢复过程就是把这几千万条命令一条一条重新执行一遍。我当时遇到过线上 Redis 数据量只有几百 G其实内存才十几个 G但纯 AOF 文件因为历史积累能膨胀到几十 G重启一次的时间从十几分钟到半小时不等。另一个问题是 AOF 文件会无限增长如果长时间不重写文件体积会远超实际数据量对磁盘空间也是压力。1.3 混合方案的设计逻辑用 RDB 当 AOF 的“快进前缀”纯 RDB 加载快但丢数据多纯 AOF 丢数据少但加载慢能不能把两者结合这就是 Redis 4.0 引入的混合持久化。核心思路很简单做一次 AOF 重写时不再把所有历史命令都写到新文件里而是先以 RDB 格式把当前内存中的全量数据写进去等这份 RDB 数据流写完再把重写期间产生的增量写命令以传统 AOF 命令流的形式继续追加到后面。这样生成的文件就叫“混合持久化文件”它的结构就是标题里说的「前半段 RDB后半段命令」。你可以这么理解RDB 段相当于一张带全部数据的“底表”AOF 段相当于“底表之后的新增变更”。恢复时先快速加载底表再顺着变更日志追平最后一段速度和可靠性两头都兼顾。这个设计后来也成为 Redis 7.0 多部件 AOF 的雏形思想。2. 混合持久化文件长什么样前半段RDB和后半段命令的边界在哪2.1 文件开头的 REDIS 魔数是整个判断的分水岭要理解 Redis 怎么加载混合文件首先得知道它在文件开头放了什么东西。任何一个正常生成的 RDB 格式数据流文件头一定是 5 个 ASCII 字符REDIS紧接着是 4 位数字字符的 RDB 版本号比如 Redis 7.x 常见的是REDIS0011。这个REDIS就是 RDB 格式的魔数签名类似 ELF 文件开头的\x7fELF、PNG 图片开头的\x89PNG。混合持久化文件的前半段就是一段完整的 RDB 数据流所以整个混合文件的第 1 个字节同样是R。Redis 启动加载 AOF 文件时只要读一眼前 5 个字节是不是REDIS就能立刻判断出这个文件到底是纯 AOF 命令文件还是混合持久化文件。这个判断在源码里非常直接不是REDIS开头就按传统 AOF 命令流逐条解析是REDIS开头就切到 RDB 加载流程。理解了这一点后面加载流程就很好懂了。2.2 前半段 RDB 的组成前半段 RDB 部分和普通 dump.rdb 文件的内部结构没有本质区别都是由几个区段拼接而成。文件头部是刚才说的魔数 RDB 版本号比如REDIS0011。紧接着是辅助字段区里面会记录一些元信息比如 Redis 版本号、当前进程 ID、RDB 文件占用的内存大小、最后一次保存的写命令条数等。然后就是真正的数据区Redis 实例里每个非空数据库会先有个数据库选择标记后面跟着这个库里的所有键值对。每个键值对不只是存 key 和 value还会带上对应的数据类型、过期时间、LRU 信息等元数据。数据写完以后RDB 数据流会以一个专门的 EOF 标记收尾最后是 8 字节 CRC64 校验和用来保证从文件头到 EOF 这一段数据没有损坏。所以混合文件的前半段其实是一段“有头有尾、自带校验”的完整 RDB 流。很多人在分析文件时以为 RDB 段只是简单地把键值对二进制堆到文件前面实际上这段数据是封闭的有明确的结束标记和校验信息这为后面加载器确定切换位置提供了便利。2.3 后半段 AOF 命令流从哪个位置开始关键来了混合文件的 AOF 部分从哪里开始答案就藏在 RDB 段的结束标记后面。加载 RDB 段时只要读到 EOF 标记并完成 CRC 校验这一段的使命就完成了。文件指针继续往下读剩余的内容全部都是传统 AOF 命令流。后半段的命令流和纯 AOF 文件里的写法完全一样每一条写命令都是 RESP 协议格式。我举一个例子比如执行SET username zhangsan在 AOF 文件里会变成这样*3\r\n$3\r\nSET\r\n$8\r\nusername\r\n$8\r\nzhangsan\r\n人眼直接看是带\r\n的文本本质上就是*3表示后面有三个命令参数$3表示接下来的字符串长度是 3 个字节然后是SET本身以此类推。后半段里还可能夹杂着SELECT指令用来切换这次重放要写入的数据库编号保证多库场景下命令能够落到正确的库。也正因为后半段是可读文本协议你用strings命令查看混合持久化文件时通常能看到一串以SELECT、SET、DEL、EXPIRE等开头的命令文本这些就是前半段 RDB 结束之后追加进来的增量命令。2.4 配置参数与重写触发混合持久化不是 Redis 默认开启的需要同时满足两个条件。appendonly yes aof-use-rdb-preamble yesappendonly yes是开启 AOF 持久化的总开关aof-use-rdb-preamble yes是开启“在 AOF 重写时写入 RDB 前缀”的开关。需要注意aof-use-rdb-preamble不是立刻改变现有 AOF 文件格式而是要等到下一次 AOF 重写BGREWRITEAOF才会生成混合格式文件。具体触发的场景包括手动执行BGREWRITEAOF、AOF 文件体积超过auto-aof-rewrite-percentage和auto-aof-rewrite-min-size的阈值、主从全量同步时从节点触发重新生成文件等。这里有一个很多人忽视的点只要没触发重写当前正在使用的 appendonly.aof 还是老的纯命令格式也没关系Redis 启动加载时能在同一个逻辑里兼容两种形态。后面我会写加载流程逻辑上就清楚了。3. Redis 加载混合文件的全过程从读魔数到命令重放3.1 启动加载入口为什么只有一个文件也能分清格式Redis 启动时如果检测到appendonly yes就会进入 AOF 加载流程。传统 AOF 加载逻辑是按行读取命令、逐条执行。混合持久化引入后这段逻辑不能简单地按老办法走因为文件前半段是二进制 RDB 数据按文本行读取必然解析失败。Redis 的做法是先读文件开头 5 个字节做一个快速分拣。伪代码角度大致是open appendonly.aof read 5 bytes if bytes REDIS: 按 RDB 格式加载整个前半段 继续加载剩余的命令段 else: 从文件头开始按纯 AOF 命令流逐条重放 close file从 Redis 源码角度看核心入口在 aof.c 的loadSingleAppendOnlyFile流程里。它会先检测文件头是不是 RDB 魔数是的话就用 RDB 加载器处理前缀部分处理完再回转到 RDB 结束位置把后半段交给常规的 fake client 命令重放逻辑。这个设计让用户完全不需要手动指定“这个文件是混合的”这个事实Redis 自己就能判断。3.2 RDB 段加载阶段重建全量键值对一旦确认文件是REDIS开头Redis 会进入 RDB 加载流程。它会先校验 RDB 版本号是否在当前实例支持范围内然后开始反向解析整个 RDB 数据流。解析过程中遇到数据库选择标记就把加载上下文切换到对应数据库遇到普通键值对就在对应库里构造对应数据类型遇到带过期时间的键就把过期时间和键一起恢复。这个阶段是整段加载里性能最高的部分。因为 RDB 数据流在生成时就是按照内存数据结构的序列化结果写入的加载器只需要反序列化不需要像执行写命令那样做完整的命令查询、权限判断、键空间检查、慢日志记录之类的路径。所以多个 G 的数据RDB 段往往几十秒内可以加载完成。如果 RDB 生成时启用了压缩加载时会解压数据这个动作会消耗一些 CPU但依旧比逐条执行命令快得多。3.3 RDB 段结束位置如何定位RDB 段不是随便跑到哪里就结束它有一个明确的结束标记。当加载器读到 RDB 自己的 EOF 标记时就说明 RDB 数据区结束了后面还会紧跟着 8 字节的 CRC64 校验和。Redis 会读取这段校验值同时对从文件头部一直到 EOF 标记前的所有字节做一次 CRC64 计算比对是否一致。这一步能有效发现磁盘损坏、文件被截断或者被篡改的问题。校验通过后RDB 加载完成文件指针此时停留在 RDB 段之后、AOF 命令段的起点。Redis 接下来会把加载状态从“RDB 模式”无缝切换到“AOF 命令重放模式”。切换过程对内存数据没有任何特殊操作RDB 段已经把全量键值对构建好了AOF 段只需要把后面的增量命令逐条执行到这份已经存在的内存数据上。你可以把它理解成“先恢复底表再追日志”。3.4 AOF 段重放阶段逐条命令应用到内存切换完成后Redis 会按照传统 AOF 加载方式一条一条读取剩余内容。实现上和正常客户端发命令没有本质区别只是通过一个 fake client 模拟客户端执行。加载器按 RESP 协议从文件里解析出一条完整命令比如SET key value然后走标准命令执行链路写入内存。这里你会看到“后半段命令”的几个重要特征。首先它是追加在 RDB 完成之后的所以理论上这段命令里出现的每一个 keyRDB 段里的事后状态已经存在这些命令执行后会把实例更新到接近宕机前的状态。其次因为 AOF 段里可能涉及多个数据库Redis 在加载 AOF 段时如果遇到SELECT命令会切换当前 fake client 的目标数据库。整段 AOF 命令重放到底是快还是慢取决于后半段命令条数。如果两次 AOF 重写之间间隔很短后半段可能只有几百条命令恢复只在毫秒级如果触发重写不频繁后半段可能有几百万条命令这部分耗时就会比较明显。这也是为什么运维上要设置合理的 AOF 重写阈值避免增量命令段无限膨胀。3.5 加载收尾与通知数据就绪所有命令都重放完毕后Redis 会关闭文件结束加载流程然后进入正常的网络事件循环开始对外提供服务。启动日志里会输出类似DB loaded from append only file: xxx seconds的信息有经验的运维可以通过这条日志判断加载耗时。此时客户端连上来看到的数据已经是恢复完成后的完整状态了。在线环境如果想观察加载进度可以在 Redis 还在加载时用redis-cli info persistence查看loading状态如果返回loading:1说明数据还没加载完这时 Redis 不会处理普通读写请求。很多监控系统就是用这个字段判断 Redis 是否处于启动恢复期。4. 实操验证手动打开混合持久化文件一帧一帧看4.1 准备一个能生成混合文件的 Redis 实例纸上谈兵讲再多不如自己亲手验证一遍。我建议在一台测试机上装一个全新的 Redis然后按下面的步骤操作。先在 redis.conf 里确认如下配置appendonly yes appendfilename appendonly.aof appendfsync everysec aof-use-rdb-preamble yes启动 Redis 后写入一批数据redis-cli 127.0.0.1:6379 SET user:1 zhangsan 127.0.0.1:6379 SET user:2 lisi 127.0.0.1:6379 SET user:3 wangwu 127.0.0.1:6379 RPUSH list:1 a b c d然后手动触发一次 AOF 重写redis-cli BGREWRITEAOF等重写完成后appendonly.aof就会变成混合格式。注意看文件大小里面不再是一行行纯文本命令而是前半段一堆二进制后半段才是可读的文本命令。4.2 用 xxd 看魔数文件头是怎么写的这一步最关键我一般用xxd看文件前几个字节xxd appendonly.aof | head正常输出会类似00000000: 5245 4449 5330 3031 3105 0d0a fe00 ... REDIS0011......看到没有文件开头前 13 个字节里的52 45 44 49 53对应的 ASCII 字符正好就是REDIS后面跟着30 30 31 31也就是0011。这就是 RDB 格式的铁证。如果是一个纯 AOF 命令文件开头绝不会出现REDIS而是直接以*开头的 RESP 协议也就是十六进制的2a。所以 Redis 只要检查开头的 5 个字节就能判断自己拿到的是哪种文件。4.3 用 strings 看后半段命令流RDB 段是二进制数据直接cat会满屏乱码。想确认后半段是不是一堆命令文本用strings最方便strings appendonly.aof | tail -n 50如果文件生成后还有增量写命令你会看到类似这样的输出SELECT 0 SET user:1 zhangsan SET user:2 lisi RPUSH list:1 a b c d这个位置已经进入 AOF 命令段。比如RPUSH list:1这组命令说明 AOF 重写期间之后又执行了对列表的写操作所有这些操作都以追加式命令流的形式出现在 RDB 段后面。为了进一步验证“后半段确实是文本命令流”我做过一个比较野的实验AOF 重写完成后停掉 Redis备份原文件然后向文件尾部手动追加一条合法的 RESP 命令printf *3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n appendonly.aof再次启动 Redis然后执行redis-cli GET hello输出的结果很干净world这条命令能生效的原因很简单RDB 段本身的 CRC 校验只覆盖到 RDB 段结束位置不会校验后面追加的内容而 AOF 段加载是按 RESP 协议逐条读到文件末尾的没有整体校验和因此尾部追加的命令同样会被正确执行。这个实验能直观证明混合文件的边界和加载行为不需要改任何源码就能读懂整个结构。需要提醒一句这个操作只建议在测试环境做验证生产环境的 AOF 文件千万别手动改除非你提前做过完整备份。4.4 用 redis-check-aof 检查文件完整性Redis 自带的redis-check-aof工具可以用来对 AOF 文件做完整性检查。如果文件是混合格式工具会识别出 RDB 前缀并继续检查后面的命令段redis-check-aof appendonly.aof完整无损的文件一般会输出类似AOF analyzed: sizexxx, ok_up_toxxx, ok_up_to_linexxx AOF is valid如果我把刚才手动追加的命令里故意改坏一个字节比如把SET改成SEX再跑一遍检查工具就会在出错位置报错并给出合法命令无法解析的提示。这类检查对恢复数据前的文件健康度判断很有用我一般会在每次计划重启前跑一次防止启动中途卡在加载阶段。5. 踩坑记录加载混合文件时的高频故障与排查清单5.1 bad file format / CRC 错误怎么排查混合 AOF 文件恢复过程中最常见的错误有两种。一种是在 RDB 段报错比如日志里出现Bad file format reading the append only file而且错误位置在一些二进制区块上另一种是CRC mismatch说明 RDB 段的校验和和实际内容不一致。遇到这两种情况第一反应应该是怀疑磁盘损坏或者文件被部分覆盖而不是立刻认为是代码缺陷。排查顺序我一般是这样先复制一份文件出来用redis-check-aof检查再把错误位置附近的十六进制内容导出来看看如果确认数据非常关键急需恢复就考虑先修文件再启动。最坏的情况下直接从上次 RDB 备份或从节点把数据导回接受丢失一小段窗口数据的代价。5.2 文件末尾截断aof-load-truncated 的默认行为另一种高频场景是突然断电导致 AOF 文件末尾被截断。Redis 配置里有一个aof-load-truncated参数默认值是yes意思是启动加载时如果发现 AOF 文件末尾只有半截命令Redis 会把它当成崩溃残留直接截断忽略同时打印一条 warning然后正常启动。我自己遇到过很多次这样的情况机房断电后重启 Redis第一眼看到日志里有个警告很慌但实际上 Redis 已经自动兜底把不完整的尾部跳过了。如果业务对数据完整度要求非常高可以把aof-load-truncated改成no这样 Redis 遇到截断文件会拒绝启动避免在一份已经损坏的文件上继续写入造成二次污染。这个参数建议在核心业务上设成no配合完善的备份体系让“人工介入恢复”成为必经环节。5.3 redis-check-aof --fix 到底能救回多少数据当 AOF 文件损坏导致 Redis 无法启动时很多人第一反应就是跑redis-check-aof --fix appendonly.aof。这个工具的修复策略本质上是扫描文件找出一条命令解析失败的位置然后让你选择是否把该位置之后的内容全部截断掉。也就是说它能保住损坏点之前的所有命令。注意一个边界如果损坏点出现在 RDB 段内修复工具的截断策略就可能把整个 RDB 前缀切掉到时候能恢复出的数据量就不乐观了。我的经验是先备份文件再跑 fix恢复完立刻找一个临时实例验证加载结果验证通过后再替换到生产路径上。千万不要在生产环境的原文件上直接 fix万一修坏了就是二次事故。5.4 版本升级与 RDB 版本兼容问题混合文件的前半段是 RDB 格式因此同样受 RDB 版本兼容性约束。Redis 6.x 生成的 RDB 文件版本号通常比 Redis 4.x 高Redis 7.x 又比 6.x 高。理论上新版本 Redis 可以加载旧版本 RDB因为 RDB 文件兼容策略原本就是向前的但旧版本 Redis 去加载新版本 RDB 就会报版本不支持的错误。所以把 Redis 从高版本降级到低版本时要注意混合 AOF 文件里的 RDB 段可能无法被低版本识别。我有一个习惯跨大版本升级后一定要在低版本实例上验证一次数据可恢复性验证失败就说明降级路径走不通。混合文件让 AOF 里夹杂了 RDB 版本信息很多人很容易忽略这个问题等到真正做版本回退时才发现恢复不了那时候处理成本就高了。5.5 加载耗时变长怎么定位瓶颈如果混合文件恢复时间越来越长要分两种情况排查。第一种是 RDB 段变大说明当前键值对总量增长瓶颈在 RDB 数据的加载和反序列化速度。第二种是 AOF 段变大说明两次 AOF 重写间隔太长增量命令积压太多瓶颈在逐条重放命令的耗时。定位方法也很直接先看启动日志里DB loaded from append only file的总耗时再用strings看 AOF 命令段的最后部分比如最后一条命令的写入时间戳基本可以判断这段增量命令覆盖了多长的时间窗口。一旦 AOF 段覆盖了几个小时甚至几天的所有写入就应该调小auto-aof-rewrite-percentage或手动定期执行BGREWRITEAOF。混合持久化虽然把恢复速度优化了一个量级但不代表可以无限放纵 AOF 段膨胀合理控制重写频率依然是关键手段。我在实际排查中最后想强调的一点聊到这基本原理和运维方法都说清楚了。混合持久化文件本质上就是“用 RDB 做底、用 AOF 追尾”的结构加载时的分水岭就在文件头部那 5 个字节的REDIS魔数上。读懂了这次切换你不仅能回答常见的面试题“AOF 混合文件怎么加载”也能在真实事故里快速判断文件报错是坏在 RDB 段还是 AOF 段启动慢到底是全量数据拖慢的还是增量命令拖慢的。我个人这两年实践下来的体会是混合持久化确实是我在生产环境里最推荐的一种持久化方案但前提是配套好 AOF 定时重写、启动前文件检查、版本兼容预演这三件套。没有这套兜底任何一个环节失控都可能抵消混合格式带来的恢复效率优势。下次如果你的 Redis 也遇到启动恢复慢的困扰不妨先用xxd看一眼前 13 个字节确认一下文件确实是以REDIS开头再按文章里的思路去定位问题排查路径会比你想的清晰很多。
返回列表