【Redis 神秘大陆】这个系列写到第六篇,聊一个平时没人主动提起、真出事时第一个被追问的话题:Redis 灾备方案。前五篇谈的是怎么把 Redis 用快、用稳、用得顺手,这一篇反过来,谈它彻底不可用之后,数据还能不能捞回来。促成本篇写的是一次很典型的现场复盘:某团队的主从架构跑了一年多,监控面板上两个节点一直是绿色,所有人都默认它高可用。直到某个凌晨,一个本该只连测试库的清理脚本误连了生产实例,一条FLUSHALL下去,主库瞬间清空,两百毫秒后从库同步到位,主从双双归零。事后翻配置才发现,RDB 快照是有的,但上一次写入磁盘是六个小时前;AOF 根本没开;备份文件从部署那天起就没被复制出过这台机器。
这里没有一处可以归因于 Redis 本身。问题全部出在灾备设计上:防的是什么事故没想清楚,持久化和备份被混为一谈,副本被当成了保险箱。所以这篇按这个逻辑走:先把事故类型分级,再用 RPO/RTO 把“容灾”翻译成可验收的数字,接着拆持久化、复制、异地归档三层怎么配,最后讲怎么用演练把纸面方案跑成能真正救命的流程。适合已经在跑 Redis、准备补一套容灾体系的读者,也适合刚接手别人留下的 Redis 集群、需要快速摸清家底的人。
1. 灾备不是“备份”:先分清你要防的三类事故
做灾备最常见的错误,是一上来就问“备份放哪”。这个问题本身没错,但顺序反了。正确的起点是:你打算防哪几种事故?不同事故的应对手段在两个完全不同的层面,配错层,等于白配。
1.1 进程崩溃、机器宕机、机房失联:三种事故的应对层完全不同
我一般把 Redis 的故障分成四类,逐一对照手段:
| 事故类型 | 典型场景 | 有效手段 | 无效手段 |
|---|---|---|---|
| 进程级 | OOM 被系统杀掉、崩溃重启、配置改错 | RDB / AOF 持久化 | 主从(数据仍在内存中,重启即失) |
| 单机级 | 磁盘损坏、主板故障、宿主机下线 | 主从 / 哨兵 / Cluster | 只在本机持久化(磁盘坏了文件也没了) |
| 机房级 | 供电中断、网络整体不可达 | 异地副本、异地备份归档 | 同机房多副本(会一起下线) |
| 逻辑级 | 误删、脚本错误、脏数据覆盖 | 时间点快照、多版本备份 | 主从(错误会立刻同步到所有副本) |
这张表是我每次排查灾备缺口时的第一张检查单。四个格子只要有一个是空的,方案就是不完整的。
特别说一下第四类,也就是逻辑级事故。这是被低估得最厉害的一类。主从在这里不但没用,反而会加速故障扩散:你删了主库,从库跟着删;你从从库切主,切上去的还是删干净的数据。能救这一类的只有不跟随写入的时间点副本。
1.2 用 RPO 和 RTO 把“容灾”翻译成可验收的数字
“方案要可靠”是句空话,验收不了。能验收的只有两个数字:
- RPO(Recovery Point Objective):最多能接受丢多长时间的数据。它决定了持久化频率和备份频率。
- RTO(Recovery Time Objective):最多能接受停多长时间。它决定了你选热备、温备还是冷备。
这两个数字必须由业务方给出,不能由 DBA 自行确定。实际沟通可以先给一个推导起点,再让对方确认:
| 数据性质 | 典型 RPO | 典型 RTO | 建议方案 |
|---|---|---|---|
| 纯缓存,缓存失效后可从数据库回源重建 | 小时级 | 10 分钟内 | 单副本 + 定期快照 + 快速重建脚本 |
| 会话 / 登录态 | 秒级 | 1 分钟内 | 主从 + 哨兵 + AOF everysec |
| 排行榜 / 统计结果 | 分钟级 | 5 分钟内 | 主从 + 定期快照+可从源数据重算 |
| 计数 / 队列 / 幂等键 | 接近 0 | 1 分钟内 | 必须落库或 AOF always + 写完确认 |
这里有个判断标准我一直在用:Redis 里的数据到底是不是唯一数据源。如果它只是数据库前面的缓存,那丢了就丢了,灾备的重点反而变成“重建不能把后端数据库压垮”,需要提前准备限流和分批预热。如果 Redis 承担了唯一存储的职责,那么再强的灾备也只是缓解,真正的解法是把关键数据补一份写到持久化存储里。这一点想不清楚,后面所有参数配置都是无源之水。
1.3 最常见的错位:把持久化当灾备,把主从当备份
这两个混淆几乎是所有事故现场的共同特征,值得单独说明。
持久化防的是重启,不防事故。RDB 和 AOF 让进程重启后能恢复数据,但文件就在本机磁盘上,磁盘损坏、误格式化、机器被回收,文件一样没了。把持久化当灾备,等于把保险柜和房子建在一起。
主从防的是单机故障,不防逻辑错误。从库是主库的实时镜像,同步延迟通常在毫秒级。主库写错了,从库五十毫秒后就错了。想靠“出问题从从库切过去”来救误删,是行不通的。
备份是时间点快照,离线,不跟随写入。它恢复出来的数据一定是过去的,也不实时,但正因为这样,它才是唯一能对抗逻辑错误的手段。
三者的关系可以这么理解:持久化是让房子重建之后家具还在;主从是隔壁还有一套一模一样的房子;备份是老照片,房子拆了也能照着老照片把样子还原出来,只是还原的是当时的样子。
2. 持久化配置的取舍:RDB 快照、AOF 日志与混合模式
持久化是灾备的地基。这一层配得不对,上面搭什么都站不住。Redis 给了两套机制加一种混合模式,每一套的价值和代价都非常具体,值得逐项说明。
2.1 BGSAVE 的 fork 代价与内存水位红线
RDB 靠BGSAVE生成快照,BGSAVE会 fork 一个子进程。fork 本身的耗时取决于页表规模,经验上讲大致在每 GB 内存十几毫秒的量级,机器和内核版本不同差异很大,32GB 的实例 fork 花上百毫秒并不罕见。这段时间主线程是阻塞的,客户端会感知到一次请求毛刺。
更关键的是写时复制(COW)带来的内存放大。BGSAVE期间如果写入量大,被修改的内存页会被复制一份给子进程,实例的内存占用可能涨到原来的 1.5 倍甚至接近 2 倍。所以有一条必须遵守的水位线:used_memory长期不要超过maxmemory的 60% 到 70%。超过这个比例,一次BGSAVE加上一次全量同步叠加,就很容易触发 OOM 或者 swap,那就不是毛刺而是雪崩了。
还有一个真实经历过的问题:透明大页(THP)。系统开启 THP 之后,COW 的内存复制单位会从 4KB 变成 2MB,同样的写入量下,COW 复制的内存会放大,BGSAVE期间延迟尖刺会明显得多。我的做法是在部署脚本里固定关掉:
echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defragRDB 的常用配置:
save 900 1 save 300 10 save 60 10000 stop-writes-on-bgsave-error yes rdbcompression yes rdbchecksum yes dbfilename dump.rdb dir /data/redis/stop-writes-on-bgsave-error这一行值得展开说,因为它直接站在数据安全和业务可用性的对立面上。
- 设为
yes:写入磁盘失败时主库拒绝写请求,数据安全性好,但业务会立刻感知到写入不可用。 - 设为
no:继续接受写入,但此时 RDB 已经坏了,你以为什么都没发生,其实备份链路早已中断。
我的选择是yes加严格的告警。原因很直白:你在硬盘损坏时继续写入,得到的只是一份看起来正常、实际无法恢复的数据,掩盖问题的代价比短暂不可用高得多。但前提是必须监控rdb_last_bgsave_status,把这个指标作为重点告警项,否则等于给自己埋雷。
对于单实例超过 32GB 的情况,我更倾向于拆分成多个小实例,而不是在一个大实例上调整参数。原因很实在:fork 抖动和全量同步的代价都随数据量非线性上升,缩小单实例体积比调参有效得多。
2.2 appendfsync 三档选择的真实后果
AOF 是把写命令追加到日志文件,appendfsync决定多久真正调用一次fsync写入磁盘。
| 取值 | 行为 | 最坏情况丢失 | 代价 |
|---|---|---|---|
always | 每条写命令都 fsync | 基本为 0 | 每次写多一次磁盘往返,吞吐可能降到几千 QPS,寿命消耗也大 |
everysec | 后台线程每秒 fsync 一次 | 约 1 秒 | 周期性延迟尖刺,通常 1 到 25 毫秒 |
no | 交给操作系统决定 | 可能 30 秒 | 延迟平滑,但丢失量不可控 |
绝大多数生产环境用everysec,但它的副作用很多人没意识到。当磁盘 IO 已经很紧张、上一次 fsync 还没完成时,主线程在刷新缓冲时会等待,表现出来的现象是每隔一两秒出现一次延迟毛刺。如果你的监控里看到 p99 延迟稳定地呈现锯齿状,而 CPU 和网络都不高,先去看磁盘的await和%util,八成就是这个原因。
配套的两个参数:
appendonly yes appendfsync everysec no-appendfsync-on-rewrite yes aof-load-truncated yesno-appendfsync-on-rewrite yes的含义是:AOF 重写期间不做 fsync。目的是避免重写的大量磁盘写和 fsync 抢 IO。代价是这段时间的丢失窗口会变大,取决于操作系统的刷盘节奏。这个参数属于典型的两害相权,我倾向于打开,同时把重写时间安排在业务低峰。
aof-load-truncated yes允许 AOF 尾部不完整时继续启动。这里的取舍很明确:尾部截断说明最后一批写入可能不完整,但总比起不来强。生产上建议打开,同时监控日志里的截断告警,因为它往往意味着进程曾经异常退出或者磁盘写满。
2.3 AOF 重写与混合持久化:为什么要打开 aof-use-rdb-preamble
AOF 是追加写,文件会越来越大,必须靠重写来压缩。触发条件:
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 512mb第一条表示文件比上次重写后增长了一倍就触发,第二条是下限保护,避免小实例频繁重写。min-size我一般设得比默认的 64MB 大,因为小文件重写的收益很低,而重写本身有 fork 和 IO 代价。
aof-use-rdb-preamble yes这个参数从 4.0 起提供,现在应该默认打开。它让 AOF 文件的前半段用 RDB 格式保存全量数据,后半段才是增量命令。收益有两个:文件体积明显变小,加载速度大幅提升。在几十 GB 数据量下,纯命令格式的 AOF 加载可能要好几分钟,混合格式通常能压到几十秒。
7.0 之后还有一个功能值得用起来:aof-timestamp-enabled yes。开启后 AOF 里会记录每条命令的写入时间戳,恢复时可以按时间点截断,把数据恢复到“误操作发生前的那一刻”。对应工具支持按时间戳截断 AOF:
redis-check-aof --aof-file /data/redis/appendonlydir/appendonly.aof.1.incr.aof \ --truncate-to-timestamp 1735660800这个能力对误删类事故价值极高,等于把 RPO 从“最后一次备份”压缩到了“精确到秒的某个时刻”。前提是 AOF 本身要一直开着。
2.4 从写入磁盘的文件恢复的完整操作链路与校验
恢复不是把文件拷过去启动就完事,这套流程我踩过太多次坑,现在固定成七步:
- 先冻结现场。把当前
dir整个目录改名或复制一份,不要覆盖。现场一旦丢失,后续排查完全没法做。 - 确认要恢复的时间点。从备份清单里挑最近的、在事故之前的那个版本,注意核对备份时间戳而不是文件名。
- 校验文件完整性。不要跳过这一步。
两个工具都会打印文件的基本信息,包括redis-check-rdb /backup/dump.rdb redis-check-aof --aof-file /backup/appendonly.aofredis-ver、库数量、键数量。这一步能在启动之前就发现文件损坏。 - 核对版本兼容性。RDB 格式是向下兼容、向上不兼容的:高版本写的 RDB,低版本加载不了。恢复前确认目标实例版本不低于备份来源版本。
- 处理文件权限。文件属主必须是运行 Redis 的那个用户。这个问题极其常见,尤其是从对象存储拉下来再解压的场景,解压出来的文件属主是执行解压操作的用户,Redis 读不到,启动后
dir是空的,你可能还以为恢复成功了。 - 注意 AOF 和 RDB 的优先级。如果
appendonly yes且 AOF 目录存在,Redis 会优先加载 AOF。很多人把新的dump.rdb拷进去,结果加载的还是那份旧 AOF,数据看起来没变。恢复 RDB 时必须把 AOF 目录一并移走。 - 启动后做验证。至少查四项:
dbsize是否接近预期、抽样几个 key 的值是否正确、TTL 是否保留、DB 编号分布是否符合预期。
还有一个容易忽略的点:databases配置数量。如果源实例配了databases 32,恢复的目标实例还是默认的 16,超出范围的库数据就会出现问题。这个错误在跨环境恢复时特别常见,恢复前一定要逐行核对配置文件差异。
3. 复制链路:主从、哨兵、Cluster 各自的容灾能力与盲区
持久化解决了重启问题,复制解决的是“机器没了”。这两者叠加才是完整的高可用底座。但复制链路本身有大量参数决定了它在故障时到底能不能顶上,配不好就是“看着有两个节点,真挂了切不过去”。
3.1 全量同步、增量同步与 repl-backlog 的容量估算
从库第一次连上主库时走全量同步:从库发PSYNC ? -1,主库执行BGSAVE生成 RDB,传输给从库,传输期间的新写入暂存在 backlog 里,传完之后再把 backlog 里的增量补发。重连时如果能对上 offset,就走增量。
关键在 backlog:它是一个环形缓冲区,默认只有 1MB。这个默认值在实际业务里几乎必然不够。一旦从库断线的时间超过 backlog 能覆盖的时长,就会退化成全量同步。而一次全量同步的代价可能是几分钟的传输加一次BGSAVE抖动,在业务高峰期相当危险。
容量估算有个简单公式:
repl-backlog-size ≥ 平均写入字节速率(B/s) × 预计最长断线时间(s) × 2举例:写入量 5MB/s,希望容忍 120 秒的网络抖动,需要5MB × 120 × 2 = 1200MB,也就是 1.2GB。这个数字看起来夸张,但用默认 1MB 的时候,断线 0.2 秒就会退化成全量。
repl-backlog-size 512mb repl-diskless-sync yes repl-diskless-sync-delay 5 client-output-buffer-limit replica 1024mb 512mb 300repl-diskless-sync让主库直接把 RDB 通过网络发给从库,不写入磁盘,省掉一次磁盘写。磁盘性能一般而网络充裕的环境,这个参数收益明显。
client-output-buffer-limit replica这一行很多人没配过,但它会导致一个很难查的现象:全量同步反复失败,日志里是“从库超时断开”。原因是主库给从库准备的发送缓冲区超过了默认限制(硬限 256MB),主库直接把连接断了。大实例全量同步时,缓冲区堆积到几百 MB 很正常,必须调大,否则永远同步不完。
3.2 哨兵选主:quorum 与脑裂防护参数怎么配
哨兵负责监控和自动切换。部署上的第一条硬性要求:至少三个、且为奇数。两个哨兵的部署看似省钱,实际上任何一个挂掉就无法达成多数,切换能力直接归零。
sentinel monitor mymaster 10.0.0.11 6379 2 sentinel down-after-milliseconds mymaster 30000 sentinel failover-timeout mymaster 180000 sentinel parallel-syncs mymaster 1quorum的取值按“哨兵总数的一半加一”起步,三个哨兵配 2。判定下线需要 2 个哨兵都认为主库不可达,这个门槛既避免单点误判,又保证真故障时能触发。
down-after-milliseconds我一般设 30 秒而不是更短。设成 5 秒看起来很敏捷,但一次 GC 停顿、一次网络抖动、一次BGSAVEfork 抖动都可能超过 5 秒,结果是频繁误切换。切换本身有代价,新主选出来之后所有从库要重新同步,业务会抖一阵。
parallel-syncs 1表示切换完成后,一次只有一个从库向新主发起同步。设成 2 或更多会让切换后的数据可用性恢复更快,但新主同时服务多个全量同步,本身可能被压垮。我在数据量大的环境里坚持用 1。
脑裂防护是这一节最该讲的部分。网络分区时,老主和哨兵失联,但客户端还能连上老主继续写;与此同时哨兵选出新主,集群里出现了两个接受写入的主库。分区恢复后,老主被降级为从库,它在这段时间接受的写入会被清空并重新同步,那部分数据直接消失。
防护参数配在主库上:
min-replicas-to-write 1 min-replicas-max-lag 10含义是:至少要有一个副本在线,且延迟不超过 10 秒,否则主库拒绝写入。这样老主在被隔离之后会自动变成只读状态,避免产生无法合并的写入。
代价必须说清楚:副本全部挂掉时,主库也会拒绝写,可用性下降。这个取舍没有标准答案,取决于业务是“宁可不可写也不能写乱”还是“先保证能写”。我的经验是交易类、计数类业务用这组参数,纯缓存类业务不用。
还有一个隐蔽的坑:这组参数哨兵切换时不会自动下发到新主。新主是从原来的从库提升上来的,如果配置文件里没有同样的两行,切换完成后防护就失效了。所以配置必须写进所有节点的配置文件,而不是只在主库上临时CONFIG SET。
3.3 Cluster 的副本迁移与 cluster-require-full-coverage
Cluster 模式把数据分到 16384 个槽上,至少三主三从。主节点挂掉后,对应的从节点在多数主节点达成一致后自动提升,不需要外部组件。
cluster-node-timeout 15000 cluster-require-full-coverage yes cluster-migration-barrier 1cluster-node-timeout决定多久判定节点失联。设太短,一次网络抖动就触发切换和槽迁移,集群会反复震荡;设太长,真故障时恢复慢。15 秒是多数场景能接受的折中。
cluster-require-full-coverage是个必须由业务方拍板的参数:
yes:任何一个槽没有可用主节点,整个集群拒绝服务。一致性优先,但一个分片出问题会拖垮全部。no:只有受影响的槽不可用,其他槽照常服务。可用性优先,但客户端需要能正确处理部分 key 报错。
我见过的大多数业务选择no,但前提是客户端 SDK 对CLUSTERDOWN和MOVED有正确的处理逻辑,否则会出现零星的、难以复现的失败。
cluster-migration-barrier控制副本自动迁移:当某个主节点没有任何副本时,从副本富余的主节点迁一个过来。默认值 1 是合理的。设太大会导致副本迁移不触发,某个主节点长期处于没有副本的裸奔状态。
Cluster 还有一个经常被灾备方案忽略的限制:只有 db0,不支持多库。如果原方案依赖多库做数据隔离,上 Cluster 之前必须改数据模型。
3.4 切换瞬间到底会丢多少数据
这个问题必须讲清楚,因为它是灾备方案里最容易被高估的部分。
Redis 的复制是异步的:主库执行完写命令,返回客户端成功,然后再把命令发给从库。如果在“返回客户端”和“从库收到”这两步之间主库挂了,这部分数据就永久丢失了。
丢失量可以这么估:
丢失条数 ≈ 主库写入 QPS × 复制延迟同机房内网复制延迟通常在 1ms 以内,10 万 QPS 下丢失量在百条量级。跨机房 RTT 2 到 10ms,同样的写入量下,丢失量会到几千条。这个数字在选方案的时候必须摆到桌面上。
想要更小的窗口,Redis 提供了一个原语:WAIT。
SET order:1001 "paid" WAIT 1 100WAIT numreplicas timeout会阻塞当前连接,直到指定数量的副本确认收到了之前所有写命令,或者超时。它把异步复制在关键路径上变成了“近似同步”。代价是每次关键写多一个 RTT,吞吐会受明显影响。
我的用法是只在真正不能丢的写上调用WAIT,比如支付状态、库存扣减的最终标记,普通缓存写入完全不碰。全量使用WAIT会把 Redis 的吞吐优势抹平。
4. 异地与误删场景:延迟从库、冷备归档与备份生命周期
前面两层解决的是“机器和机房”,这一层解决的是“数据和人的手”。逻辑错误是所有灾备场景里最棘手的一类,因为它不触发任何技术告警,等你发现的时候错误已经被复制到所有副本上了。
4.1 误删场景没有捷径,只有时间点快照
必须先说清楚一件事:Redis 原生没有延迟复制参数。MySQL 有类似MASTER_DELAY的机制可以轻松搭出延迟从库,Redis 没有对应能力。想做一个“比主库慢一小时”的副本,只能借助外部同步工具做延迟应用,或者用定期快照来模拟。
想靠延迟从库救误删,需要满足两个条件:错误在一小时内被发现,以及延迟副本没有跟着执行同样的错误。第二个条件在脚本误连的场景里经常不成立,因为误操作往往是对着所有节点执行的。
所以我的实际方案更朴素,也更可靠:多时间点快照。具体做法是从库上定时触发快照并归档,比如每小时一份,保留最近 48 小时。误删发生时,你只需要找到事故发生前的那份快照。
一个很实用的技巧,很多人不知道:可以直接用redis-cli从远端拉一份 RDB,不需要登录服务器操作文件。
redis-cli -h 10.0.0.12 -p 6379 --rdb /backup/redis-$(date +%Y%m%d%H).rdb这个命令会触发服务端做一次无盘BGSAVE,然后通过网络把 RDB 流式拉到本地。它的好处是在从库上执行时对主库零影响,而且不需要在 Redis 服务器上预留大块磁盘空间。把它挂到定时任务里,就是一小时一份的归档。
还有一个让备份从库不被误提升的小技巧:把它的replica-priority设为 0。
replica-priority 0哨兵在选主时会跳过优先级为 0 的节点。这样专门用来做备份的从库永远不会被选为新主,避免它因为数据落后而成为生产主库。
4.2 备份文件往哪放:本地、异地与对象存储的分层
备份放置的深度决定了你能扛住哪一级事故:
| 层级 | 能防的事故 | 恢复速度 | 成本 |
|---|---|---|---|
| 与数据同盘 | 基本防不住什么 | 极快 | 极低 |
| 同机另一块盘 | 目录误删 | 快 | 低 |
| 同机房另一台机器 | 单机磁盘损坏、主机下线 | 分钟级 | 低 |
| 异地对象存储 | 机房级故障 | 取决于下载带宽 | 按量计费 |
实用的分层是:本地留最近两份用于快速回滚,异地对象存储保留完整历史用于兜底。参考 3-2-1 原则:至少 3 份副本、2 种不同介质、1 份异地。
上传用常见的命令行工具即可,例如:
rclone copy /backup/redis-2025010100.rdb remote:redis-backup/prod-a/上传完成后要做一次校验,把文件的 MD5 也记录下来。我吃过一次亏:上传中断留下了个半截文件,文件大小看着差不多,直到真需要恢复的时候才发现打不开。现在我的脚本里固定在上传后回读校验:
md5sum /backup/redis-2025010100.rdb # 与远端记录的 md5 比对一致才把备份标记为可用4.3 备份保留策略与版本兼容坑
保留策略我一般这么定:
- 每小时一份,保留 48 小时(覆盖“周末才发现”的场景)
- 每天一份,保留 14 天
- 每周一份,保留 8 周
- 每月一份,保留 12 个月
看起来量很大,但 Redis 的 RDB 经过压缩之后体积可控,对象存储的成本远低于一次数据丢失的代价。真正占成本的是下载流量和存储请求,不是容量本身。
版本兼容的坑值得单独强调,因为它会在最需要备份的时候咬你一口。
RDB 格式是向下兼容、向上不兼容的。用 7.2 写的 RDB,拿到 6.2 的实例上加载会直接失败。这不是参数问题,是格式版本问题,改配置没用。跨版本恢复有两条路:把目标实例升级到不低于源版本,或者用带格式转换能力的迁移工具做一次数据搬迁。
另外两个经常被忽略的兼容点:
databases数量。前面提过一次,这里再说一遍,因为它真的是高频错误。源实例 32 个库,目标实例默认 16 个,恢复出来的数据是不完整的,而且不一定报错。
配置文件本身的差异。备份里只有数据,没有maxmemory、appendonly、maxmemory-policy这些配置。恢复到一个配置完全不同的实例上,数据可能刚加载完就大批量过期或被淘汰。恢复前把配置文件也做一份版本化归档,和备份文件放在一起。
5. 恢复演练:灾备方案唯一的验收方式
所有前面讲的配置,如果没有验证过,都只能算“假设”。灾备方案最大的风险不是配置不足,而是你以为它能用。唯一的解法是定期演练,把它当成一次真实的故障处置。
5.1 演练脚本该怎么写
演练的目标不是“恢复成功”,而是“测量出真实的 RTO 和瓶颈位置”。所以脚本要记录时间线,而不只是最后结果。
我的演练脚本大致长这样:
#!/bin/bash set -e T0=$(date +%s) echo "[$(date)] 1. 下载最新备份" rclone copy remote:redis-backup/prod-a/latest.rdb /restore/ echo "[$(date)] 2. 校验文件" redis-check-rdb /restore/latest.rdb echo "[$(date)] 3. 准备干净的数据目录" rm -rf /restore/data && mkdir -p /restore/data cp /restore/latest.rdb /restore/data/dump.rdb chown -R redis:redis /restore/data echo "[$(date)] 4. 启动临时实例(端口 6399)" redis-server /restore/redis-restore.conf --daemonize yes sleep 2 echo "[$(date)] 5. 基础校验" redis-cli -p 6399 dbsize redis-cli -p 6399 info keyspace T1=$(date +%s) echo "[$(date)] 演练总耗时: $((T1-T0)) 秒"配合关键校验项:
dbsize与预估量级是否一致- 抽样 10 个业务关键 key,比对值
- 检查 TTL 是否保留(很多人只看值不看过期时间)
- 检查各 DB 的键分布
- 用真实业务代码连上去跑一个读请求
最后一步特别重要。我用过太多次开发环境验证通过、一到生产就出问题的情况,原因往往是客户端配置、连接串、序列化方式不一致。演练时一定要让业务方参与验证,而不是由运维单方面宣布“恢复成功”。
5.2 一次完整演练的时间线与瓶颈定位
下面是我经历过的某次演练的真实记录,数据量约 6GB:
| 阶段 | 耗时 | 说明 |
|---|---|---|
| 下载备份 | 3 分 20 秒 | 从对象存储拉 6GB,受带宽限制 |
| 校验文件 | 40 秒 | redis-check-rdb扫描全文件 |
| 拷贝与权限处理 | 15 秒 | 容易被忽略但必不可少 |
| 启动加载 | 55 秒 | RDB 加载,约 110MB/s |
| 业务验证 | 4 分钟 | 人工介入,最不可控 |
| 合计 | 约 9 分钟 |
这张表里最有价值的信息是:恢复时间的大头不在 Redis 加载,而在下载和人工确认环节。
加载 6GB 花了 55 秒,而下载花了 3 分多钟。这意味着想压缩 RTO,优化的方向不是调整 Redis 参数,而是:
- 把常用备份提前复制到同区域的存储,缩短下载路径
- 准备好恢复用的实例模板,演练时直接启动,而不是现场装 Redis
- 把业务验证做成自动化脚本,而不是靠人一条条点
我按这个思路优化之后,同样的数据量,RTO 从 9 分钟压到了 4 分半。这个提升幅度远超任何 Redis 参数调整能带来的收益。
5.3 演练里反复出现的几类翻车点
这几条都是我在演练中真实遇到过的,每一条都能让方案在关键时刻失效:
备份文件属主不对。从对象存储拉下来的是 root 属主,Redis 起不来或者加载到空目录。启动日志里只有一句“no such file”,非常容易被误认为是路径配错。
AOF 抢在 RDB 前面加载。恢复时appendonly yes,appendonlydir里留着半年前的旧 AOF,Redis 优先加载 AOF,恢复出来的数据完全是旧版本。这个坑我第一次遇到时排查了很久,因为dbsize有值,看起来“恢复成功”了。
备份从库的数据是落后的。恢复用的是从库的备份,但那台从库因为网络问题已经落后主库几小时,而且没人发现。所以演练时要检查从库的master_repl_offset与主库的差值,把“同步延迟”作为备份是否可用的前置判断条件。
maxmemory太小导致恢复后数据消失。数据加载进去没问题,但实例启动后立刻触发内存淘汰,dbsize几秒之内从几十万掉到几万。恢复前必须确认目标实例的内存规格不低于源实例。
连接串没改,业务还在连旧实例。这个听起来很傻,但在紧急处置时极其常见。演练结束后做过一次统计,五个团队里有三个没有整理过“连接串切换”的操作清单。
备份文件本身是空的。BGSAVE失败、磁盘写满、权限问题都可能留下一个 0 字节文件。所以备份脚本必须在生成之后检查文件大小和可用性,校验不通过就告警,而不是默默地“备份成功”。
6. 把配置落到文件里:一套可以直接套用的灾备参数清单
讲了这么多,最后把参数整理成可以直接放进配置文件的样子。下面这套是我在多个环境里用下来比较稳的基线,具体数值需要按实例规格和业务特征调整。
6.1 主库配置基线
# 持久化 - AOF appendonly yes appendfsync everysec no-appendfsync-on-rewrite yes aof-use-rdb-preamble yes aof-load-truncated yes aof-timestamp-enabled yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 512mb # 持久化 - RDB save 900 1 save 300 10 save 60 10000 rdbcompression yes rdbchecksum yes stop-writes-on-bgsave-error yes # 复制 repl-backlog-size 512mb repl-diskless-sync yes repl-diskless-sync-delay 5 repl-timeout 60 client-output-buffer-limit replica 1024mb 512mb 300 # 脑裂防护(交易、计数类必配) min-replicas-to-write 1 min-replicas-max-lag 10 # 连接 tcp-keepalive 300 timeout 06.2 备份从库的差异化配置
replica-read-only yes replica-serve-stale-data yes replica-priority 0 replica-announced yesreplica-priority 0保证它不会被哨兵选为主,replica-serve-stale-data yes让它即使与主库失联也还能响应读请求,方便做备份导出。
6.3 上线前的灾备自检表
| 检查项 | 判据 | 检查方式 |
|---|---|---|
| AOF 是否开启 | appendonly为 yes | CONFIG GET appendonly |
| 最近一次快照是否成功 | rdb_last_bgsave_status为 ok | INFO persistence |
| AOF 重写是否健康 | aof_last_bgrewrite_status为 ok | INFO persistence |
| backlog 是否够用 | 按写入速率估算 | CONFIG GET repl-backlog-size |
| 脑裂防护是否生效 | 两个参数均已配置 | CONFIG GET min-replicas-* |
| 从库同步延迟 | master_repl_offset差值在毫秒量级 | INFO replication |
| 备份是否出过本机 | 对象存储上有最近 24 小时的文件 | 备份系统核对 |
| 备份是否校验过 | 有 MD5 记录且回读一致 | 备份日志核对 |
| 恢复演练是否近期做过 | 90 天内至少一次 | 演练记录 |
| 连接串切换清单是否存在 | 有文档且演练时用过 | 文档核对 |
这张表我一般放在值班手册的第一页。它不需要任何工具,人工十分钟就能走完一遍,但能挡掉绝大多数“配置写了但没生效”“备份做了但没人知道能不能用”的问题。
一套完整的 Redis 灾备方案最终要回答的其实是三个问题:数据最多能丢多少、多久能恢复、最后一份可用的离线数据在哪。这三个问题只要有任何一个答不上来,方案就只能算完成了纸面作业。