我不会起名字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 快照时,不是在主线程里慢慢把数据写到磁盘,而是:
- fork 出一个子进程;
- 子进程负责把内存数据写入 RDB 文件;
- 主进程继续处理请求。
听起来很优雅——但 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:
- 主进程读某页 → 共享,不复制;
- 主进程写某页 → 内核复制该页,主进程写新页,子进程仍看到旧页。
后果:
- 内存占用上涨:RDB 期间,被修改的页面会被复制,额外内存最多可达"写入比例 × 总内存";
- 延迟抖动:每次写触发 COW,都要做一次内存页复制(4KB memcpy),频繁写时开销可观;
- 可能 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 | 总是允许 overcommit | Redis 推荐: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-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 量化
# 一次性采集 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 期间的内存涨幅。
七、总结
- RDB 落盘不是"主线程慢慢写",而是 fork 子进程 + COW,代价在 fork 本身和后续的页复制;
- 3 个放大因素:实例内存越大 fork 越慢、写操作触发 COW 页复制、
vm.overcommit_memory配错导致 fork 失败; - 量化指标是
latest_fork_usec(>10ms 要警惕); - 最彻底的方案是主从分离落盘(主库零 fork);
vm.overcommit_memory必须设为 1,否则大内存实例可能根本 fork 不出来。
留个问题给你:你们 Redis 的latest_fork_usec现在是多少微秒?主库是自己落盘,还是只让从库落盘?