持久性一直是Redis使用中的一个重要话题。很多同学在使用Redis的过程中,对于如何选择持久性策略,如何配置持久性都有疑问。本文试图对Redis的持久性进行系统的分析和比较,以期正确认识Redis的持久性,并结合实际应用选择合理的持久性机制。
1.背景知识
持久性是数据存储领域的一个常见话题。持久性可以描述为将相关数据存储在非易失性存储器中的过程。数据持久化后,即使发生一定程度的故障,只要持久化的设备没有损坏,我们就可以使用持久化的数据进行恢复,在一定程度上减少了故障造成的损失。
我们使用的数据存储,无论是传统的关系型数据库,还是类似Redis的所谓NoSQL,都是在运行后运行在操作系统上的程序,应用程序发送的数据需要经过这些数据存储程序的处理,才能以某种格式持久存储。在这个过程中,数据必须通过应用程序的内存,并由CPU操作,然后才能存储在持久设备上。所以要理解坚持,必须要有几个方面的背景知识。
1.1用户空间、内核空间和内核缓冲区
在Linux中,为了保证操作系统稳定高效的运行,内存空间分为用户空间和内核空间。简单来说,每个应用通常位于用户空间,而操作系统内核运行在内核空间。之所以这样划分,是因为调用操作系统的很多功能都会触发敏感资源的操作,比如清空内存,设置时钟等等。为了防止每个程序操作敏感资源而导致系统崩溃,内存空间按上述方式划分。在用户空间,应用程序没有操作敏感资源的权限(相应的指令受到限制),从而避免了可能出现的系统崩溃等异常现象。
众所周知,大多数应用程序都无法避免处理底层资源,如从磁盘读取数据、从网络读取或接收消息等。但是,在划分用户空间和系统空间后,应用程序无权访问系统资源。为了解决这个问题,操作系统为应用程序公开了一系列的系统调用接口(System Call Interface)来与底层资源进行交互,这样应用程序就可以通过调用这些接口来访问资源。我们常见的系统调用有write(),read()等等。
那么当应用程序触发系统调用接口时会发生什么呢?我们以写文件为例。当应用程序调用写操作时,系统不直接访问硬盘写文件,而是先将文件写入内核缓冲区,然后定期将内核缓冲区中的数据批量写入磁盘(这个过程也可以由应用程序通过触发调用fsync()来完成)。显然,内核缓冲区被划分的原因主要是为了解决底层IO读写速度和内存读写速度不匹配的问题。如果频繁同步写入硬盘,会严重降低程序的运行速度。
一般来说,要理解持久化的过程,需要知道在Linux操作系统中,内存分为用户空间和内核空间。当应用程序中的数据需要写入文件时,会依次通过应用程序内存和内核缓冲区写入硬盘。此过程涉及的系统调用包括写操作write()和强制硬盘同步操作fsync()。
1.2写入时复制
众所周知,在linux系统中,您可以通过调用fork()创建一个与父进程完全相同的子进程副本。但是在fork的过程中,如果父进程占用的内存空间太大,可能需要很长时间来复制内存数据,这会使父进程无法响应其他命令,这对于一个面向客户端的服务器程序来说是无法接受的。
为了解决这个问题,大佬们提出了写时复制技术。简单来说,写时复制技术是指在fork的过程中,应用程序的内存没有被完全复制,而是在子进程中创建一个指向父进程对应内存地址的引用,只有在子进程读取内存,发现数据是复制后新写入的时候,才执行实际的复制动作。这样,由于在fork的过程中不需要完成整个数据的复制,因此大大减少了fork()调用的时间消耗。
但实际上,写时复制技术并没有完全解决问题。比如一些大型应用占用了非常大的内存空间(比如一个Redis实例占用了几十G的内存),只指向内存地址需要很长时间。
最后,对于Redis持久化,我们只需要知道持久化过程中会涉及到fork call(发生在RDB模式和AOF重写的时候)。Linux虽然采用了写时复制技术,但是在实例内存占用量较大的情况下,fork调用仍然可能带来长期的阻塞。
前面我们简单讨论了Linux的两个背景知识点:内存空间分区和写时复制(Linux对fork调用的消耗做的一个优化,即使做了这个优化,高内存占用的实例在fork期间可能还是会长时间阻塞)。接下来,我们分析持久化过程中的数据写入过程。
2.持久数据写入的过程
当我们使用某种数据存储来保存应用数据时,数据由应用程序通过网络发送到数据存储程序,然后由数据存储程序进行处理和计算,然后以一定的格式存储在硬盘等持久性设备中。这个过程涉及很多步骤,比如网络调用、程序处理、操作系统写文件等。为了让下面的讨论更清晰,我们对这一步进行了一定程度的分解。简而言之,应用程序数据从生成到存储到永久设备经历了以下步骤:
客户端向数据库服务器发送写入或更新数据的请求,此时数据位于客户端内存中
当服务器接收到写命令时,数据位于服务器数据库的应用程序内存中(从服务器的角度来看,数据位于应用程序(数据库)内存中,即用户模式内存中)
数据库调用系统函数将数据写入硬盘,此时数据位于内核的缓冲区
操作系统将数据从写缓冲区传输到硬盘控制器,而数据位于硬盘缓存中
硬盘控制器实际上将数据写入物理介质