Redis RDB 的写时复制(Copy-On-Write)
写时复制是 Redis 生成 RDB 快照时的核心机制,理解它需要先了解 fork 系统调用。
核心概念
1. Fork 系统调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 主进程(处理客户端请求): ├─ 内存中有 10GB 数据 ├─ 需要生成 RDB 快照 └─ 调用 fork() 创建子进程
子进程(生成 RDB): ├─ 获得父进程内存的"副本" ├─ 将内存数据写入 RDB 文件 └─ 完成后退出
关键问题: 如果 fork 真的拷贝 10GB 数据,需要: - 耗时很长 - 需要额外 10GB 内存 - 阻塞主进程
解决方案:写时复制(COW)
|
2. 写时复制的工作原理
1 2 3 4 5 6 7
| fork() 调用后:
主进程内存页: 子进程"看到"的内存页: [页1] [页2] [页3] → [页1] [页2] [页3] ↑ ↑ 实际只有一份物理内存! 通过虚拟内存映射共享
|
关键机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 初始状态(fork 后): 物理内存: [页1] [页2] [页3] ↑ 主进程和子进程都指向同一份内存
当主进程要修改页2时: 1. 操作系统拦截写操作 2. 复制页2到新位置:[页2'] 3. 主进程指向页2',子进程仍指向页2 4. 主进程修改页2'
修改后: 物理内存: [页1] [页2] [页3] [页2'] ↑ ↑ 子进程用 主进程用(已修改)
|
在 Redis 中的实际流程
完整过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 1. 客户端发送 BGSAVE 命令
2. 主进程调用 fork() - 耗时很短(只是创建页表,不复制数据) - 子进程"共享"父进程的所有内存
3. 主进程继续工作: - 接收客户端请求 - 执行命令 - 修改数据时触发 COW(复制被修改的页)
4. 子进程开始生成 RDB: - 遍历共享的内存页 - 将数据写入 RDB 文件 - 看到的是 fork 时刻的数据快照
5. 子进程完成,通知主进程 - 子进程退出 - 释放复制的内存页
|
实际例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 场景:Redis 有 3 个 Key
初始状态: Key1 = "A" → 内存页 1 Key2 = "B" → 内存页 2 Key3 = "C" → 内存页 3
执行 BGSAVE: 主进程 fork() → 子进程
时间线: T1: 子进程开始读取内存页 1,2,3 写 RDB T2: 客户端执行 SET Key2 "NEW_B" → 主进程要修改页 2 → COW 触发:复制页 2 → 页 2' → 主进程修改页 2'(Key2 = "NEW_B") → 子进程仍读页 2(Key2 = "B") T3: 子进程继续读页 2,3 写 RDB T4: RDB 生成完成
结果: RDB 文件中:Key1="A", Key2="B", Key3="C"(fork 时刻的快照) 主进程内存:Key1="A", Key2="NEW_B", Key3="C"(最新数据)
|
内存开销分析
COW 的内存消耗
1 2 3 4 5 6 7 8 9 10 11
| 总内存消耗 = 原始数据 + 被修改的数据
示例: - Redis 占用 10GB 内存 - fork 期间有 2GB 数据被修改 - 实际额外内存:2GB(不是 10GB!)
极端情况: - 如果 10GB 数据全部被修改 - 额外内存:10GB - 总共需要 20GB
|
影响因素
1 2 3 4 5 6 7 8 9 10
| 1. 写操作的频率 - 写越多 → COW 复制越多 → 内存开销越大
2. 数据分布 - 修改集中在少数页 → 开销小 - 修改分散在所有页 → 开销大
3. RDB 生成速度 - 生成越快 → COW 时间越短 → 开销越小 - 生成越慢 → COW 累积越多 → 开销越大
|
性能影响
正面影响
1 2 3
| ✓ 主进程不阻塞(除了 fork 瞬间) ✓ 客户端请求正常处理 ✓ 可以并行生成 RDB
|
负面影响
1 2 3
| ✗ 内存开销:需要额外空间存储被修改的页 ✗ CPU 开销:复制页需要 CPU 时间 ✗ 写放大:原本一次写操作,变成"复制+修改"两次操作
|
实际监控
查看 COW 信息
1 2 3 4 5 6
| 127.0.0.1:6379> INFO stats
cow_memory: 4523456 rdb_last_cow_size: 4523456
|
优化建议
1 2 3 4 5 6 7 8 9 10 11 12
| # 1. 控制 RDB 频率 save 900 1 # 900秒内至少1个key变化 save 300 10 # 300秒内至少10个key变化 save 60 10000 # 60秒内至少10000个key变化
# 2. 使用大页内存(减少页数量) echo always > /sys/kernel/mm/transparent_hugepage/enabled
# 3. 监控 COW 内存 INFO stats 中的 cow_memory
# 4. 避免在写入高峰期 BGSAVE
|
总结对比
| 维度 |
没有 COW |
有 COW |
| Fork 耗时 |
长(拷贝所有数据) |
短(只创建页表) |
| 内存开销 |
2倍(完整副本) |
取决于写入量 |
| 阻塞时间 |
长 |
极短(毫秒级) |
| 实现复杂度 |
简单 |
依赖操作系统 |
核心理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 写时复制 = 延迟复制 + 按需复制
不用时: - 父子进程共享同一份内存 - 零拷贝,零开销
要用时: - 写操作触发复制 - 只复制被修改的部分 - 保证子进程看到的是 fork 时刻的快照
类比: - 共享文档的"快照"功能 - 初始不复制内容,只记录差异 - 修改时才真正复制被改的部分
|
简单记忆:COW 让 fork 变得轻量,只在真正需要修改时才复制,是 Redis 能高效生成 RDB 的关键技术。