☰
Redis启动加载RDB内存估算与规格变更避坑实践
2026/10/5 2:42:30 网站建设 项目流程

话说回来,搞过 Redis 的人都知道,实例规格变更的时候最容易被忽略的环节,就是新实例启动时要把 RDB 文件重新加载进内存。这个操作看起来是"常规恢复",实际上等于把整个数据集在内存里重建一遍,整个过程对内存、对时间、对稳定性都有硬性要求。很多人栽就栽在"以为 RDB 文件几个 GB,内存给够就行",结果真正加载的时候发现内存直接顶穿,或者加载到一半 OOM 被系统杀掉,业务在一大早高峰直接断流。

这篇文章我从头到尾把"Redis 启动同步加载 RDB 文件到内存"这件事拆开来讲,结合我实际做过的一次规格变更,说清楚底层逻辑、内存估算方法、实操步骤、以及那些不遇到一次就很难记住的坑。不管你是正准备给 Redis 扩容,还是只是想把实例从单机迁到更高配置,这篇文章都值得你在动手前花十分钟看完。

1. 启动加载 RDB 的底层逻辑:内存才是真正的瓶颈

1.1 RDB 到底是个什么东西,加载时发生了什么

RDB 是 Redis 的持久化快照文件,默认文件名是dump.rdb,里面存的是某一时刻所有键值对的序列化二进制数据。Redis 启动的时候,如果配置了dbfilename并且目录下存在这个文件,主进程就会在初始化阶段调用rdbLoad()函数,把文件里的数据一条一条读出来,然后分配内存、重建数据结构、插入到对应的数据库字典里。

关键点在于:这个加载过程不是"边读边丢",也不是"按需加载",而是一次性全量加载。Redis 是单线程模型,加载期间主进程不做任何其他事情,所有客户端请求都会被阻塞。也就是说,从进程启动到 RDB 加载完成,期间 Redis 是不可用的。这个窗口期有多长,取决于三个因素:文件大小、数据结构复杂度、机器磁盘和内存带宽。

很多人容易把 RDB 加载想象成"把文件复制到内存",好像文件多大,内存就占多大。实际完全不是这么回事。RDB 文件是经过压缩的,常见的 LZF 压缩算法能把字符串内容压掉很大一部分,尤其对于 JSON 文本、日志这类可压缩性强的数据,压缩比能达到 3:1 甚至 5:1。但 Redis 在内存里存的是原始数据,加上每个 key 的字典节点开销、对象头开销、过期时间字段、底层编码结构,最终加载完后的实际内存占用,往往是 RDB 文件大小的好几倍。

拿我最常举的例子来说:一个 1GB 的 RDB 文件,里面如果全是平均长度 20 字节的小 key 小 value,那么上百万个 key 光字典节点和对象头的固定开销就非常可观。Redis 每个 dictEntry 大概要占 24 字节左右,每个 robj 对象头占 16 字节,再加上 SDS 字符串的头部和分配器对齐,一个 key-value 对的实际内存开销可能在 100 字节上下。同样的数据量,如果 value 是 500 字节的大字符串,反而单个键值对的内存占比更多是数据本身,固定开销被摊薄了。所以"文件越大越危险"这个直觉不严谨,"键值对数量越多、单个 value 越小"才是真正的内存杀手。

1.2 规格变更场景下,为什么这个加载过程会变成高危动作

常态下,Redis 启动加载 RDB 是故障恢复或正常的进程重启,这时候大家心里有数是"恢复现场",内存用量本来就是原实例在运行时的内存规模。可规格变更不太一样:你迁移到的新实例往往是刚开通的、内存规格可能比老实例大,但也可能只是刚好够用。更重要的是,规格变更往往意味着你设置了新的maxmemory参数,或者平台自动应用了一套默认配置。

如果maxmemory设置得过低,加载过程中一旦内存用量触碰到这个上限,Redis 就会立刻触发内存淘汰策略。这里有个很隐蔽的连锁反应:在 RDB 加载阶段,如果淘汰策略是allkeys-lru或volatile-lru,Redis 会在加载过程中直接开始驱逐键。被驱逐的数据不会写回到 RDB,但加载本身还在继续往内存里填,这就形成了"边加载边淘汰"的怪异状态,最终可能出现加载完成后,实际数据量比源实例少了一大截,而且你完全没有感知。

还有更糟的情况:如果maxmemory设得比系统物理内存还高,或者系统本身内存就吃紧,加载过程中 Redis 进程的内存使用会一路暴涨,直到触发 Linux 的 OOM Killer。结果就是新实例启动失败,或者启动后没几秒就被 kill,反复重启反复死。规格变更变成了事故现场。

我见过一个典型的失败案例:源实例内存用了 8GB,RDB 文件 2.6GB,新实例开通了 4GB 内存,maxmemory设成 3GB。看起来挺合理对吧?文件才 2.6GB,3GB 绰绰有余。实际上那个业务里存了大量的小 key,数据量大约 1200 万个 key-value 对,加载到 70% 的时候内存就撞上 3GB 上限,开始触发淘汰,淘汰本身又要消耗 CPU,加载速度进一步变慢。最后等了四十分钟,加载是完成了,但数据只剩原来的 62%,业务方直接炸了。

1.3 RDB 加载期间的内存分布,你得心里有数

如果你用top观察加载期间的 Redis 进程,会看到 RES 值一直往上涨,这个过程不是匀速的,而是有一波一波的峰值。原因是 Redis 读取 RDB 时,不是一条条插入那么简单,它会先把文件内容通过缓冲读进来,然后在dbAdd的时候一次性分配 key、value、dictEntry 三块内存。如果你的数据里有大量 list、set、hash 这种复杂结构,加载时还涉及构建内部编码结构,比如 ziplist 转 hashtable、skiplist 的逐节点插入,这期间的临时内存开销比纯 string 数据要大得多。

所以我一直强调一个经验:估算规格变更的内存需求,别只盯着 RDB 文件大小,也不要用"老实例当前 used_memory"直接套新实例。老实例运行了很长时间,内存里可能已经有碎片、有缓存、有主从复制带来的额外开销,而新实例加载 RDB 是一个"从零构建"的过程,它不会继承那些历史包袱,但会有一个明显的瞬时峰值。这个峰值可能出现在加载的中后段,因为随着字典越来越大,rehash 的触发会带来额外的临时内存分配。

2. 动手前先算账:精准估算 RDB 加载需要多少内存

2.1 第一步,从老实例收集三个关键数据

在动规格变更之前,先登录老实例,执行三条命令,拿到最核心的输入:

redis-cli info memory redis-cli info persistence redis-cli dbsize

info memory重点看两个字段:used_memory表示当前实际占用,RDB 文件大小可以从config get dbfilename和文件系统ls -lh拿到。dbsize告诉你总共有多少个 key,这个数字对于估算小 key 场景下的固定开销特别有用。然后是info keyspace,可以看到每个 db 里的 key 数量和有过期时间的 key 数量。

把这三项数据记录下来之后,我一般会再手动验证一下 RDB 文件本身有没有问题:

redis-check-rdb /data/redis/dump.rdb

这个工具会扫描整个 RDB 文件,报告文件完整性、包含的 key 数量、过期 key 占比。如果输出的 key 数量和dbsize对不上,说明源实例在生成 RDB 之后又发生了写入,这是正常现象,但要意识到:你迁移的数据量至少要以 RDB 里的为准。

2.2 内存预估公式和三条经验线

我自己在迁移前会先算一版"乐观值"和"悲观值",不追求精确,但求有个上下界。经验上可以这样粗算:

  • 字符串数据为主:RDB 文件大小 × 2.5 ~ 4是加载后的预期内存占用。压缩比越高,这个倍数越大。
  • 小 key 密集场景(平均 key 加 value 小于 100 字节):建议按dbsize × 150 字节来估算固定开销,再把 RDB 文件未压缩的数据量估算加进去。
  • 复杂结构(list/hash/set/zset)占比超过 30%:在字符串估算基础上再乘 1.3,因为这些结构在加载时要构建额外的索引节点和编码转换。

最稳妥的办法是直接把新实例的内存规格定在老实例used_memory的 1.5 倍以上。如果老实例used_memory是 8GB,新规格直接上 12GB 或 16GB。这个余量不是为了平时运行,而是专门给 RDB 加载时的瞬时峰值准备的。

另外有个特别容易忽略的点:检查 Linux 的vm.overcommit_memory设置。如果这个值是 0 或 1,Redis 在加载大 RDB 时大量分配内存不太会被系统拒绝;但如果设置成 2,内核会严格控制内存超额分配,即使物理内存还剩不少,Redis 分配内存也可能直接失败。我在生产环境都建议把vm.overcommit_memory设为 1,这不仅对 RDB 加载有好处,对 BGSAVE 时的 fork 也至关重要。

2.3 maxmemory 和淘汰策略必须在加载前先想清楚

很多人在新实例上沿用一套默认配置,maxmemory可能就是物理内存的一半甚至更低。对于平时运行的实例,这或许是合理的保护机制,但在规格变更、RDB 加载的场景下,这个值就成了最危险的一条线。

我建议在加载阶段不要把maxmemory设得太保守。有两种可行做法:

一种做法是临时把maxmemory设置为 0(表示不限制),等加载完成、确认数据完整、业务流量也接进来之后,再修改配置设成目标值。这种做法安全性最高,坏处是需要你在加载完成后记得改回来,而且如果加载期间有其他进程抢内存,Redis 可能被系统 OOM,所以你得确认这台机器上没跑别的吃内存的服务。

另一种更严谨的做法是预估一个"加载峰值内存 + 20%"作为临时maxmemory,同时把淘汰策略临时改成noeviction。这样如果内存估算出错,Redis 不会默默删数据,而是直接报 OOM 错误,能让你第一时间发现,而不是等业务反馈说数据少了。

我自己的习惯是:先算预估,留足余量,把maxmemory临时设成 0,加载完立刻验证数据量,然后马上调回正常配置。整个过程控制在十分钟内完成,风险窗口很短。

3. 规格变更实操:带着 RDB 加载这个关键动作走完整个迁移

3.1 备份和预校验,这两步不能省

进入实操环节,第一步永远是备份。规格变更本质上是一次数据迁移,无论你是用平台自带的"变更规格"功能,还是自己搭新实例来迁,源实例的 RDB 文件都是最后一道保险。

先手动触发一次 BGSAVE,确保拿到最新的 RDB:

redis-cli bgrewriteaof 2>/dev/null redis-cli bgsave redis-cli info persistence | grep rdb_bgsave_in_progress

等rdb_bgsave_in_progress变成 0,再检查rdb_last_bgsave_status是否为ok。然后把 RDB 文件复制到一个安全的地方。如果你用的是云厂商的 Redis 规格变更功能,平台一般会在后台做备份,但我仍然建议手动导出一份,原因很简单:平台操作出问题时,你手里得有自己的底牌。

拿到备份文件之后,用redis-check-rdb做一次完整校验,同时也能提前知道这个文件的 key 总量。

3.2 新实例配置清单,照着设置不出错

新实例启动前,我建议逐项检查这几个配置:

# 关闭持久化减少启动干扰,等数据确认后再开 save "" # 临时放开内存上限,避免加载触发淘汰 maxmemory 0 # 加载期间禁止所有外部写入和读取 protected-mode yes bind 127.0.0.1 # 保证 fork 和内存分配不被内核卡脖子 # 需要在系统层设置:sysctl vm.overcommit_memory=1

这里把bind设成 127.0.0.1 可能有点极端,但在加载阶段确实有必要。你想,如果新实例一启动,业务客户端就连上来了,而 RDB 还在加载中,客户端会收到大量超时或错误。与其让调用方踩坑,不如先让实例完全不可达,等加载完成、验证通过之后再放开网络。

另外有个细节:save ""是临时关闭 RDB 快照,这个操作的意义是防止加载过程中又触发自动 BGSAVE,那样会造成额外的磁盘和 CPU 压力。加载完成后,记得把持久化配置恢复原样。

3.3 启动加载过程的现场观察

启动命令很简单,无非是:

redis-server /etc/redis/redis.conf

但启动之后不要干等着,我一般会在另一个终端窗口实时观察:

redis-cli info stats | grep instantaneous redis-cli --stat dmesg -T | tail -20

--stat可以每秒刷新一次,看到keys字段在往上跳,说明加载在正常推进。同时用top观察 RES 内存增长曲线。正常情况是内存和 key 数同步增长,如果发现内存涨得很快但 key 数增长变慢,大概率是遇到了大 value 或复杂结构,这通常意味着加载时间会比你预期的要长。

加载期间,Redis 日志文件里会出现类似这样的记录:

Making user:db:0 Loading RDB produced by version 7.2.4 rdbLoad: 12345678 keys in 123 seconds Done loading RDB, keys loaded: 12345678, keys expired: 345

keys expired这个字段很有意思:源实例里已经过期的 key,在加载时并不会全部被立即清除,有一部分会在加载完成后惰性删除。所以加载完成后你看到的dbsize可能会比源实例略小,这是正常的,不必恐慌。

3.4 加载完成后的第一轮确认

当日志出现Done loading RDB之后,先别急着放业务流量。我习惯按固定顺序做四件事:

第一,对比 key 总数:

redis-cli dbsize

和源实例的dbsize对比,偏差在合理范围(主要差在已过期未清理的 key)就继续。

第二,抽查关键业务 key。取几个已知的、线上正在用的 key,用get或type验证结构和值是否正确。这一步能快速发现数据错乱或序列化类型变更的问题。

第三,检查内存状态:

redis-cli info memory | egrep "used_memory|used_memory_rss|mem_fragmentation_ratio"

mem_fragmentation_ratio在加载刚结束时可能很高,甚至超过 1.5,这通常是分配器的瞬时状态,过一阵会回落,不用太紧张。

第四,确认没有内存淘汰发生:

redis-cli info stats | grep evicted_keys

如果这个值不是 0,说明加载期间发生过淘汰,数据已经不完整了,需要回到上一步排查。

4. 加载故障排查:RDB 相关的真实问题记录与处理思路

4.1 文件损坏类问题:尽快区分"文件坏了"还是"版本不兼容"

RDB 加载最常见的报错有两类,一类是 Redis 日志里直接提示:

Bad file format reading the DB file: Invalid RDB header

另一类是:

RDB file was created with a different server version

看到Invalid RDB header,首先要怀疑文件本身损坏或复制不完整。用redis-check-rdb扫描一遍,注意它输出的最后一行,如果是[error]开头,那基本可以判定 RDB 文件废了。这时候唯一的选择是从更早的备份恢复,或者让源实例再生成一份新的 RDB 转移过来。

different server version则常见于跨大版本的迁移。比如老实例还是 Redis 5.x,新实例已经升级到 7.x,RDB 格式不向下兼容是完全可能的事。遇到这种,先别换数据文件,优先调整新实例版本到和源实例一致,或者用redis-migrate-tool这类工具做在线迁移而不是直接加载 RDB。

这里我要多说一句:很多云平台的"规格变更"不会改版本,但如果你是自己搭新实例,非常容易顺手装个最新版 Redis,结果 RDB 版本对不上。迁移前务必要用redis-server --version确认两边大版本一致。

4.2 加载阻塞和超时:为什么明明数据不多却加载很慢

有次我帮朋友排查一个 Redis 加载超时的问题,新实例配置很高,内存也够,RDB 文件只有 1.2GB,理论上加载应该一分钟内搞定,结果等了十五分钟还没完。用top一看,CPU 是跑满了,但内存增长很慢。

后来分析才明白,那个 RDB 里有大量非常大的 hash 结构,每个 hash 包含几十万个 field。Redis 加载这种结构的时候,如果hash-max-ziplist-entries配置比较保守,数据在原始 RDB 里是以 ziplist 紧凑格式存储的,但加载到内存时一旦超过配置阈值,Redis 要把 ziplist 转换成 hashtable,这个转换过程涉及大量 rehash,单个 key 可能要花好几秒。几个大 hash 加起来,加载时间就被无限拉长了。

解决方案有两个方向:一是提前把hash-max-ziplist-entries和hash-max-ziplist-value调大,让加载时尽量保持紧凑编码;二是接受较长的加载时间,把变更窗口调大。我个人更推荐后者,因为前者改了配置之后,后续运行时的内存行为也跟着变,容易引入新的性能问题。

另外,加载慢还有一个隐藏原因:RDB 文件所在磁盘的 IO 能力。如果新实例用的是普通云硬盘而不是 SSD,随机读大文件的速度可能只有几十 MB/s,加载时间就会被 IO 卡住。这个在加载前就能通过hdparm或dd测试预估,别等到加载了才发现磁盘跟不上。

4.3 加载失败的排查速查表

现象可能原因优先处理动作
日志出现Can't open the file文件路径或权限不对检查dir配置和运行用户对 RDB 文件的读权限
日志出现Short read or OOM文件在复制过程中被截断重新从源实例生成 RDB 并完整传输
日志出现Invalid RDB header文件损坏或不是 RDB 格式用 redis-check-rdb 验证,必要时从备份恢复
加载完成后evicted_keys不为 0maxmemory 设置过小临时把 maxmemory 设为 0 后重新加载
加载完成后部分 key 不存在源实例有过期 key先看日志中keys expired的数量是否合理
加载期间连接闪断客户端超时配置过短变更窗口内让业务直接停写,或加大客户端超时时间
加载期间系统报 OOM物理内存不足或 overcommit 设置过严检查vm.overcommit_memory,必要时加物理内存

这张表是我自己整理出来的,每次做迁移前都拿出来过一遍,尤其是evicted_keys和Short read这两行,踩过的坑最深。

5. 规格变更后的长尾验证:别以为加载完成就算结束

5.1 数据完整性快检清单

加载完成、业务恢复之后,真正的验证才刚开始。我建议在恢复流量后的 5 分钟、30 分钟、2 小时三个时间点各做一轮检查。

第一个时间点看核心指标:dbsize、used_memory、evicted_keys、rejected_connections。第二个时间点重点看慢查询:

redis-cli slowlog get 20

如果出现大量慢查询,可能和加载后的数据结构编码有关,特别是大集合类型。第三个时间点看主从复制状态(如果有从库):

redis-cli info replication

确认master_link_status:up和偏移量一致。

另外我强烈建议在恢复业务前,先从源实例导一个 key 样本列表,哈希值存下来,等新实例加载完之后用同样的哈希算法比对。这个做法只要写个几十行的脚本就行,但能发现很多肉眼看不出的数据差异,特别是 key 值被截断、类型被转换这类问题。

5.2 内存碎片和 used_memory_rss 的隐藏炸弹

规格变更后经常遇到一种情况:used_memory显示正常,但used_memory_rss高得吓人,内存碎片率超过 2.0。这是因为加载 RDB 时 Redis 一次性申请了大量内存,释放以后内存分配器没有把内存归还给操作系统,而是留在进程里备用。

这个本身不是故障,但如果你接下来还要在这台机器上部署其他服务,就可能出现"Redis 看着没占多少,系统却已经 swap"的怪现象。处理办法是等业务低峰期执行:

redis-cli memory purge

或者更彻底的做法,先确认数据无误,然后重启一次 Redis,让内存重新整理。重启成本在这个阶段是可以接受的,因为数据已经通过 RDB 持久化,重启后加载速度通常比第一次快。

5.3 别忘了把配置从"加载模式"切回"生产模式"

回到当初临时改的那几个配置:save ""、maxmemory 0、bind 127.0.0.1、protected-mode yes,现在要一项一项恢复。顺序很重要:

先开持久化,让 Redis 恢复自动 BGSAVE 和 AOF 写入能力;然后设置正常的maxmemory和淘汰策略;最后放开网络访问。每改一项就验证一项,不要一次性把所有配置都写进文件然后重启,那样万一哪里配置错了,排查范围就太大了。

我个人的生产配置一般是:

maxmemory 12gb maxmemory-policy allkeys-lru save 900 1 appendonly yes

maxmemory设成规格的 75% 左右,剩下 25% 留给系统缓存和 fork 时需要的额外内存。比如 16GB 实例,maxmemory设 12GB,这样即使 RDB 加载的瞬时峰值冲得比较高,也不太容易碰到系统物理内存上限。

6. 我个人踩过的一次规格变更的坑,拿出来给大家提个醒

那次变更的源实例是老牌 8GB 单机,数据约 5.8GB,RDB 文件 1.9GB,业务全是用户会话类的小 key,大概 900 万个 key。我当时的估算很简单:5.8GB 的数据,新实例 8GB 肯定够了,maxmemory设了 7GB,满以为万无一失。

结果加载到一半,内存占用直接冲破 7GB,触发淘汰,这时候我还在用--stat看着keys数量下跌,心里一凉。赶紧登录新实例,把maxmemory改成 0,但已经晚了,之前被淘汰的 key 没了。我不得不删掉 RDB 重新开始加载,这次学乖了,maxmemory设 0 放到加载完,数据一次就齐了。

回头分析那次为什么内存估算差这么多,原因就是小 key 密度太高。900 万个 key,平均每个 key 的固定开销加在一起,比我从 RDB 文件大小推导出来的预期多了一倍多。就因为这一个教训,我后来每次迁移都会先跑一遍redis-check-rdb,拿到真实的 key 数量,再结合dbsize做双重验证,再也不敢只瞄一眼文件大小就拍脑袋定内存。

还有一次是加载完成后忘了恢复maxmemory,结果新实例以无上限模式跑了一整天。那天恰逢业务做活动,写入量是平时的三倍,内存一路涨到物理内存的 95%,等到我发现的时候,系统已经开始 swap,整个 Redis 的读写延迟飙到了秒级。所以我现在把"恢复配置文件"写进了操作清单,每改完一项打一个勾,全部完成了才宣布变更结束。

规格变更碰到 RDB 加载,本质上是拿内存换时间、拿预算换稳定性的博弈。只要你能把文件大小、key 数量、结构占比这三件事算清楚,把临时配置和恢复流程都写成 checklist,这个操作就是一个相当成熟的例行动作。真遇到问题也别慌,日志里给的信息已经足够定位九成以上的故障。

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

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

立即咨询