bgsave阻塞主进程的根本原因是fork时内核需同步复制页表,page cache中大量脏页会激增cow开销;fsync则因等待脏页刷盘而不可控延迟。

Page Cache 会直接干扰 Redis 的 RDB fork 和 AOF fsync,这是毛刺的根源,不是配置没调好,而是内核机制和应用行为天然冲突。
Redis bgsave 时 fork 阻塞的根本原因
Redis 执行 bgsave 时,内核需为子进程复制父进程的虚拟内存页表。但 Page Cache 中大量脏页(尤其是 AOF 文件或 RDB 临时文件所在页)会导致 fork() 触发写时复制(Copy-on-Write)开销激增——不是复制数据,而是为每个脏页建立独立映射、更新页表项、刷新 TLB。这个过程是同步且不可中断的。
常见错误现象:
- 监控看到
bgsave启动后主线程卡顿 100–500ms,redis-cli --latency出现尖峰 -
/proc/[pid]/status中mm->nr_ptes和mm->nr_pmds值极大,说明页表复杂度高
关键点:
- Page Cache 脏页越多,fork 开销越大;而 Redis 持续写 AOF 就在不断制造脏页
- 即使你禁用 AOF,RDB 生成前的临时文件写入(如
temp-xxx.rdb)也会被缓存并变脏 -
vm.swappiness=0对此无效,因为问题不在 swap,而在页表克隆
fsync() 被 Page Cache 拖慢的真实表现
AOF 模式下,Redis 默认使用 everysec 策略:主线程把命令追加到内核缓冲区,后台线程每秒调用一次 fsync()。但 fsync() 实际要等 Page Cache 中对应文件的所有脏页刷盘完成才返回。当磁盘负载高或脏页堆积多时,fsync() 可能阻塞数百毫秒。
容易踩的坑:
- 误以为
appendfsync everysec= “每秒最多一次 fsync”,实际是“每秒触发一次 fsync 调用”,但每次调用耗时不可控 - 用
iostat -x 1观察到await正常,却仍有毛刺——因为阻塞发生在fsync()等待脏页落盘,而非 I/O 提交阶段 -
echo 1 > /proc/sys/vm/dirty_background_ratio设太低,会导致内核频繁唤醒pdflush,反而加剧 I/O 抢占
参数差异影响显著:
-
dirty_ratio(默认 20)决定脏页上限:超过则所有写进程同步阻塞等待刷盘,Redis 写命令直接 hang 住 -
dirty_expire_centisecs(默认 3000)控制脏页“保质期”:超时未刷就强制进入回写队列,但不保证立即执行
为什么 drop_caches 解决不了问题
有人试过在 bgsave 前执行 echo 3 > /proc/sys/vm/drop_caches,发现毛刺更严重。这是因为:
- 清空 Page Cache 后,
bgsave第一次读取 RDB 模板或 AOF 文件时全部 miss,触发大量同步磁盘读,反而拖慢 fork 前的准备工作 - Redis 自身数据仍在内存(heap),清缓存不影响它,但会破坏预热状态,让后续客户端请求也出现首次访问延迟
-
drop_caches不清理脏页,只清理干净页;而 fork 和 fsync 的瓶颈恰恰来自脏页
真正该关注的是脏页生命周期管理,而不是“清缓存”这种表面操作。
生产环境最有效的三个干预点
不用改 Redis 源码,靠内核参数和部署约束就能大幅收敛毛刺:
- 把 AOF 文件和 RDB 目录挂载到独立 SSD 分区,并用
mount -o noatime,nodiratime,barrier=0(仅限有掉电保护的 SSD)减少元数据开销 - 调低
vm.dirty_ratio到 10–15,提高脏页回收积极性;同时设vm.dirty_background_ratio为 5,让后台线程更早介入 - 确保
sysctl vm.overcommit_memory=1,避免 fork 因内存检查失败而退化为全量复制(尤其在大内存机器上)
复杂点在于:这些参数的效果高度依赖磁盘类型、负载模式和 Redis 数据写入节奏。同一套参数在 NVMe 上可能压平毛刺,在 SATA SSD 上却引发更多小 I/O。没有银弹,只有持续观测 /proc/vmstat 中的 pgpgin/pgpgout 和 pgmajfault 才能定位真实瓶颈。











