Redis RDB 落盘时的 fork 停顿:COW 机制与 3 个降低影响的配置

我不会起名字322 · 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • [Redis RDB 落盘时的 fork 停顿:COW 机制与 3 个降低影响的配置](#Redis RDB 落盘时的 fork 停顿:COW 机制与 3 个降低影响的配置)
    • [一、先把 fork + COW 的机制讲清](#一、先把 fork + COW 的机制讲清)
    • [二、因素 1:实例内存越大,fork 越慢](#二、因素 1:实例内存越大,fork 越慢)
    • [三、因素 2:写操作触发 COW 页复制](#三、因素 2:写操作触发 COW 页复制)
    • [四、因素 3:vm.overcommit_memory 配错](#四、因素 3:vm.overcommit_memory 配错)
    • [五、配置层面的 3 个改善方案](#五、配置层面的 3 个改善方案)
    • [六、用 latest_fork_usec 量化](#六、用 latest_fork_usec 量化)
    • 七、总结

Redis RDB 落盘时的 fork 停顿:COW 机制与 3 个降低影响的配置

"每天凌晨三点,接口 P99 会从 20ms 抖到 300ms,持续一两秒,然后就恢复了。"

这个现象持续了一周,直到我把时间点和 Redis 的 RDB 落盘对上------每天凌晨 3 点整,正好是 save 策略触发 RDB 快照的时刻。

问题就出在 fork。这篇讲清 fork + COW(写时复制)的机制,拆 3 个放大停顿的因素,给出配置层面的改善方案。

一、先把 fork + COW 的机制讲清

Redis 做 RDB 快照时,不是在主线程里慢慢把数据写到磁盘,而是:

  1. fork 出一个子进程;
  2. 子进程负责把内存数据写入 RDB 文件;
  3. 主进程继续处理请求。

听起来很优雅------但 fork 本身有代价:

两个关键代价:

  • fork 本身的停顿 :内核要复制父进程的页表(虚拟地址到物理地址的映射)。内存越大,页表条目越多,复制越慢 。这就是 latest_fork_usec 指标的含义。
  • COW 的内存复制 :fork 后父子进程共享内存页,任一进程修改 某页,内核就复制一份。RDB 期间主进程的所有写操作都会触发 COW,额外占用内存,还可能触发延迟抖动。

二、因素 1:实例内存越大,fork 越慢

fork 的停顿时间与页表大小成正比。一个 16GB 的实例,页表可能有几十万个条目(每页 4KB),fork 时要逐个复制。

bash 复制代码
# 看最近一次 fork 耗时(微秒)
redis-cli INFO stats | grep latest_fork_usec
# 典型值:
# 小实例(1GB):几百微秒
# 大实例(16GB+):几毫秒甚至几十毫秒
bash 复制代码
# 观察 fork 相关的统计
redis-cli INFO stats | grep -E "fork|rdb_last"
redis-cli INFO persistence | grep -E "rdb_|latest_fork"

判据 :latest_fork_usec 就是最直接的证据。如果它达到几万微秒(几十毫秒),P99 抖动就解释得通了。

注意 :latest_fork_usec 只记录最近一次,所以要连续采样几次,最好在 RDB 刚触发后立刻看。

三、因素 2:写操作触发 COW 页复制

fork 之后,RDB 子进程需要看到"fork 那一刻的完整数据快照"。但我们不能真的复制整个内存,所以用 COW:

  • 主进程读某页 → 共享,不复制;
  • 主进程写某页 → 内核复制该页,主进程写新页,子进程仍看到旧页。

后果:

  1. 内存占用上涨:RDB 期间,被修改的页面会被复制,额外内存最多可达"写入比例 × 总内存";
  2. 延迟抖动:每次写触发 COW,都要做一次内存页复制(4KB memcpy),频繁写时开销可观;
  3. 可能 OOM :如果 RDB 期间写入量很大,内存翻倍,可能撞 maxmemory 或容器 limit。
bash 复制代码
# 看 fork 期间的 COW 次数
redis-cli INFO stats | grep -E "rdb_changes_since_last_save"
# 直接看内存变化(在 RDB 期间快速执行多次)
redis-cli INFO memory | grep used_memory_human

判据 :RDB 期间 used_memory 明显上涨,且与"这期间的写量"相关。

四、因素 3:vm.overcommit_memory 配错

这是 Linux 层面的配置,配错会让 fork 直接失败:

bash 复制代码
cat /proc/sys/vm/overcommit_memory
值 行为 对 Redis fork 的影响
0 启发式(默认) 大内存实例 fork 可能被拒
1 总是允许 overcommit Redis 推荐:fork 不受内存检查限制
2 严格限制 极易 fork 失败

Redis 官方明确建议设为 1。原因:fork 时子进程会"承诺"使用与父进程相同的内存地址空间,但因为 COW,实际物理内存不会立即翻倍。内核如果按"最坏情况"检查,就会拒绝 fork。

bash 复制代码
# 临时设置
sysctl -w vm.overcommit_memory=1
# 永久(写入 /etc/sysctl.conf)
echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf

判据:Redis 日志里出现

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

或启动时的警告

复制代码
WARNING overcommit_memory is set to 0! Background save may fail under low memory condition.

五、配置层面的 3 个改善方案

方案 做法 效果 代价
调整 save 策略 减少 RDB 频率,或改用 AOF 减少 fork 次数 AOF 文件更大
主从分离落盘 主库不落盘,只在从库配 save 主库零 fork 需要主从架构
no-appendfsync-on-rewrite AOF 重写期间不 fsync 减少磁盘 IO 争抢 极端情况下可能丢数据

方案 2 是最彻底的------生产环境的常见做法:

复制代码
# 主库 redis.conf:关闭所有 RDB 落盘
save ""

# 从库 redis.conf:只从库落盘
save 900 1
save 300 10
save 60 10000

这样主库完全不做 fork,RDB 的负担转移到从库。

方案 1 的具体建议:

复制代码
# ❌ 过于频繁
save 60 10000      # 一分钟一万次写就落盘 → 高频 fork

# ✅ 减少频率,或用大间隔
save 900 1         # 15 分钟至少 1 次写
save 300 10

六、用 latest_fork_usec 量化

bash 复制代码
# 一次性采集 fork 相关指标
redis-cli INFO | grep -E "latest_fork_usec|rdb_last_bgsave_status|rdb_last_bgsave_time_sec|rdb_changes_since_last_save"
指标 含义 关注点
latest_fork_usec 最近一次 fork 耗时(微秒) >10000 就要关注
rdb_last_bgsave_time_sec 上次 bgsave 耗时(秒) 越长说明数据越多
rdb_changes_since_last_save 距上次保存的变更数 影响 COW 内存
rdb_last_bgsave_status 上次结果(ok/err) err 说明落盘失败

一个实测对照的思路 :在 RDB 触发前后各采一次 used_memory 和 latest_fork_usec,就能量化"这次 fork 花了多久、COW 涨了多少内存"。

结论分三档:

  • 现象 :P99 抖动与 RDB 触发时间精确对齐,latest_fork_usec 达数万微秒;
  • 已排除:非 RDB 时段的延迟正常(说明与持续负载无关);
  • 待确认:具体是"fork 停顿"还是"COW 内存压力"为主 ------ 需对比 RDB 期间的内存涨幅。

七、总结

  1. RDB 落盘不是"主线程慢慢写",而是 fork 子进程 + COW,代价在 fork 本身和后续的页复制;
  2. 3 个放大因素:实例内存越大 fork 越慢 、写操作触发 COW 页复制 、vm.overcommit_memory 配错导致 fork 失败;
  3. 量化指标是 latest_fork_usec(>10ms 要警惕);
  4. 最彻底的方案是主从分离落盘(主库零 fork);
  5. vm.overcommit_memory 必须设为 1,否则大内存实例可能根本 fork 不出来。

留个问题给你 :你们 Redis 的 latest_fork_usec 现在是多少微秒?主库是自己落盘,还是只让从库落盘?

相关推荐
蓝胖的四次元口袋1 小时前
Redis 知识梳理(4)
数据库·redis
我不会起名字3222 小时前
Redis 从 0 到 1(十一):缓存穿透/击穿/雪崩三连击,Go 3 套防御代码
redis·缓存·go·布隆过滤器·雪崩
hweiyu0015 小时前
Redis命令:LMOVE
redis·缓存
bug菌16 小时前
一个周末就想把短链服务推到 10 万 QPS,真能靠加 Redis 做到吗?
java·redis·go
程序员Sunday18 小时前
Redis 缓存穿透、击穿、雪崩怎么区分?从回源压力找原因
数据库·redis·缓存
guo_wen_qiang19 小时前
redis实现秒杀功能-超卖&库存遗留问题的解决
数据库·redis·缓存
逐流人19 小时前
Redis内存数据库:从数据类型到集群部署
linux·运维·数据库·redis·缓存·云计算
.Bryce.21 小时前
三协议合一实战:用MCP+A2A+ANP搭建一个智能研究助手
网络·redis·agent·codex·mcp·a2a·anp
大侠归来21 小时前
Redis Cluster 的 16384 个槽(Slot)是什么?
数据库·redis·缓存