☰
Redis RDB 落盘时的 fork 停顿:COW 机制与 3 个降低影响的配置
2026/10/17 10:36:27 网站建设 项目流程

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

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

文章目录

  • Redis RDB 落盘时的 fork 停顿:COW 机制与 3 个降低影响的配置
    • 一、先把 fork + COW 的机制讲清
    • 二、因素 1:实例内存越大,fork 越慢
    • 三、因素 2:写操作触发 COW 页复制
    • 四、因素 3:vm.overcommit_memory 配错
    • 五、配置层面的 3 个改善方案
    • 六、用 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 时要逐个复制。

# 看最近一次 fork 耗时(微秒)redis-cli INFO stats|greplatest_fork_usec# 典型值:# 小实例(1GB):几百微秒# 大实例(16GB+):几毫秒甚至几十毫秒
# 观察 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。
# 看 fork 期间的 COW 次数redis-cli INFO stats|grep-E"rdb_changes_since_last_save"# 直接看内存变化(在 RDB 期间快速执行多次)redis-cli INFO memory|grepused_memory_human

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

四、因素 3:vm.overcommit_memory 配错

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

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

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

# 临时设置sysctl-wvm.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-rewriteAOF 重写期间不 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 量化

# 一次性采集 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现在是多少微秒?主库是自己落盘,还是只让从库落盘?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询