虽然我们经常将 Redis 看做一个纯内存的键值存储系统,但是我们也会用到它的持久化功能,RDB 和 AOF 就是 Redis 为我们提供的两种持久化工具,其中 RDB 就是 Redis 的数据快照。

我们在这篇文章想要分析 Redis 为什么在对数据进行快照持久化时会需要使用子进程,而不是将内存中的数据结构直接导出到磁盘上进行存储。

概述

在具体分析今天的问题之前,我们首先需要了解 Redis 的持久化存储机制 RDB 究竟是什么,RDB 会每隔一段时间中对 Redis 服务中当下的数据集进行快照。

除了 Redis 的配置文件可以对快照的间隔进行设置之外,Redis 客户端还同时提供两个命令来生成 RDB 存储文件,也就是 SAVEBGSAVE,通过命令的名字我们就能猜出这两个命令的区别。

其中 SAVE 命令在执行时会直接阻塞当前的线程,由于 Redis 是 单线程 的,所以 SAVE 命令会直接阻塞来自客户端的所有其他请求,这在很多时候对于需要提供较强可用性保证的 Redis 服务都是无法接受的。

我们往往需要 BGSAVE 命令在后台生成 Redis 全部数据对应的 RDB 文件,当我们使用 BGSAVE 命令时,Redis 会立刻 fork 出一个子进程,子进程会执行『将内存中的数据以 RDB 格式保存到磁盘中』这一过程,而 Redis 服务在 BGSAVE 工作期间仍然可以处理来自客户端的请求。

rdbSaveBackground 就是用来处理在后台将数据保存到磁盘上的函数:

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 */...}...
}

Redis 服务器会在触发 BGSAVE 时调用 redisFork 函数来创建子进程并调用 rdbSave 在子进程中对数据进行持久化,我们在这里虽然省略了函数中的一些内容,但是整体的结构还是非常清晰的,感兴趣的读者可以在点击上面的链接了解整个函数的实现。

使用 fork 的目的最终一定是为了不阻塞主进程来提升 Redis 服务的可用性,但是到了这里我们其实能够发现两个问题:

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

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

既然 Redis 选择使用了 fork 的方式来解决快照持久化的问题,那就说明这两个问题已经有了答案,首先 fork 之后的子进程是可以获取父进程内存中的数据的,而 fork 带来的额外性能开销相比阻塞主线程也一定是可以接受的,只有同时具备这两点,Redis 最终才会选择这样的方案。

设计

为了分析上一节提出的两个问题,我们在这里需要了解以下的这些内容,这些内容是 Redis 服务器使用 fork 函数的前提条件,也是最终促使它选择这种实现方式的关键:

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

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

子进程

在计算机编程领域,尤其是 Unix 和类 Unix 系统中,fork 都是一个进程用于创建自己拷贝的操作,它往往都是被操作系统内核实现的系统调用,也是操作系统在 *nix 系统中创建新进程的主要方法。

当程序调用了 fork 方法之后,我们就可以通过 fork 的返回值确定父子进程,以此来执行不同的操作:

  • fork 函数返回 0 时,意味着当前进程是子进程;

  • fork 函数返回非 0 时,意味着当前进程是父进程,返回值是子进程的 pid

int main() {if (fork() == 0) {// child process} else {// parent process}
}

fork 的 手册 中,我们会发现调用 fork 后的父子进程会运行在不同的内存空间中,当 fork 发生时两者的内存空间有着完全相同的内容,对内存的写入和修改、文件的映射都是独立的,两个进程不会相互影响。

The child process and the parent process run in separate memory spaces.  At the time of fork() both memory spaces have the same content.  Memory writes, file mappings (mmap(2)), and unmappings (munmap(2)) performed by one of the processes do not affect other.

除此之外,子进程几乎是父进程的完整副本(Exact duplicate),然而这两个进程在以下的一些方面会有较小的区别:

  • 子进程用于独立且唯一的进程 ID;

  • 子进程的父进程 ID 与父进程 ID 完全相同;

  • 子进程不会继承父进程的内存锁;

  • 子进程会重新设置进程资源利用率和 CPU 计时器;

  • ...

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

写时拷贝

既然父进程和子进程拥有完全相同的内存空间并且两者对内存的写入都不会相互影响,那么是否意味着子进程在 fork 时需要对父进程的内存进行全量的拷贝呢?假设子进程需要对父进程的内存进行拷贝,这对于 Redis 服务来说基本都是灾难性的,尤其是在以下的两个场景中:

  1. 内存中存储大量的数据,fork 时拷贝内存空间会消耗大量的时间和资源,会导致程序一段时间的不可用;

  2. Redis 占用了 10G 的内存,而物理机或者虚拟机的资源上限只有 16G,在这时我们就无法对 Redis 中的数据进行持久化,也就是说 Redis 对机器上内存资源的最大利用率不能超过 50%;

如果无法解决上面的两个问题,使用 fork 来生成内存镜像的方式也无法真正落地,不是一个工程中真正可以使用的方法。

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

写时拷贝(Copy-on-Write)的出现就是为了解决这一问题,就像我们在这一节开头介绍的,写时拷贝的主要作用就是将拷贝推迟到写操作真正发生时,这也就避免了大量无意义的拷贝操作。在一些早期的 *nix 系统上,系统调用 fork 确实会立刻对父进程的内存空间进行复制,但是在今天的多数系统中,fork 并不会立刻触发这一过程:

fork 函数调用时,父进程和子进程会被 Kernel 分配到不同的虚拟内存空间中,所以在两个进程看来它们访问的是不同的内存:

  • 在真正访问虚拟内存空间时,Kernel 会将虚拟内存映射到物理内存上,所以父子进程共享了物理上的内存空间;

  • 当父进程或者子进程对共享的内存进行修改时,共享的内存才会以页为单位进行拷贝,父进程会保留原有的物理空间,而子进程会使用拷贝后的新物理空间;

在 Redis 服务中,子进程只会读取共享内存中的数据,它并不会执行任何写操作,只有父进程会在写入时才会触发这一机制,而对于大多数的 Redis 服务或者数据库,写请求往往都是远小于读请求的,所以使用 fork 加上写时拷贝这一机制能够带来非常好的性能,也让 BGSAVE 这一操作的实现变得非常简单。

总结

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

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

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

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

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

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

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

如果对文章中的内容有疑问或者想要了解更多软件工程上一些设计决策背后的原因,可以在博客下面留言,作者会及时回复本文相关的疑问并选择其中合适的主题作为后续的内容。

Reference

  • Redis Persistence

  • Understanding Redis Background Memory Usage

  • FAQ · Redis

  • Copy-on-write

  • rdbSaveBackground · Redis

  • Fork (system call)

  • Which file in kernel specifies fork(), vfork()… to use sys_clone() system call

  • Trying to understand fork() and Copy-on-Write (COW)

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

Redis 的快照为什么不会阻塞其他请求?相关推荐

  1. Redis的快照与AOF

    为什么80%的码农都做不了架构师?>>>    我们知道,redis的数据是保存在内存里,而内存一断电就没了,所以为了数据持久化,我们得想办法把内存中的数据持久化到硬盘或者另一台机子 ...

  2. Redis 删除Key命令会导致阻塞么?

    Redis 删除Key命令会导致阻塞么? 会的 如果删除大字符串类型,比如几个G,几百M. 删除单个元素多的列表.集合.有序列表或者哈希表类型的Key 都会导致Redis阻塞发生 如有错误欢迎指正

  3. Redis 内存快照:宕机后,Redis如何实现快速恢复?

    上节课,我们学习了 Redis 避免数据丢失的 AOF 方法.这个方法的好处,是每次执行只需要记录操作命令,需要持久化的数据量不大.一般而言,只要你采用的不是 always 的持久化策略,就不会对性能 ...

  4. 工作问题之:redis 保存快照问题

    今天开发突然和我说redis 不能写了.我进入redis后发现确实是这样,不可以执行set指令了.报错如下: 172.31.18.90:6379> set  test test1 (error) ...

  5. 服务器高并发时请求报错_基于redis的分布式锁防止高并发重复请求

    需求: 我们先举个某系统验证的列子:(A渠道系统,业务B系统,外部厂商C系统) (1)B业务系统调用A渠道系统,验证传入的手机.身份证.姓名三要素是否一致. (2)A渠道系统再调用外部厂商C系统. ( ...

  6. yii2 请求外部api_[PHP] 基于redis的分布式锁防止高并发重复请求

    需求: 我们先举个某系统验证的列子:(A渠道系统,业务B系统,外部厂商C系统) (1)B业务系统调用A渠道系统,验证传入的手机.身份证.姓名三要素是否一致. (2)A渠道系统再调用外部厂商C系统. ( ...

  7. PHP session锁:如何避免session阻塞PHP请求

    来源:https://log.zvz.im/2016/02/27/PHP-session/ https://ma.ttias.be/php-session-locking-prevent-sessio ...

  8. redis如何通过读写分离来承载读请求QPS超过10万多

    单机redis,能够承载的QPS大概就在上万,到几万不等 方案 读写分离,一般都是用来支撑读高并发,写请求比较少,可能请求也就一秒几千 大量的请求都是读,一秒钟二十万次 master 同步数据 sla ...

  9. [tomcat]源码简析 异步/非阻塞和请求构成

    提出疑惑 SpringFramework5.0又新增加了一个功能Webflux(响应式编程),是一个典型非阻塞异步的框架. 我们知道servlet3.0实现异步(AsyncContext),servl ...

最新文章

  1. 通知 | 首届中国心电智能大赛复赛开启
  2. 留的住叫做幸福. 流逝的叫做遗憾
  3. CF986B Petr and Permutations 思维
  4. 3行Python代码完成人脸识别
  5. 方法总结及易错点总结
  6. Tomcat配置解析
  7. 【Java每日一题】20161228
  8. uniapp中使用colorUI说明文档
  9. UINO优锘:数字孪生助力运维工程场景化可视化管理
  10. 处理器后面的字母含义_Intel处理器背盖上的字母含义
  11. 【重识云原生】第四章云网络4.3.9节——Graceful Restart(平滑重启)技术
  12. 色彩系列教程(3):实际运用
  13. 一文带你了解APS生产计划排程系统
  14. 如何在Mac上获取App Store的ipa包(非越狱手机也可以)
  15. [纵横网络靶场社区]MMS协议分析
  16. 120年奥运史:运动员和成绩(相关数据集)
  17. java大麦_大麦大 - SegmentFault 思否
  18. Dynamics CRM 知识库设置
  19. pt04-object-oriented
  20. 【240期】面试官问:说说基于 Redis 实现延时队列服务?

热门文章

  1. 莫比乌斯带catia建模_独家教程 | 循环曲面“莫比乌斯”,康石石教你Rhino“3步”快速打造...
  2. mmd python error_python_mmdt:一种基于敏感哈希生成特征向量的python库(一)
  3. 讯飞智能录音笔SR101:考研的温暖陪伴
  4. queuedeclare参数说明_MQ 学习笔记之RabbitMQ
  5. 模型描述的关系模式_你的项目该用哪种编程模式?
  6. Shodan新手入坑指南
  7. PCI总线的含义是什么?PCI总线的主要特点是什么?
  8. LeetCode 51 N 皇后
  9. arp 命令详解(安装、arp欺骗防御)
  10. Windows编程—控制面板程序显示信息修改(程序图标、名称、链接等)