我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [Redis 突然只能读不能写:MISCONF 报错的 3 类触发条件与 4 步恢复](#Redis 突然只能读不能写:MISCONF 报错的 3 类触发条件与 4 步恢复)
-
- 一、这条报错到底在说什么
- [二、3 类触发条件](#二、3 类触发条件)
-
- [条件 1:磁盘写不进去(最常见)](#条件 1:磁盘写不进去(最常见))
- [条件 2:fork 失败(容器里高频)](#条件 2:fork 失败(容器里高频))
- [条件 3:你连的是只读从节点(报错不同,但现象一样)](#条件 3:你连的是只读从节点(报错不同,但现象一样))
- 三、命令清单
- [四、4 步恢复(第 2 步最容易被做错)](#四、4 步恢复(第 2 步最容易被做错))
- 五、配置模板(防再犯)
- 六、总结
Redis 突然只能读不能写:MISCONF 报错的 3 类触发条件与 4 步恢复
线上 Redis 一点征兆没有,突然所有写入都失败了,客户端抛的是这一句:
text
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.
Please check Redis logs for details about the error.
读取完全正常,只有写入被拒。更让人困惑的是:Redis 明明是内存数据库,为什么写不进去要怪「磁盘」?
答案在最后一句配置里:Redis 为了保护持久化的一致性,在 RDB 快照失败时会主动关闭写命令 。这不是 Redis 坏了,是一道保护机制起作用了------它宁可让你写不进去,也不愿意让你以为数据已经落盘、实际上没有。
这篇文章讲清这道保护机制的 3 类触发条件,以及真正正确的 4 步恢复动作(其中第 2 步在网上被大量错误地当成「解决方案」)。
一、这条报错到底在说什么
触发逻辑只有一条:
text
Redis 配置了 RDB 快照(save 指令)
↓
stop-writes-on-bgsave-error = yes(默认值,生产建议保持 yes)
↓
最近一次 BGSAVE 失败了
↓
所有可能修改数据集的命令被拒绝(SET/DEL/INCR/LPUSH...)
GET 之类的读命令不受影响
所以排查的第一步不是 改配置,而是去问:上一次 BGSAVE 为什么失败?
bash
# 看持久化状态,重点看这两个字段
redis-cli INFO persistence
# rdb_last_bgsave_status:err ← 上一次快照失败
# rdb_last_save_time:1696300000 ← 最后一次成功的时间戳
二、3 类触发条件
条件 1:磁盘写不进去(最常见)
BGSAVE 要把内存里的数据写成 RDB 文件,写不进去的常见原因:
- 磁盘满了 (
df -h一看就知道,但往往是别的进程把盘占满了); - 目录没有写权限 (Redis 进程用户 ≠
dir的所有者,或者用了只读挂载); dir配置指向了一个不存在的路径(改过配置文件但没建目录);- 磁盘配额(quota)或 inode 用尽 (
df -i)。
日志里通常长这样:
text
# Write error saving DB on disk: No space left on device
# Error trying to save the DB, can't exit.
条件 2:fork 失败(容器里高频)
BGSAVE 靠 fork() 子进程来做快照。fork 失败时日志是:
text
# Can't save in background: fork: Cannot allocate memory
两个高频成因:
vm.overcommit_memory = 0:内核保守估计,觉得 fork 一个和父进程一样大的进程可能超内存,直接拒绝。Redis 官方明确要求设成 1:
bash
sysctl vm.overcommit_memory=1
echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf
- 容器/cgroup 限制:Redis 在容器里,fork 瞬间的内存需求超过 cgroup limit(哪怕实际用的是 Copy-On-Write,内核在限定时仍可能按最坏情况判断)。这种场景要么放宽容器内存,要么把 Redis 放到内存更充裕的节点上。
顺带一个相关现象:INFO stats 里的 latest_fork_usec 很大(比如超过 1 秒),说明 fork 很慢------fork 期间主进程是阻塞的,这也是 Redis 延迟尖刺的常见来源。
条件 3:你连的是只读从节点(报错不同,但现象一样)
有一类「只能读不能写」不是 MISCONF,报错是:
text
READONLY You can't write against a read only replica.
但它经常和上面那类混淆,因为现象完全一致:读正常、写失败。区分方法很简单:
bash
redis-cli INFO replication
# role:slave ← 说明你连的是从节点
常见成因:客户端连错了地址(主从切换后VIP 指向了从节点)、或者用了读写分离的客户端却把写请求发到了读连接上。这一类改 stop-writes-on-bgsave-error 完全没有用,必须换连接。
三、命令清单
| 命令 | 作用 | 看什么 |
|---|---|---|
redis-cli INFO persistence |
持久化状态 | ⭐ rdb_last_bgsave_status 是不是 err |
| `redis-cli INFO stats | grep latest_fork_usec` | fork 耗时 |
redis-cli CONFIG GET dir / dbfilename |
落盘路径 | 路径是否存在、是否可写 |
df -h <dir> / df -i <dir> |
磁盘空间 / inode | 是否满了 |
ls -ld <dir> |
目录权限 | Redis 进程用户有没有写权限 |
tail -n 200 <logfile> |
服务端日志 | ⭐ 真正的原因只在这里 |
redis-cli CONFIG GET save |
快照策略 | 是不是配了 save ""(关掉 RDB) |
redis-cli CONFIG GET stop-writes-on-bgsave-error |
保护开关 | 默认 yes |
redis-cli BGSAVE |
手动触发一次 | 用来验证修复是否成功 |
redis-cli LASTSAVE |
最后成功时间 | 与当前时间对比,看多久没成功过了 |
redis-cli INFO replication |
主从角色 | 排除「连到了从节点」 |
sysctl vm.overcommit_memory |
内核参数 | 应该是 1 |
redis-cli CONFIG GET rename-command |
命令重命名 | 有没有把 SAVE/BGSAVE 改掉(某些加固规范会这么做) |
四、4 步恢复(第 2 步最容易被做错)
第 1 步:看日志,拿到真实原因。
bash
tail -n 200 /var/log/redis/redis-server.log | grep -iE "error|fail|fork|save"
这一步不能跳。磁盘满、权限不足、fork 失败,三者的修复动作完全不同。凭猜改配置只会把问题盖住。
第 2 步:恢复写入(这是临时动作,不是修复)。
bash
redis-cli CONFIG SET stop-writes-on-bgsave-error no
网上大量教程到此结束,但这句话的真实含义是:「Redis 你别再拦我了,数据能不能落盘我不在乎了」 。它是拆掉烟雾报警器,不是灭火。执行之后:
- 写入立刻恢复 ✅
- 但你的数据处于没有成功持久化的状态 ❌ ------ 此刻宕机,从最后一次成功的 RDB 之后的所有写入都会丢
所以第 2 步和第 3 步必须连着做,中间不能隔夜。
第 3 步:按第 1 步找到的原因修根因。
| 根因 | 修复动作 |
|---|---|
| 磁盘满 | 清理空间(优先清 dir 之外的东西:日志、core dump、旧备份),并加磁盘告警 |
| 目录不可写 | chown redis:redis <dir>;确认不是只读挂载 |
dir 不存在 |
建目录并 CONFIG SET dir <path> + CONFIG REWRITE |
| fork 失败 | sysctl vm.overcommit_memory=1;容器场景放宽内存限制 |
| 连的是从节点 | 改客户端连接地址,READONLY 与 MISCONF 是两回事 |
BGSAVE 被 rename 禁用 |
改回 rename-command 配置,或改用手动备份方式 |
第 4 步:验证,并把保护开关开回去。
bash
redis-cli BGSAVE # 手动跑一次
redis-cli INFO persistence | grep rdb_last_bgsave_status # 应该是 ok
redis-cli LASTSAVE # 时间戳应该刚刚更新
# ⭐ 验证通过后,把保护重新打开
redis-cli CONFIG SET stop-writes-on-bgsave-error yes
redis-cli CONFIG REWRITE
只有 rdb_last_bgsave_status:ok 之后再把保护开回 yes,才是真正恢复了。
五、配置模板(防再犯)
conf
# ===== 落盘路径:指向一个确定存在、确定可写、空间充足的目录 =====
dir /data/redis
dbfilename dump.rdb
# ===== 快照策略:按业务可容忍的丢失窗口来配 =====
save 900 1 # 15 分钟内至少 1 个 key 变化
save 300 10 # 5 分钟内至少 10 个 key 变化
save 60 10000 # 1 分钟内至少 10000 个 key 变化
# 缓存型 Redis 可以干脆关掉 RDB:save "" (但要清楚这意味着重启即丢)
# ===== 保护开关:生产保持 yes =====
stop-writes-on-bgsave-error yes
# ===== AOF:对数据安全性要求高时开启 =====
appendonly yes
appendfsync everysec
# ===== 日志:出事时唯一的证据来源 =====
logfile /var/log/redis/redis-server.log
loglevel notice
配套的两条运维纪律:
- 磁盘使用率告警阈值设到 80%,不要等 95% 才告警------Redis 快照需要连续空间,等到快满时往往已经来不及;
- 监控
rdb_last_bgsave_status(不是只看写入是否可用)。它变成err就是明确的告警信号,比"接口报错"早得多。
六、总结
- MISCONF 不是故障,是保护机制生效了 :
stop-writes-on-bgsave-error yes在 BGSAVE 失败时主动拒写,防止你以为数据已落盘。读命令不受影响,所以现象是「能读不能写」。 - 3 类触发条件 :① 磁盘/权限/路径导致写不进去(最常见);② fork 失败(
vm.overcommit_memory=0或容器内存限制);③ 你连的是只读从节点(报错是READONLY,与 MISCONF 无关,改保护开关没用)。 CONFIG SET stop-writes-on-bgsave-error no只是临时恢复手段,等于拆掉烟雾报警器。真正的修复是去服务端日志里找出 BGSAVE 失败的原因并处理掉。- 恢复流程闭环 :看日志定位 → 临时放开写入 → 修根因 → 手动
BGSAVE验证rdb_last_bgsave_status:ok→ 把保护开回yes并CONFIG REWRITE。 - 防再犯靠两件事 :磁盘告警阈值设 80%,以及把
rdb_last_bgsave_status纳入监控------它变err时接口还没开始报错,那才是最佳处理窗口。
本文基于 Redis 6/7 的行为;
stop-writes-on-bgsave-error的默认值、字段名在各版本一致,但INFO persistence的细分字段在不同版本略有差异,以你实际版本的输出为准。