mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
853 字
2 分钟
为什么 Redis 快照使用子进程
2023-03-25

Redis 的 RDB 持久化机制会在后台通过 fork() 创建一个子进程来生成数据库快照。为什么要用子进程?这个设计背后有什么考量?

一、Redis 持久化概述#

1.1 Redis 的两种持久化方式#

flowchart LR subgraph RDB(快照) S[定时快照] --> F[生成 dump.rdb] end subgraph AOF(日志) W[写命令追加] --> A[appendonly.aof] end
持久化方式原理优点缺点
RDB定时快照恢复快、文件紧凑可能丢失数据
AOF命令日志数据安全性高文件较大、恢复慢

1.2 RDB 快照的工作方式#

# 触发 RDB 快照的方式
BGSAVE # 后台异步执行(使用 fork)
SAVE # 同步执行(阻塞)
SAVEOR AUTO # 配置触发

二、为什么使用 fork()?#

2.1 fork() 的基本原理#

// fork() 创建一个子进程
pid_t pid = fork();
if (pid == 0) {
// 子进程:执行快照逻辑
create RDB snapshot
exit(0);
} else {
// 父进程:继续处理请求
process client requests
}
flowchart LR P[父进程] -->|fork| C[子进程] subgraph 父进程 PA[(内存数据)] end subgraph 子进程 CA[(内存副本)] end CA -->|写入| RDB[dump.rdb]

2.2 关键优势:Copy-On-Write#

Copy-On-Write(COW,写时复制) 是 Linux 内核的机制:

flowchart LR subgraph fork 之后 P[父进程] <-->|共享物理内存| C[子进程] Note over P,C: 虚拟地址不同,物理内存共享 end subgraph 写入时 P <-->|各自独立| C P --> PP[物理页副本] C --> CP[物理页副本] end

工作原理

  1. fork() 后,父子进程共享同一块物理内存
  2. 只有当某个进程尝试写入时,才复制物理页
  3. 读取操作不需要复制

2.3 Redis 如何利用 COW#

sequenceDiagram participant P as Redis 父进程 participant C as fork 出的子进程 participant M as 操作系统 Note over P,C: fork() 完成,共享内存 P->>M: 写入 key_A M->>M: 复制 key_A 所在页 M->>P: 返回修改后的页 Note over C: 子进程看到的仍是旧数据 C->>M: 读取 key_A M-->>C: 原始数据(未修改)

关键洞察:子进程在 fork 瞬间看到了数据库的完整快照,这个快照是一致的,之后父进程的修改不会影响子进程。

三、使用子进程的好处#

3.1 非阻塞快照生成#

flowchart LR subgraph 方式 1:单线程阻塞 S[SAVE] --> B[阻塞写 dump.rdb] B --> W[等待完成] end subgraph 方式 2:fork 子进程 BG[BGSAVE] --> F[fork] F --> P[父进程继续服务] F --> C[子进程写 dump.rdb] end

BGSAVE 的优势

  • 父进程不阻塞,继续处理客户端请求
  • 子进程专门负责 IO 操作
  • 对客户端请求零影响

3.2 内存效率#

如果使用线程

  • 线程共享进程地址空间
  • 需要复杂的锁来保护数据结构
  • 任何修改都需要考虑线程安全

使用子进程 + COW

  • 子进程有独立的地址空间
  • 读取时共享内存,无额外开销
  • 只有写入时才复制页

3.3 一致性保证#

sequenceDiagram participant C as 客户端 participant P as Redis 父进程 participant S as 子进程 participant D as dump.rdb Note over P,S: t=0: fork 完成,快照起点 C->>P: SET key "value1" P->>P: 修改内存 Note over S: 子进程仍看到旧数据 C->>P: SET key "value2" P->>P: 修改内存 S->>D: 写入快照(基于 fork 时的状态) D-->>S: 完成 S-->>P: exit(0) Note over P: 快照包含 "value1",不包含 "value2"

COW 保证了快照的一致性:子进程看到的是 fork 时刻的内存状态。

四、性能考量#

4.1 fork() 的开销#

# fork() 的成本
time redis-cli BGSAVE
# Background saving started by pid 12345
# fork() 的开销:
# - 复制进程描述符(文件描述符、信号等)
# - 复制进程调度信息
# - 复制内存映射
# - 不复制实际物理内存(COW)
操作耗时
fork() 创建进程10-50 ms(取决于进程大小)
复制页表几 ms
复制实际内存0(COW)

4.2 COW 的开销#

# 如果父进程在 fork 后大量修改
# 会触发大量页复制
# 监控 COW 页面
cat /proc/[redis-pid]/status | grep -i rss
# RSS: 内存使用量(包含共享页)

COW 最坏情况

  • 如果 fork 后父进程大量修改内存
  • 会复制大量物理页
  • 可能导致内存不足

4.3 优化建议#

# 1. 在低峰期执行 BGSAVE
redis-cli BGSAVE # 凌晨 3 点执行
# 2. 监控 COW 情况
redis-cli info stats | grep -i fork
# 3. 限制 AOF 重写时的 COW
# redis.conf
aof-rewrite-incremental-fsync yes # 每写入一定量就 fsync

五、与其他持久化方案对比#

5.1 vs 线程持久化#

方案优点缺点
子进程 fork独立地址空间,无锁,COW 节省内存fork 开销,进程创建慢
线程持久化共享地址空间,创建快需要锁,可能阻塞

5.2 vs AOF#

flowchart LR subgraph RDB S[定时快照] --> F[完整数据文件] F --> R[恢复快] end subgraph AOF L[命令日志] --> J[逐步追加] J --> R2[恢复慢] end

RDB 适合

  • 定时备份
  • 灾难恢复
  • 主从同步

AOF 适合

  • 需要更高的数据安全性
  • 写密集型负载

六、实际案例#

6.1 主从同步中的 RDB#

flowchart LR M[Master] -->|BGSAVE| D[dump.rdb] D -->|传输| S[Slave] S -->|加载| R[(内存)]

流程

  1. Master 执行 BGSAVE,fork 子进程生成 RDB
  2. 子进程将快照写入磁盘
  3. Master 将 RDB 文件传输给 Slave
  4. Slave 加载 RDB 到内存

6.2 容器环境中的 fork#

# 容器环境中 fork() 可能有问题
# 因为 COW 会复制内存页到物理内存
# 而容器可能有内存限制
# 推荐配置
docker run --memory=4g redis redis-server --maxmemory=3g

七、总结#

Redis 使用子进程生成快照的原因:

原因说明
非阻塞父进程继续服务客户端
COW 效率共享内存,只在写入时复制
一致性fork 瞬间的内存快照
简单性无需复杂的线程同步

设计哲学:利用操作系统提供的 COW 机制,用最简单的方式实现高效、一致的快照生成。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么 Redis 快照使用子进程
https://blog.souloss.com/posts/why-the-design/why-redis-snapshot-uses-child-process/
作者
Souloss
发布于
2023-03-25
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时