ShenQQQ的网络日志

为什么Redis持久化是多进程的

我们都知道使用子进程的形式会避免主进程被阻塞情况的出现,那么有两个问题是我们必须要考虑的。

问题一:FORK 之后的子进程必须要获取父进程内存中的数据,这是否能实现?

问题二:FORK 函数会带来额外的性能开销,这些开销我们怎么样才可以避免?

对于问题一:
​通过 fork 生成的父子进程会共享包括内存空间在内的资源;

​fork操作往往都是被操作系统内核实现的系统调用。fork后的父子进程虽然会运行在不同的内存空间中,但是当fork执行时,两者的内存空间拥有完全相同的内容。两者对于内存的写入,修改文件的映射都是独立的。两个进程不会相互影响。

​最关键的点在于父子进程的内存在 fork 时是完全相同的,在 fork 之后进行写入和修改也不会相互影响,这其实就完美的解决了快照这个场景的问题 —— 只需要某个时间点下内存中的数据,而父进程可以继续对自己的内存进行修改,这既不会被阻塞,也不会影响生成的快照。

​那么就出现了一个新的问题:

问题三:当执行FORK操作时,是否意味着子进程需要对父进程的内存进行全量的拷贝?

对于问题二&问题三:
就算脱离了Redis的场景,fork是全量拷贝内存也是难以接受的,假设我们需要在命令行中执行一个命令,我们需要先通过fork创建一个新的进程再通过exec来执行程序,fork拷贝的大量内存空间对于子进程来说可能完全没有任何作用。但是却引入了巨大的额外开销。

为了解决这一个问题,今天的操作系统通常来使用写时拷贝(Copy-on-Write),来解决这个问题。

问题四:啥是写时拷贝(COPY-ON-WRITE)?

对于问题四:
写时拷贝的主要作用就是将拷贝推迟到写操作真正发生时,这也就避免了大量无意义的拷贝操作。

在 fork 函数调用时,父进程和子进程会被 Kernel 分配到不同的虚拟内存空间中,所以在两个进程看来它们访问的是不同的内存,但在真正访问虚拟内存空间时,Kernel 会将虚拟内存映射到物理内存上,所以父子进程实质上共享了物理上的内存空间;

​而由于执行拷贝的子进程不会执行写操作。所以只有当父进程对共享的内存进行修改时,父进程才会以页为单位(4k一页)拷贝共享的内存,父进程会保留原有的物理空间,而子进程会使用拷贝后的新物理空间;

通过上述的机制,Redis将对应的内存拷贝的压力,分散到了各个写操作的执行时刻,大大缓解了主进程阻塞的情况,同时带来了非常好的性能,所以Redis会将RDB设计成使用子进程的形式。

参考资料: https://draveness.me/whys-the-design-redis-bgsave-fork/