Redis为什么在RDB快照时会通过子进程的方式进行实现?

rdbSaveBackGround函数:将内存中的数据以 RDB 格式保存到磁盘中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;

if (hasActiveChildProcess()) return C_ERR;
...

if ((childpid = redisFork()) == 0) {
int retval;

/* Child */
redisSetProcTitle("redis-rdb-bgsave");
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
sendChildCOWInfo(CHILD_INFO_TYPE_RDB, "RDB");
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* Parent */
...
}
...
}

函数中在快照前会先通过redisFork()创建子进程

  • 为什么 fork 之后的子进程能够获取父进程内存中的数据?

  • fork 函数是否会带来额外的性能开销,这些开销我们怎么样才可以避免?

设计
  • 通过 fork 生成的父子进程会共享包括内存空间在内的资源;

  • fork 函数并不会带来明显的性能开销,尤其是对内存进行大量的拷贝,它能通过写时拷贝将拷贝内存这一工作推迟到真正需要的时候。

Redis 实现后台快照的方式非常巧妙,通过操作系统提供的 fork写时拷贝的特性轻而易举的就实现了这个功能,从这里我们就能看出作者对于操作系统知识的掌握还是非常扎实的,大多人在面对类似的场景时,想到的方法可能就是手动实现类似写时拷贝的特性,然而这不仅增加了工作量,还增加了程序出现问题的可能性。

到这里,我们简单总结一下 Redis 为什么在使用 RDB快照时会通过子进程的方式进行实现:

  • 通过 fork 创建的子进程能够获得和父进程完全相同的内存空间,父进程对内存的修改对于子进程是不可见的,两者不会相互影响;

  • 通过 fork 创建子进程时不会立刻触发大量内存的拷贝,内存在被修改时会以页为单位进行拷贝,这也就避免了大量拷贝内存而带来的性能问题;

上述两个原因中,一个为子进程访问父进程提供了支撑,另一个为减少额外开销做了支持,这两者缺一不可,共同成为了 Redis 使用子进程实现快照持久化的原因。到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:

  • Nginx 的主进程会在运行时 fork 一组子进程,这些子进程可以分别处理请求,还有哪些服务会使用这一特性?

  • 写时拷贝其实是一个比较常见的机制,在 Redis 之外还有哪里会用到它?