Redis存储揭秘(翻译)

Redis存储揭秘(翻译)

原文地址: http://oldblog.antirez.com/post/redis-persistence-demystified.htmlhtml

我在Redis的部分工做是阅读博客,论坛消息以及推特上关于Redis的搜索。对于一个开发者来讲,社区用户以及非用户的对他开发的产品的见解很是重要。个人感触是Redis的持久化被人误解很是多。mysql

在这篇博客中,我会努力的作到公正:不安利Redis,不跳过可能让Redis有负面影响的细节。我想要提供一个清晰的,好理解的Redis存储的流程图,它有多可靠,以及和其余数据库的对比。linux

##操做系统和磁盘redis

首先咱们能够探讨一下数据库如何作到耐久性。为此,咱们能够模拟一下一个简单的写入操做。sql

  • 1: 客户端发送一个写命令到数据库(数据存储客户端的内存中)。
  • 2:数据库接收了这条命令(存储到了数据库的内存)。
  • 3;数据库调用system call(系统级调用),尝试写入到硬盘(这一步实际上写入到了操做系统内核的缓冲区)。
  • 4:操做系统将缓冲区的数据写入到磁盘控制器(数据转移到磁盘缓冲区)。
  • 5:磁盘控制器将数据真实写入到物理介质(硬盘,闪存……)

注意:上述步骤是很是简化的,真实环境的缓存更为复杂。数据库

第二步在数据库中,每每被实现为一个复杂的缓存系统,有时候写入会在不一样的线程或是进程执行(接收线程和IO线程分离)。以咱们的视角,数据库早晚会将数据写入到磁盘。换言之,在内存中的数据会在某一时刻传输到操做系统内核(第三步)。缓存

第三步中有一个较大的遗漏。由于真实的操做系统更为复杂,它实现了多个不一样的缓存层。一般的,有文件系统缓存(在linux中被称为page chche,页面缓冲),以及一个小一些的写缓冲会缓存将要写入到硬盘的数据。使用特定的API能够绕开这两层(例如linux中的O_DIRECT和O_SYNC参数)。以咱们的视角,咱们能够认为有一层不透明的缓存层(咱们不明确实现细节)。这已经足够说明,若是数据库实现了本身的缓存系统,页面缓冲通常会被禁用。由于数据库和内核会在同时作一样的事情(引发不良后果)。写缓冲通常会开启,由于频繁的提交到磁盘对于大部分软件来讲太慢了。安全

在真正的实现中,数据库不会老是调用系统调用来同步写缓冲到磁盘,仅在必须的时候调用。服务器

在何时,咱们的写是安全的

若是错误发生在数据库系统(管理员杀掉进程或者崩溃),若是第三步调用成功了(已经进入到页面缓冲),就能够认为数据已经安全写入了。这一步以后即便数据库崩溃,操做系统内核仍会将数据安全的转移到磁盘控制器。数据结构

若是咱们考虑更严峻的事件,例如断电,那么只有第五步写入磁盘成功后,数据才是安全的。

步骤三、四、5中针对数据安全性的的重要阶段,咱们能够总结一下。

  • 数据库间隔多久调用一次系统调用,将数据从用户空间写入到内核(即从数据库的内存到操做系统缓冲)。
  • 系统内核间隔多久将数据刷入到磁盘控制器。
  • 磁盘控制器多久将数据写入到物理介质。

注意 上文提到的磁盘控制器,主要是表达缓存的行为,不管是控制器仍是磁盘自己。在耐久性要求很高的环境中,系统管理员通常会禁用这一层缓存。

默认的,磁盘控制器会穿透缓存直接写(即,仅有读缓存,没有写缓存)。若是有断电保护设备,那么启用写缓冲是安全的。

操做系统API

以数据库开发人员的角度看,数据真正到达物理介质前的数据流转是颇有意思的,但更有意思的是,API提供的各类控制。

从第三步开始说明。咱们可使用write系统调用将数据传入系统内核缓存,从这点上看,咱们借助系统API能够良好的控制行为。然而咱们并不清楚这个系统调用须要执行多久才会返回成功。内核的写入缓冲有大小限制,若是磁盘不能应对写入带宽,内核写入缓冲会达到极限值,这时系统会阻塞咱们的写入。当磁盘能够接收新的数据,write系统调用才会返回。全部的操做执行完,数据写入到物理介质。

第四步,这一步,系统内核将数据传入磁盘控制器。默认的,系统会限制写入频率,由于传输更大的块写入更快。例如,在Linux中,30秒以后写入才会被真正提交。这意味着,若是有故障,最近30秒的数据有可能丢失。

系统API提供了一系列系统调用来强制将内核缓冲写入到硬盘,最有名的是fsync系统调用(更多信息能够搜索msync,fdatasync)。数据库使用fysnc将内核中的数据提交到硬盘,可是能够猜想到,这是一个很是昂贵的操做:若是fsync被调用而且内核缓冲有数据的时候,会当即执行执行一个写操做。fsync会一直阻塞写入的进程,在Linux上,fsync还会阻塞写入同一个文件的其余线程。

哪些是咱们没法控制的

目前为止,咱们学习到能够控制第三、4步,第5步能够么?正式的说,咱们经过系统API没法控制。也许一些内核的实现能够通知驱动提交数据到物理介质,或者为了速度考虑,磁盘控制器本身记录数据但不当即提交,而是过几个毫秒或更久再提交。这已经脱离了咱们的控制。

在接下来的文章中,咱们会简化到两种数据安全级别:

  • 经过write系统调用,将数据写入到内核缓冲区。即便用户系统(例如数据库)故障,仍保证数据安全性。
  • 经过fsync系统调用,将数据提交到磁盘。提供了完整的数据安全性,操做系统故障,断电,仍保证数据安全性。咱们明确的知道这个没有保障性,由于磁盘控制器还有缓存。咱们不对此作考虑,是由于对于大部分数据库系统这是不变的。另外,系统管理员可使用一些特殊的工具来控制磁盘设备的写入行为。

注意 不是全部的数据库都使用系统API。一些专有的数据库使用一些内核模块能够直接和硬件设备交互。然而,面临的问题是相似的。你可使用用户空间的内存,内核缓存,或早或晚将数据写入到磁盘来保障安全性。Oracle就是使用内核模块的一个例子。

数据损坏

前面的文章中,咱们从几个方面分析了确保数据安全写入到磁盘的的一些问题:应用程序(用户空间),系统内核。然而这并非困扰耐久性的惟一问题。另外一个问题以下:数据库在灾难后是否能够恢复?或者内部的结构由于某种行为被破坏,致使它不能被正确读取,须要恢复步骤来重建结构。

不少SQL和NoSQL数据库实现了基于磁盘的树形结构来保存数据和索引。这种数据结构在写入的时候会被调整。若是在写入过程当中,系统中止工做了,那么树形结构还能保持正确么?

通常的,这里有3种数据故障安全级别:

  • 数据库不关心写入磁盘故障,要求用户使用一个副本作数据恢复。或者提供工具来尝试重建内部结构。
  • 数据库记录操做日志,在故障后经过日志重放保证一致性。
  • 数据库从不修改已经写入的数据,仅在后面调价,因此没有数据损坏。

如今咱们拥有了全部的知识,用来评估数据库系统持久层的可靠性。如今去检验Redis在这一点上的表现。Redis提供了两种不一样的持久化模式,咱们会一一的检验。

Snapshotting 快照

Redis快照是最简单的Redis持久化模式。它将再如下场景产生某一时刻的快照,前一个快照时间点已经超过2分钟,同时有100个写入。这个触发条件能够用户自定义,能够修改配置文件重启,也能够在运行期配置。快照会生成一个.rdb文件,文件中包含了整个数据集。

快照的耐久性被用户定义的触发条件所限制。若是数据每隔15分钟存储一次,若是Redis进程崩溃或者更严重的时间,那么至多有15分钟的数据会丢失。从这点来看Redis的MULTI/EXEC事务或许完整存储到快照,或许彻底没有存储。

RDB文件不会发生数据损坏。首先它启动子进程将Redis内存中的数据写入到镜像,而后在文件后面追加。一个新的rdb快照被建立成临时文件,子进程写入成功后,经过原子性的重命名到目标文件(仅在调用fsync系统调用持久化到磁盘后发生)。

Redis快照提供不了足够的耐久性,若是没法忍受几分钟的数据丢失。这种存储模式仅适合数据丢失影响不大的场景。

然而,即便使用更加先进的“AOF”模式,咱们仍建议开启快照模式。由于快照很是适合用来作备份,发送数据到远程数据中心作灾难恢复,或者将数据回滚到一个旧版本。

特别要提一点,RDB快照被用来作主从同步。

  • RDB有一个附加的好处,对于给定的数据库规模,无论在数据库上执行什么动做,系统上的IO数据是固定的。这个特性是大部分传统的数据库没有的(包括Redis其余的持久化,AOF)。

##追加文件

AOF是Redis主要的持久化选项。它的执行很是简单:每一次对内存数据形成修改的写入操做被执行,记录该操做。日志格式和客户端提交给Redis的格式是一致的,因此AOF能够经过netcat传输到另外一个Redis实例。或者若是须要,能够简单的将内容解析出来。Redis重启的时候,会重放aof文件的内容来重建数据。

为了描述AOF是如何工做的,我作了一个简单的实验。安装Redis实例,经过如下方式启动:

./redis-server --appendonly yes

执行一些写入操做:


redis 127.0.0.1:6379> set key1 Hello

OK

redis 127.0.0.1:6379> append key1 " World!"

(integer) 12

redis 127.0.0.1:6379> del key1

(integer) 1

redis 127.0.0.1:6379> del non_existing_key

(integer) 0


开始的3个操做会修改数据,第四个不会,由于没有对应的key。下面是aof文件的格式范例:


$ cat appendonly.aof

*2

$6

SELECT

$1

0

*3

$3

set

$4

key1

$5

Hello

*3

$6

append

$4

key1

$7

World!

*2

$3

del

$4

key1


如你所见,最后的删除没有体现,由于它没有形成数据的修改。

就这么简单,被接收到的命令会被写入到AOF,只要它修改了数据。然而,不是全部的命令会被记录。举个例子,在lists上面的阻塞操做会被记录成非阻塞的命令,由于这个对于数据的影响是一致的。一样的,INCRBYFLOAT被记录成SET,使用计算后的最终结果作记录,因此重载AOF文件的时候,不会由于架构不一样产生不一样的结果。

如今咱们知道Redis AOF仅追加文件,因此没有数据损坏。然而这个使人满意的特性也会成为一个问题:在上述例子中,DEL操做的数据没有记录,可是仍然浪费了一些空间。AOF文件是持续增加的,如何避免它过大?

AOF重写

当AOF过大的时候,Redis会尝试在临时文件重写。重写操做不是读取旧的文件,而是直接读取内存中的数据,因此Redis能够建立尽量小的AOF文件。而且在写一个新的文件的时候不须要从磁盘读取。

当重写结束后,临时文件会经过fsync命令同步到到磁盘,替换到原先老的AOF文件。

你可能会疑惑,当重写进程在执行的时候,正好有数据写入到Redis,这时会发生什么。新的数据会写入一份到老的AOF文件,同时写入一份到内存缓冲区,当新的AOF文件已经重写完成后,将新的数据写入。最后将老的AOF替换成新的。

如你所见,全部的数据仅追加在末尾,当咱们重写AOF文件的时候,咱们仍将这些数据写入到老的文件,直到新AOF文件建立成功。这意味着咱们的分析过程能够不考虑AOF的重写。真实的问题是,咱们多久调用一次write,以及多久调用一次fsync。

AOF重写使用顺序的IO操做,因此整个dump进程很是高效(没有随机的IO读写)。这个在生成RDB文件中也是一样的。几乎没有随机IO是一个数据库中很是稀有的特性,大部分缘由是Redis仅从内存中读取数据,不须要在磁盘上组织数据结构提供随机访问。磁盘文件仅用来重启时候经过顺序加载来恢复。

AOF耐久性

这一整篇文章是为了这个段落。

Redis AOF使用用户态缓冲数据。每一次当咱们返回到时间循环的时候,一般会将数据刷新到磁盘,使用write系统调用写入到AOF文件。实际上有3中不一样的配置来控制write和fsync的行为。

这个配置由appendfsync参数控制,有3中不一样的值:no、everysec、always。这个配置能够在线读取,或者经过CONFIG SET在线修改。因此能够随时修改而不须要关闭Redis实例。

appendfsync no

这个配置Redis彻底不执行fsync。可是,它会确认客户端没有使用管道:(一次请求/响应服务器能实现处理新的请求即便旧的请求还未被响应 这样就能够将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复)。指令结果会在命令成功执行后返回:已经经过write系统调用将数据写入到系统缓存。

由于fsync没有被调用,数据什么时候被持久化到磁盘须要看内核的实现,好比在Linux上每30秒刷新一次。

appendfsync everysec

这个配置下,每隔一秒钟,数据会经过write写入到内核缓冲,而后经过fsync刷新到磁盘。 通常的,write操做在返回到事件线程的时候,几乎都会执行,可是这不能保证。

可是,若是磁盘不能应对写入速度,后台的fsync调用所花的时间超过1秒钟,Redis可能会延迟1秒钟再写入到系统缓冲(为了住址写入操做阻塞主进程。由于后台fsync线程写入的是同一个文件)。若是2秒时间过去,fsync未能终止,Redis最终执行一个阻塞式的写入,无论花费多少代价。

因此在这个模式下,Redis能保证在最坏的状况下,2秒钟以内写入的全部数据都会被写入到系统缓冲,而后持久化到磁盘。在通常的场景下,每一秒钟数据都会被提交。

appendfsync always

在这种模式下,而且不使用管道,在返回给客户端响应以前,数据会写入到AOF文件而且经过fsync刷新到磁盘。

这个是你能获取到的关于耐久性的最高保障,可是它比其余模式都要更慢。

默认的Redis配置是appendfsync everysec,它提供了速度(几乎和appendfsync no同样快)和耐久性的平衡。


当Redis的appendfsync配置成always的时候,它采起群组提交的形式。这意味着,写入的时候,Redis不是每次都提交,它可以将这些操做组合成一个write+fsync的操做。

在实践中,这意味着你能够用几百个客户端同时操做redis:fsync操做会被分解 - 因此即便在这种模式下,Redis能够支持几千个TPS,即便磁盘每秒只能支持100~200次写入。

对于传统数据库来讲,这个特性一般很难实现,可是Redis让它变得更加简单。

为何采用pipeline会不同

用不一样的方式处理客户端使用管道的缘由是,使用pipeline的客户端为了速度,牺牲了响应性。开启了pipeline后,指令会批量提交,那么Redis就不能在下一个指令被执行前知道前一个指令的结果。对于pipeline客户端来讲,在响应前提交写入是没有必要的,客户端需求的是速度。然而即便客户端使用管道,write和fsync常常在返回到事件循环的时候发生。

AOF和Redis事务

AOF保证MULTI/EXEC事务语义上的正确性,它发现文件末尾有损坏的事务的时候,会拒绝加载。Redis有工具能够整理AOF文件,去除掉末尾损坏的事务

注意:由于AOF只是经过单独的write写入(没有同步到磁盘),不完整的事务只会出如今磁盘已经被写满的时候。

和PostrgreSQL对比

AOF默认配置的耐久性如何?

  • 最坏状况:它保证write和fsync在两秒内执行。
  • 通常状况:给客户端响应以前已经write,每秒钟fsync一次。

有趣的是在这种模式下,Redis依然很是快。这里有几个缘由,一方面是fsync经过后台线程执行,另外一方面是redis是文件追加的形式写,这个是一个很大的提高。

然而,若是你须要最大程度的数据安全性保障,并且负载不高,那么为了达到最好的耐久性,应该配置fsync always。

这点和PostgreSQL相好比何?PostgreSQL被认为是一个很好很可靠的数据库。

让咱们一块儿阅读PostgreSQL的文档(注意:我只应用有趣的片断,你能够在这里查看完整的PostgreSQL文档)


fsync(boolean)

若是开启这个参数,PostgreSQL会尝试去保证修改被写入到物理介质,经过fsync或者相似的方式(好比wal_sync_method)。这个能够保证数据库集群能够在操做系统或者软件宕机后恢复到一致的状态。

在不少场景中,不重要的事务能够关闭同步提交,能够提供更好的性能,不存在数据损坏的风险。


因此PostgreSQL须要fsync防止数据损坏。幸亏Redis有AOF,咱们不存在数据损坏的问题。让咱们看下一个参数,这个和Redis的fsync策略更为接近,尽管名字不一样。


synchronous_commit (enum)

这个参数指定在返回给客户端“success”以前,事务提交是否须要等待WAL记录写入到磁盘。正确的参数值是on、local、off。默认的安全的配置是on。当参数为off的时候,返回给客户端的是成功,可是事务此时尚未安全的写入到磁盘,若是服务宕机,不能保证安全性(最大的延迟是3个wal_writer_delay)。不像fsync,这个参数设置成off不会致使数据库的磁盘存在不一致的风险:操做系统或者数据库宕机会引发最近提交的事务丢失,可是数据库的状态和抛弃这些事务的状态是一致的。


这里有不少类似点,咱们能够参照来调优Redis。基本的,PostgreSQL的兄弟会告诉你,设么是速度?禁用同步的提交多是一个好主意。就像Redis中同样:想要速度?不要配置appendfsync always。

若是在PostgreSQL中禁用同步提交,那么和使用Redis的appendfsync everysec很想,由于PostgreSQL默认的wal_writer_delay 延迟200毫秒,文档中写明了须要将这个延迟乘以3才是真实的延迟,也就是600毫秒,很是接近Redis默认的1秒。


Mysql的InnoDb有相似的参数可供用户调优。从文档中能够看到:

若是innodb_flush_log_at_trx_commit的配置项为0,日志缓冲每隔一秒钟将数据写入到文件并刷新到磁盘。这个配置下事务提交不会当即提交到磁盘。当配置值为1的时候,日志缓冲在每次事务提交的时候都会刷新到磁盘。当配置值为2的时候,每次事务提交都会写入到文件,可是不肯定刷新到硬盘的时间。然而,当配置值为2的时候,仍是会每秒刷新到磁盘。须要注意的是,由于进程调度的问题,每一秒刷新不是百分百肯定的。

你能够点击这里查看更多。

长话短说:虽然Redis是一个内存数据库,可是它提供了和基于磁盘的数据库差很少良好的耐久性。

从更实际的角度去看,Redis提供了AOF和RDB,它们能够同时开启(这个也是推荐配置)。同时开启能够提供高效的操做和耐久性。

这里咱们讨论的Redis耐久性,不仅仅适用于将Redis做为数据存储,也适用于作中间件,由于它提供了良好的耐久性。


因为我的能力有限,在翻译过程当中不免有一些疏漏,能够在博客下面留言,或者联系个人邮箱moyiguke@hotmail.com修改。

原文有一些地方直译较为难懂,在尽可能保持原意的基础上作了一些修改。


一些总结

持久化的核心点:write,fsync。write在语义上是写入到文件,可是操做系统作了优化,不会直接往磁盘去写,而是保存在页面缓冲(page cache)中。fsync这条指令会强制将页面缓冲的数据往磁盘写入,可是磁盘并不会当即固化,亦它也有一层缓冲。不过磁盘的缓冲咱们通常不去考虑,简单的认为fsync保证了数据写入到物理介质了。

再谈为何会有多层缓冲。考虑这样的场景,若是持续性的写入到磁盘,IO会一直处于高负荷,并且负载到达必定程度,磁盘没法提供写入。这时为了避免丢失数据,仍须要有内存进行数据的缓冲。这是第一点,缓冲没法避免。另外一点,缓冲能够带来写入性能的提高。在内存中,将小段的数据组合成了大段的数据,而后一次性同步到磁盘,减小了不少的磁盘访问。

这篇文章主要讲了数据安全性以及耐久性。数据安全即数据落地到磁盘,耐久性即能作到什么程度的数据同步。这两点实际上有部分重叠,数据安全和耐久性的评价标准都是持久化到磁盘,经过fsync或者相似的实现。举一个例子,若是某个内存数据库宕机后没法保留数据,启动后是全新的,那么它是不安全的,不耐久的。Redis在AOF默认配置下,重启后能够恢复数据,至多丢失两秒数据,那么它是安全的,耐久性是平均一秒数据的丢失。

若是在阅读的时候,仍有一些难以理解的地方,欢迎留言给我,我将尽力解答。