Memory pressure lab / 14
fork 没有复制 16 GB,内存为什么还是爆了?
Redis 创建持久化子进程时,父子进程先共享同一批物理页。真正把内存顶上去的,是后台任务尚未结束时,父进程持续改写数据,内核不得不为这些页保存两份版本。
先记住结论:fork 瞬间不会完整复制数据集;只有 fork 之后被父进程修改的页,才会触发写时复制(COW)。
写时复制压力模拟器
模型用于容量预判,不替代真实机器上的 INFO、LATENCY 与压测数据。
fork 前只有父进程持有数据页。
01 / 误解发生在哪
“父子各有一份数据”不等于“fork 当场复制一份数据”
逻辑地址空间看起来各自独立,物理页却可以暂时共享。COW 正是用“先共享、写时再分开”换取快速创建子进程。
执行 BGSAVE 或 BGREWRITEAOF 时,Redis 主进程调用 fork()。内核会为子进程建立地址空间和页表,让父子进程的虚拟地址都指向原来的物理页,并把相关页标记为写时复制。子进程看到的是 fork 那一刻的数据视图,可以一边读取并生成 RDB 或新 AOF,父进程则继续响应客户端。
当父进程随后修改某个共享页,内核不能直接覆盖它,因为子进程还要读取旧版本。于是内核分配新物理页、复制原页内容,再让父进程写入新页。这个动作才是 COW 的真正成本。修改越分散,触发复制的页越多;哪怕只改一个很小的对象,也可能复制它所在的整个 4 KiB 页面。
写入可触达数据量 ≈ 写入强度 × 后台任务时长
COW 额外内存 ≈ min(数据集 × 脏页比例,写入可触达数据量)
峰值 RSS ≈ 数据集 × 1.10 + COW 额外内存
上面的模型故意保持保守和透明。真实 COW 还会受对象分布、jemalloc 内存布局、Transparent Huge Pages、子进程完成速度、内核回收以及写放大影响。它更适合回答“这个量级是否危险”,不适合假装给出精确到 MB 的生产结论。
02 / 事故是怎样形成的
真正危险的是:慢子进程撞上高写入
持久化时间越长,父进程制造脏页的窗口越长。内存余量原本够用,也可能在几分钟的重写过程中被一点点吃完。
- fork 发生,主线程短暂停顿
不会复制整个数据集,但页表建立成本会随数据集和页表规模增长。机器繁忙或数据集很大时,客户端会感到尾延迟抖动。
- 子进程开始顺序读取旧视图
它把 RDB 写到临时文件,或生成新的 AOF。磁盘吞吐不足会延长整个后台窗口。
- 父进程继续处理写请求
每次首次改写共享页,都可能让该页出现父、子两个物理版本。写请求越密集、修改越分散,COW 增长越快。
- 可用内存被 COW 吃尽
系统开始回收、交换甚至触发 OOM Killer。此时问题看起来像“持久化导致 Redis 被杀”,根因却是没有为写时复制留容量。
03 / 上线前的边界
把“能不能 fork”变成可观测、可预算的问题
不要只看日常 used_memory。持久化发生时的写入速率、子进程耗时与 COW 峰值才决定是否安全。
容量规划时,至少把峰值写入窗口代入一次。生产环境还要观察 latest_fork_usec、rdb_last_cow_size、aof_last_cow_size、current_cow_size,并把它们与写入 QPS、磁盘时延和可用内存放到同一条时间线上。Linux 的 vm.overcommit_memory 会影响 fork 能否被允许,但把它改成 1 并不会创造真实物理内存,更不会消除 COW。