Redis 实战(三):持久化出错 —— fork 失败、磁盘写满、AOF 膨胀与损坏,逐个复现(6.2.4)

这一篇讲什么

上一篇讲宕机丢多少数据。这一篇讲另一种情况:Redis 好好地活着,但持久化出错了 。它的表现往往不是丢数据,而是突然所有写入都报错:

vbnet 复制代码
MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. ...

这一篇把几种常见的成因逐个复现,每种都贴出日志原文、给出不用重启的恢复办法:

  1. 磁盘满,RDB 写不进去;
  2. 磁盘满,AOF 写不进去;
  3. fork: Cannot allocate memory,bgsave 根本没起来;
  4. AOF 文件比数据大几万倍;
  5. AOF 文件损坏,起不来。

实测环境 :CentOS 7.9(内核 3.10),Redis 6.2.4 源码编译(jemalloc-5.1.0),实验实例端口 6380。「磁盘满」是用一个 100M 的 loop 文件系统模拟的(可用 92M),Redis 的 dir 指向它。为了让 RDB 不被压缩得太小,磁盘满的实验用的是随机值。

1. 磁盘满,RDB 写不进去 → MISCONF

appendonly no,stop-writes-on-bgsave-error 是默认的 yes。灌 100 万个 key(内存 168.97M),然后 BGSAVE。日志:

bash 复制代码
# Write error saving DB on disk: No space left on device
# Background saving error

INFO persistence:

makefile 复制代码
rdb_last_bgsave_status:err
rdb_changes_since_last_save:1000000

这时候再写:

vbnet 复制代码
127.0.0.1:6380> set a 1
(error) MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.

SET、INCR 被拒,DEL 和 EXPIRE 也被拒 ,只有读命令能用。这和第一篇内存满的 OOM 不一样:OOM 时 DEL 还能执行,MISCONF 是「所有会修改数据的命令」都拒。想靠删 key 腾空间自救,这时候行不通。

Java 端(Spring Boot 4.1.1 / Lettuce)拿到的:

vbnet 复制代码
org.springframework.data.redis.RedisSystemException: Error in execution
  io.lettuce.core.RedisCommandExecutionException: MISCONF Redis is configured to save RDB snapshots, ...

还有一个容易误判的地方 :bgsave 失败后,写了一半的临时文件会被删掉,目录是空的,df 看已用空间也回到了原样。事后去看磁盘,看不出它满过 。判断依据只能是日志和 rdb_last_bgsave_status。

1.1 不重启怎么恢复

办法一:换个有空间的目录,再存一次。

bash 复制代码
127.0.0.1:6380> config set dir /works/redis-lab/p3/big
OK
127.0.0.1:6380> bgsave
Background saving started
vbnet 复制代码
* DB saved on disk
* Background saving terminated with success
rdb_last_bgsave_status:ok

一次成功的 bgsave 之后,写入自动恢复 ,不用重启。(CONFIG SET dir 只改运行时,要永久生效再执行 CONFIG REWRITE,或者改配置文件。)

办法二:临时关掉这个保护。

vbnet 复制代码
127.0.0.1:6380> config set stop-writes-on-bgsave-error no
OK
127.0.0.1:6380> set a 1
OK

能写了,但 rdb_last_bgsave_status 还是 err ------ 问题没解决,只是 Redis 不再拦你。这时候宕机,丢的是上次成功快照之后的全部数据。改回 yes,立刻又是 MISCONF。它只适合在你清理磁盘的那几分钟里让业务先跑着。

2. 磁盘满,AOF 写不进去

save "" 关掉 RDB,只开 appendonly yes、appendfsync everysec,同样灌 100 万个 key:

yaml 复制代码
errors: 261720, replies: 1000000

被拒的 26 万条,报错是另一句:

vbnet 复制代码
MISCONF Errors writing to the AOF file: No space left on device

INFO persistence:

makefile 复制代码
aof_last_write_status:err
aof_last_bgrewrite_status:err

2.1 日志在刷屏:每秒重写 5 次

日志里是这样的循环:

yaml 复制代码
* Starting automatic rewriting of AOF on 9221962000% growth
* Background append only file rewriting started by pid 2153
# Write error writing append only file on disk: No space left on device
# AOF write error looks solved, Redis can write again.
# Background AOF rewrite terminated with error
* Starting automatic rewriting of AOF on 9584625300% growth
...
# Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): No space left on device
# Background AOF rewrite terminated with error
* Starting automatic rewriting of AOF on 9584625300% growth
...

AOF 超过了自动重写的门槛(默认 auto-aof-rewrite-min-size 64mb),Redis 就去重写;重写也要写盘,写不进去就失败,然后马上再来。实测 10 秒内自动重写了 49 次 ------ 每一次都是一次 fork。日志 10 秒涨了约 19KB。

中间那句 AOF write error looks solved, Redis can write again. 别当真:它出现之后,写入照样是 MISCONF。

2.2 腾出空间后自动恢复

把那块盘扩到 400M(相当于清出了空间),5 秒后再看:

markdown 复制代码
* SYNC append only file rewrite performed
* Background AOF rewrite terminated with success
* Background AOF rewrite finished successfully
aof_last_write_status:ok

写入恢复,不用重启;AOF 还顺带被重写小了(95.8M → 79.6M)。之后重启校验,key 数一个不差,日志 DB loaded from append only file。

3. fork: Cannot allocate memory

bgsave 和 AOF 重写都要 fork 子进程。fork 失败的时候,子进程根本没起来,日志里是:

bash 复制代码
# Can't save in background: fork: Cannot allocate memory

3.1 怎么复现出来的

Redis 每次启动都会打这样一条警告(第一篇也见过):

bash 复制代码
# WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf ...

vm.overcommit_memory 是 Linux 决定「内存不够时还给不给分配」的策略:0 是估算,1 是总是给,2 是严格按上限算(上限是 CommitLimit,已经承诺出去的是 Committed_AS,都在 /proc/meminfo 里)。

我先在默认的 0 下起实例、灌了 1.16G 数据,再临时改成严格模式(overcommit_memory=2、overcommit_ratio=0,CommitLimit 约 3G),BGSAVE ------ 居然成功了 。看 /proc/meminfo,Redis 占了 1.2G 物理内存,Committed_AS 却几乎没涨。也就是说,这个实例的内存根本没被算进「已承诺」,fork 的时候也就不会因为超限被拒。为什么 jemalloc 分配的内存在 0 模式下没被计入,我没有去核实,这里只记现象。

换个顺序:先改成严格模式,再启动 Redis、灌数据 。这次灌完 1.00G,Committed_AS 从 1.21G 涨到了 2.31G;离上限 3.07G 只剩约 0.8G(832,896 kB),而 fork 一个 1G 的进程要再承诺约 1G:

ruby 复制代码
127.0.0.1:6380> bgsave
(error) ERR

客户端只收到一个 ERR,没有任何说明。原因在日志里:

bash 复制代码
# Can't save in background: fork: Cannot allocate memory

随后 rdb_last_bgsave_status:err,所有写入都是 MISCONF ------ 和第 1 节磁盘满时一模一样的报错,但原因完全不同。所以看到 MISCONF,一定要去翻日志,看是 No space left on device 还是 fork: Cannot allocate memory。

(实验期间这台机器的 overcommit_memory 只是临时改的,做完立即改回 0 / 50。)

3.2 恢复

overcommit_memory 改回 0 以后,再 BGSAVE:

vbnet 复制代码
* Background saving started by pid 3018
* DB saved on disk
* Background saving terminated with success
rdb_last_bgsave_status:ok

写入随即恢复,同样不用重启。

排查顺序 :日志里找到 fork: Cannot allocate memory → sysctl vm.overcommit_memory 看当前模式 → grep -E "CommitLimit|Committed_AS" /proc/meminfo 看离上限还有多远 → 再比一下 Redis 的 used_memory。如果是 2,fork 一个多大的 Redis,就要再有多大的余量。

4. AOF 文件比数据大几万倍

只有一个 key,对它 INCR 300 万次。先把自动重写关掉(auto-aof-rewrite-percentage 0)看看会怎样:

ini 复制代码
dbsize=1   used_memory_human:851.59K
appendonly.aof  81,000,023 字节

数据只有一个数字,AOF 有 81MB ------ AOF 记的是每一次修改,同一个 key 改 300 万次就记 300 万条。手动重写:

ruby 复制代码
127.0.0.1:6380> bgrewriteaof
Background append only file rewriting started

116 毫秒后,AOF 变成 111 字节。

4.1 开着自动重写,也可能很大

改回默认(auto-aof-rewrite-percentage 100、auto-aof-rewrite-min-size 64mb),连续 7 批、每批 300 万次 INCR:

ini 复制代码
第 1 批后 aof=81000157  base=111
第 2 批后 aof=93338370  base=17299142
第 3 批后 aof=105819421 base=57270239
第 4 批后 aof=70952107  base=51567623
第 5 批后 aof=151952130 base=51567623
第 6 批后 aof=125125659 base=56006342
第 7 批后 aof=89988970  base=49560389

日志里自动重写触发了好几次(Starting automatic rewriting of AOF on 102% growth 等),但在持续高频写入时,文件一直在 70M ~ 152M 之间;重写完的大小(aof_base_size)也有十几到几十 MB。写入停下以后再手动 BGREWRITEAOF,又回到 111 字节。

所以「AOF 文件太大」先看三样:INFO persistence 里的 aof_current_size 和 aof_base_size,以及 CONFIG GET auto-aof-rewrite-* ------ 是没开自动重写,还是写入一直很密。在业务低峰手动 BGREWRITEAOF 是最直接的。

4.2 运行中关掉 AOF,重启后数据回退

想关 AOF,很多人会直接 CONFIG SET appendonly no:

arduino 复制代码
127.0.0.1:6380> config set appendonly no
OK
aof_enabled:0

AOF 文件还留在目录里,不再更新。之后又写了 6 次(counter 到 24000006,新 key after-disable)。配置文件里还是 appendonly yes,重启(这个实验实例 save "",不开 RDB):

go 复制代码
重启后:counter=24000000  after-disable=(nil)  aof_enabled=1
* DB loaded from append only file: 0.000 seconds

重启后 AOF 又开了,加载的是那个停在关闭时刻的旧文件,关掉之后的写入全没了。 运行中改了 appendonly,一定要 CONFIG REWRITE,让配置文件跟着变。

5. AOF 损坏

写 1000 条 SET,正常关闭,得到一个 33,809 字节的 AOF。

5.1 尾部少了一截:能起来,丢最后一条

把文件末尾截掉 10 个字节(相当于最后一条命令只写了一半),启动:

ini 复制代码
# !!! Warning: short read while loading the AOF file !!!
# !!! Truncating the AOF at offset 33773 !!!
# AOF loaded anyway because aof-load-truncated is enabled

dbsize 999,最后一个 key 没了。这是 aof-load-truncated yes(默认)的效果:尾部不完整,就截掉那一截照样启动。

5.2 中间坏了:起不来

把文件中间(偏移 16025)的一个字节改坏,启动失败,日志:

bash 复制代码
# Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>

先检查:

vbnet 复制代码
$ redis-check-aof appendonly.aof
0x            3e99: Expected \r\n, got: 780d
AOF analyzed: size=33809, ok_up_to=16025, ok_up_to_line=3345, diff=17784
AOF is not valid. Use the --fix option to try fixing it.

再修:

python 复制代码
$ redis-check-aof --fix appendonly.aof
This will shrink the AOF from 33809 bytes, with 17784 bytes, to 16025 bytes
Continue? [y/N]: y
Successfully truncated AOF

修完能启动了,dbsize 是 477 ------ 原来 1000 个 。--fix 的做法是把坏点之后的内容全部截掉,只坏了 1 个字节,丢了 523 个 key。

所以:

  1. 修之前先备份 ,日志里也是这么说的(make a backup of your AOF file);
  2. 先跑不带 --fix 的 redis-check-aof,看 ok_up_to 在哪、diff 有多大 ------ diff 就是 --fix 会扔掉的字节数(这次 17,784 字节,对应 523 个 key)。

6. 速查

报错 / 现象 先看 原因 不重启怎么恢复
MISCONF Redis is configured to save RDB snapshots ... 日志、rdb_last_bgsave_status RDB 写失败:磁盘满(No space left on device)或 fork 失败 解决原因后 BGSAVE 成功一次即恢复;临时可 stop-writes-on-bgsave-error no
MISCONF Errors writing to the AOF file: No space left on device aof_last_write_status AOF 写不进去 腾出空间,几秒内自动恢复
日志里 Starting automatic rewriting of AOF 一直刷 磁盘空间 盘满,自动重写不停重试 同上
BGSAVE 只回 ERR 日志 Can't save in background: fork: Cannot allocate memory 内存承诺超限(overcommit_memory) 调整 overcommit_memory 或腾内存,再 BGSAVE
AOF 比数据大很多 aof_current_size / aof_base_size、auto-aof-rewrite-* 反复修改同一批 key,没重写 BGREWRITEAOF
关了 AOF,重启后数据回退 配置文件里的 appendonly 只 CONFIG SET 没写回配置 CONFIG REWRITE
启动报 Bad file format reading the append only file redis-check-aof 的 ok_up_to / diff AOF 中间损坏 先备份;--fix 会截掉坏点之后的全部
启动日志 short read while loading the AOF file --- AOF 尾部不完整 aof-load-truncated yes 时会自动截掉尾巴启动

下一篇:启动失败和「已经启动了却连不上」------ bind、protected-mode、防火墙、daemonize 的「没反应」,以及源码编译失败。

相关推荐
学长毕设2 小时前
基于SpringBoot的网上流浪狗救助捐赠平台-附源码
java·spring boot·后端
UNIHHIOT2 小时前
开源一套工业物联网平台:驱动崩了不影响别的协议,64MB 堆跑边缘网关
前端·后端
废柴程序员_熊熊2 小时前
DeepSeek Harness 实测:两条命令装好,11 步写完 632 行代码,3 个测试全绿
后端
phil62 小时前
沙箱核心功能系列 05:沙箱内 Agent 与进程执行 API
后端
用户5947614425952 小时前
Windows 部署 Playwright MCP 供 linux Codex 远程操作 Chrome:403 Forbidden 与 0 Tools 问题排查
后端
用户394995084632 小时前
Python 的哈希契约:为什么 list 不能当字典的键
后端
leo在掘金2 小时前
744B模型,32GB内存跑起来
后端·架构
dpharness2 小时前
我亲手跑了一遍存量统计,兜底类居然排到第二
后端