这一篇讲什么
上一篇讲宕机丢多少数据。这一篇讲另一种情况:Redis 好好地活着,但持久化出错了 。它的表现往往不是丢数据,而是突然所有写入都报错:
vbnet
MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. ...
这一篇把几种常见的成因逐个复现,每种都贴出日志原文、给出不用重启的恢复办法:
- 磁盘满,RDB 写不进去;
- 磁盘满,AOF 写不进去;
fork: Cannot allocate memory,bgsave 根本没起来;- AOF 文件比数据大几万倍;
- 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。
所以:
- 修之前先备份 ,日志里也是这么说的(
make a backup of your AOF file); - 先跑不带
--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 的「没反应」,以及源码编译失败。