
我不会起名字322· 后端 / 算法 / 数据库 技术栈 力扣 Hot100 Go 项目 Redis MySQL文章目录Redis RDB 落盘时的 fork 停顿COW 机制与 3 个降低影响的配置一、先把 fork COW 的机制讲清二、因素 1实例内存越大fork 越慢三、因素 2写操作触发 COW 页复制四、因素 3vm.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 的实例页表可能有几十万个条目每页 4KBfork 时要逐个复制。# 看最近一次 fork 耗时微秒redis-cli INFO stats|greplatest_fork_usec# 典型值# 小实例1GB几百微秒# 大实例16GB几毫秒甚至几十毫秒# 观察 fork 相关的统计redis-cli INFO stats|grep-Efork|rdb_lastredis-cli INFO persistence|grep-Erdb_|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-Erdb_changes_since_last_save# 直接看内存变化在 RDB 期间快速执行多次redis-cli INFO memory|grepused_memory_human判据RDB 期间used_memory明显上涨且与这期间的写量相关。四、因素 3vm.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_memory1# 永久写入 /etc/sysctl.confechovm.overcommit_memory 1/etc/sysctl.conf判据Redis 日志里出现Cant 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这样主库完全不做 forkRDB 的负担转移到从库。方案 1 的具体建议# ❌ 过于频繁 save 60 10000 # 一分钟一万次写就落盘 → 高频 fork # ✅ 减少频率或用大间隔 save 900 1 # 15 分钟至少 1 次写 save 300 10六、用 latest_fork_usec 量化# 一次性采集 fork 相关指标redis-cli INFO|grep-Elatest_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/errerr 说明落盘失败一个实测对照的思路在 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_usec10ms 要警惕最彻底的方案是主从分离落盘主库零 forkvm.overcommit_memory必须设为 1否则大内存实例可能根本 fork 不出来。留个问题给你你们 Redis 的latest_fork_usec现在是多少微秒主库是自己落盘还是只让从库落盘