Redis存储揭秘(翻译)

Redis存储揭秘(翻译)

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

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

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

##操作系统和磁盘

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

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

注意:上述步骤是非常简化的,真实环境的缓存更为复杂。

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

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

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

在什么时候,我们的写是安全的

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

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

步骤3、4、5中针对数据安全性的的重要阶段,我们可以总结一下。

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

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

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

操作系统API

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

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

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

系统API提供了一系列系统调用来强制将内核缓冲写入到硬盘,最有名的是fsync系统调用(更多信息可以搜索msync,fdatasync)。数据库使用fysnc将内核中的数据提交到硬盘,但是可以猜测到,这是一个非常昂贵的操作:如果fsync被调用并且内核缓冲有数据的时候,会立即执行执行一个写操作。fsync会一直阻塞写入的进程,在Linux上,fsync还会阻塞写入同一个文件的其他线程。

哪些是我们无法控制的

目前为止,我们学习到可以控制第3、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作为数据存储,也适用于做中间件,因为它提供了良好的耐久性。


由于个人能力有限,在翻译过程中难免有一些疏漏,可以在博客下面留言,或者联系我的邮箱[email protected]修改。

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


一些总结

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

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

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

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

猜你喜欢

转载自my.oschina.net/u/992559/blog/1815022