RDB持久化与AOF持久化
Redis高可用概述
在介绍Redis高可用之前,先说明一下在Redis的语境中高可用的含义。
我们知道,在web服务器中,高可用是指服务器可以正常访问的时间,衡量的标准是在多长时间内可以提供正常服务(99.9%、99.99%、99.999% 等等)。但是在Redis语境中,高可用的含义似乎要宽泛一些,除了保证提供正常服务(如主从分离、快速容灾技术),还需要考虑数据容量的扩展、数据安全不会丢失等。
在Redis中,实现高可用的技术主要包括持久化、复制、哨兵和集群,下面分别说明它们的作用,以及解决了什么样的问题。
- 持久化:持久化是最简单的高可用方法(有时甚至不被归为高可用的手段),主要作用是数据备份,即将数据存储在硬盘,保证数据不会因进程退出而丢失。
- 复制:复制是高可用Redis的基础,哨兵和集群都是在复制基础上实现高可用的。复制主要实现了数据的多机备份,以及对于读操作的负载均衡和简单的故障恢复。(缺陷:故障恢复无法自动化;写操作无法负载均衡;存储能力受到单机的限制)
- 哨兵:在复制的基础上,哨兵实现了自动化的故障恢复。缺陷:写操作无法负载均衡;存储能力受到单机的限制。
- 集群:通过集群,Redis解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案。
什么是持久化?
持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中、XML数据文件中等等。持久化是将程序数据在持久状态和瞬时状态间转换的机制。 —-摘自百度百科
Redis的数据都是存储在内存中的,所以Redis持久化也就是要把Redis存储在内存中的数据保存到硬盘。
Redis有两种持久化的方式:快照(RDB文件)和追加式文件(AOF文件)。前者将当前数据保存到硬盘,后者则是将每次执行的写命令保存到硬盘(类似于MySQL的binlog);由于AOF持久化的实时性更好,即当进程意外退出时丢失的数据更少,因此AOF是目前主流的持久化方式,不过RDB持久化仍然有其用武之地。
- RDB持久化方式会在一个特定的间隔保存那个时间点的一个数据快照。
- AOF持久化方式则会记录每一个服务器收到的写操作。在服务启动时,这些记录的操作会逐条执行从而重建出原来的数据。写操作命令记录的格式跟Redis协议一致,以追加的方式进行保存。
- Redis的持久化是可以禁用的,就是说你可以让数据的生命周期只存在于服务器的运行时间里。
- 两种方式的持久化是可以同时存在的,但是当Redis重启时,AOF文件会被优先用于重建数据。
RDB持久化
RDB持久化是指在客户端输入save、bgsave或者达到配置文件自动保存快照条件时,将Redis 在内存中的数据生成快照保存在名字为 dump.rdb(文件名可修改)的二进制文件中。
创建与载入
RDB文件可以通过两个命令来生成:
- SAVE:save命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在Redis服务器阻塞期间,服务器不能处理任何命令请求。
- BGSAVE:派生(fork)一个子进程来创建新的RDB文件,记录接收到BGSAVE当时的数据库状态,父进程继续处理接收到的命令,子进程完成文件的创建之后,会发送信号给父进程,而与此同时,父进程处理命令的同时,通过轮询来接收子进程的信号。
bgsave执行流程图:
注:bgsave命令执行期间 SAVE命令会被拒绝 不能同时执行两个BGSAVE命令 不能同时执行BGREWRITEAOF和BGSAVE命令
RDB文件的载入一般情况是自动的,redis服务器启动的时候如果检测到RDB文件的存在,那么redis会自动载入这个文件。
如果服务器开启了AOF持久化,那么服务器会优先使用AOF文件来还原数据库状态。
RDB是通过保存键值对来记录数据库状态的,采用copy on write的模式,每次都是全量的备份。
配置
BGSAVE可以在不阻塞主进程的情况下完成数据的备份。可以通过redis.conf中设置多个自动保存条件,只要有一个条件被满足,服务器就会执行BGSAVE命令。
1 | # 触发自动保存快照 |
优缺点
优点:
- RDB是一个非常紧凑(有压缩)的文件,它保存了某个时间点的数据,非常适用于数据的备份。
- RDB作为一个非常紧凑(有压缩)的文件,可以很方便传送到另一个远端数据中心 ,非常适用于灾难恢复.
- RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能.
- 与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些.
缺点:
- RDB容易造成数据的丢失。假设每5分钟保存一次快照,如果Redis因为某些原因不能正常工作,那么从上次产生快照到Redis出现问题这段时间的数据就会丢失了。
- RDB使用fork()产生子进程进行数据的持久化,如果数据比较大的话可能就会花费点时间,造成Redis停止服务几毫秒。如果数据量很大且CPU性能不是很好的时候,停止服务的时间甚至会到1秒。
AOF持久化
快照并不是很可靠。如果你的电脑突然宕机了,或者电源断了,又或者不小心杀掉了进程,那么最新的数据就会丢失。而AOF文件则提供了一种更为可靠的持久化方式。
AOF持久化(Append-Only-File),与RDB持久化不同,AOF持久化是通过保存Redis服务器锁执行的写状态来记录数据库的。每当Redis接受到会修改数据集的命令时,就会把命令追加到AOF文件里,当你重启Redis时,AOF里的命令会被重新执行一次,重建数据。
具体来说,RDB持久化相当于备份数据库状态,而AOF持久化是备份数据库接收到的命令,所有被写入AOF的命令都是以redis的协议格式来保存的。
在AOF持久化的文件中,数据库会记录下所有变更数据库状态的命令,除了指定数据库的select命令,其他的命令都是来自client的,这些命令会以追加(append)的形式保存到文件中。
开启AOF持久化
修改redis.conf配置文件,默认是appendonly no(关闭状态),将no改为yes即可
1 | appendonly yes |
在客户端输入如下命令也可,但是Redis服务器重启后会失效
1 | 127.0.0.1:6379> config set appendonly yes |
接下来看看AOF持久化功能的实现
实现
AOF持久化功能的实现可以分为命令追加(append)、文件写入和文件同步(sync)三个步骤。下面就是三个步骤的整个过程。
在Redis客户端输入如下命令
1 | 127.0.0.1:6379> set learnRedis testAOF |
appendonly.aof文件会增加如下内容
1 | *2 |
命令追加
AOF持久化功能开启时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。此时缓冲区的记录还没有写入到appendonly.aof文件中。
文件的写入和同步
为什么将文件写入和文件同步合在一块讲呢?因为配置文件中提供了一个appendfsync参数,这个参数控制着文件写入和同步的行为。
关于文件的写入和同步的资料如下
因为为了提高文件的写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,os通常会将写入数据暂时保存在一个内存缓冲区里面(例如,unix系统实现在内核中设有缓冲区高速缓存或页高速缓存,当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘),这种方式称为延迟写,等到缓冲区的空间被填满,或者超过了指定的时限,或者内核需要重用缓冲区存放其它磁盘块数据时,才会真正将缓冲区中的所有数据写入到磁盘里面。
简单来说就是
- 文件写入:只是写入到了内存缓冲区,可能还没有写到文件所拥有的磁盘数据块上
- 文件同步:将缓冲区中的内容冲洗到磁盘上
你可以配置Redis调用fsync的频率,有三个选项:
- 每当有新命令追加到AOF的时候调用fsync。速度最慢,但是最安全。
- 每秒fsync一次。速度快(2.4版本跟快照方式速度差不多),安全性不错(最多丢失1秒的数据)。
- 从不fsync,交由系统去处理。这个方式速度最快,但是安全性一般。
推荐使用每秒fsync一次的方式(默认的方式),因为它速度快,安全性也不错。参数如下:
appendfsync | 效果 |
---|---|
always | 每次有新命令时,就将缓冲区数据写入并同步到 AOF 文件) |
everysec | 每秒将缓冲区的数据写入并同步到 AOF 文件 |
no | 将缓冲区数据写入AOF 文件,但是同步操作到交给操作系统来处理 |
载入与数据还原
读取AOF文件并还原数据库的步骤如下
- 创建一个不带网络连接的伪客户端
- 从AOF文件中分析并读取出一条写命令
- 使用伪客户端执行被读出的写命令
- 一直执行步骤2、3,直到AOF文件中的所有写命令都被处理完毕为止
这时可能会出现一个问题。服务器可能在程序正在对 AOF 文件进行写入时停机,造成了 AOF 文件出错,那么 Redis 在重启时会拒绝载入这个 AOF 文件,从而确保数据的一致性不会被破坏 当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:
- 为现有的 AOF 文件创建一个备份。
- 使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复: redis-check-aof –fix
- (可选)使用 diff -u 对比修复后的 AOF 文件和原始 AOF 文件的备份,查看两个文件之间的不同之处。
- 重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复。
另外redis.conf配置文件中还提供了一个参数来控制是否忽略最后一条可能存在问题的指令,如下
1 | aof-load-truncated yes |
日志重写
随着写操作的不断增加,AOF文件会越来越大。例如你递增一个计数器100次,那么最终结果就是数据集里的计数器的值为最终的递增结果,但是AOF文件里却会把这100次操作完整的记录下来。而事实上要恢复这个记录,只需要1个命令就行了,也就是说AOF文件里那100条命令其实可以精简为1条。所以Redis支持这样一个功能:在不中断服务的情况下在后台重建AOF文件。
AOF重写机制的触发有两种机制,一个是通过调用命令BGREWRITEAOF
1 | 127.0.0.1:6379> BGREWRITEAOF |
另一种是根据配置文件中的参数触发,参数如下
1 | auto-aof-rewrite-percentage 100 #当前AOF文件大小和上一次重写时AOF文件大小的比值 |
服务端会出现如下信息
1 | 1349:M 30 Jul 17:19:25.311 * Background append only file rewriting started by pid 1392 |
重写步骤
- Redis调用fork(),产生一个子进程。
- 子进程把新的AOF写到一个临时文件里。
- 主进程持续把新的变动写到内存里的buffer,同时也会把这些新的变动写到旧的AOF里,这样即使重写失败也能保证数据的安全。
- 当子进程完成文件的重写后,主进程会获得一个信号,然后把内存里的buffer追加到子进程生成的那个新AOF里。
- 对新的AOF文件进行改名,原子的覆盖现有的AOF文件。
文件重写流程图:
AOF重写功能有大量写入操作,所以redis才用子进程来处理AOF重写。这里带来一个新的问题,由于处理重新的是子进程,这样意味着如果主线程的数据在此时被修改,备份的数据和主库的数据将会有不一致的情况发生。因此redis还设置了一个AOF重写缓冲区,这个缓冲区在子进程被创建开始之后开始使用,这个期间,所有的命令会被存两份,一份在AOF缓存空间,一份在AOF重写缓冲区,当AOF重写完成之后,子进程发送信号给主进程,通知主进程将AOF重写缓冲区的内容添加到AOF文件中。
注:AOF重写不需要对现有的AOF文件进行任何读取、分析和写入操作。
配置
1 | # 是否开启AOF功能 |
优缺点
优点:
- 比RDB可靠。你可以制定不同的fsync策略:不进行fsync、每秒fsync一次和每次查询进行fsync。默认是每秒fsync一次。这意味着你最多丢失一秒钟的数据。
- AOF日志文件是一个纯追加的文件。就算是遇到突然停电的情况,也不会出现日志的定位或者损坏问题。甚至如果因为某些原因(例如磁盘满了)命令只写了一半到日志文件里,我们也可以用redis-check-aof这个工具很简单的进行修复。
- 当AOF文件太大时,Redis会自动在后台进行重写。重写很安全,因为重写是在一个新的文件上进行,同时Redis会继续往旧的文件追加数据。新文件上会写入能重建当前数据集的最小操作命令的集合。当新文件重写完,Redis会把新旧文件进行切换,然后开始把数据写到新文件上。
- AOF把操作命令以简单易懂的格式一条接一条的保存在文件里,很容易导出来用于恢复数据。例如我们不小心用FLUSHALL命令把所有数据刷掉了,只要文件没有被重写,我们可以把服务停掉,把最后那条命令删掉,然后重启服务,这样就能把被刷掉的数据恢复回来。
缺点:
- 在相同的数据集下,AOF文件的大小一般会比RDB文件大。
- 在某些fsync策略下,AOF的速度会比RDB慢。通常fsync设置为每秒一次就能获得比较高的性能,而在禁止fsync的情况下速度可以达到RDB的水平。
- 在过去曾经发现一些很罕见的BUG导致使用AOF重建的数据跟原数据不一致的问题。
方案选择与常见问题
持久化策略选择
在介绍持久化策略之前,首先要明白无论是RDB还是AOF,持久化的开启都是要付出性能方面代价的:
- 对于RDB持久化,一方面是bgsave在进行fork操作时Redis主进程会阻塞,另一方面,子进程向硬盘写数据也会带来IO压力;
- 对于AOF持久化,向硬盘写数据的频率大大提高(everysec策略下为秒级),IO压力更大,甚至可能造成AOF追加阻塞问题(后面会详细介绍这种阻塞);
- 此外,AOF文件的重写与RDB的bgsave类似,会有fork时的阻塞和子进程的IO压力问题。相对来说,由于AOF向硬盘中写数据的频率更高,因此对Redis主进程性能的影响会更大。
在实际生产环境中,根据数据量、应用对数据的安全要求、预算限制等不同情况,会有各种各样的持久化策略;如完全不使用任何持久化、使用RDB或AOF的一种,或同时开启RDB和AOF持久化等。此外,持久化的选择必须与Redis的主从策略一起考虑,因为主从复制与持久化同样具有数据备份的功能,而且主机master和从机slave可以独立的选择持久化方案。
下面分场景来讨论持久化策略的选择,下面的讨论也只是作为参考,实际方案可能更复杂更具多样性。
(1)如果Redis中的数据完全丢弃也没有关系(如Redis完全用作DB层数据的cache),那么无论是单机,还是主从架构,都可以不进行任何持久化。
(2)在单机环境下(对于个人开发者,这种情况可能比较常见),如果可以接受十几分钟或更多的数据丢失,选择RDB对Redis的性能更加有利;如果只能接受秒级别的数据丢失,应该选择AOF。
(3)但在多数情况下,我们都会配置主从环境,slave的存在既可以实现数据的热备,也可以进行读写分离分担Redis读请求,以及在master宕掉后继续提供服务。在这种情况下,一种可行的做法是:
- master:完全关闭持久化(包括RDB和AOF),这样可以让master的性能达到最好
- slave:关闭RDB,开启AOF(如果对数据安全要求不高,开启RDB关闭AOF也可以),并定时对持久化文件进行备份(如备份到其他文件夹,并标记好备份的时间);然后关闭AOF的自动重写,然后添加定时任务,在每天Redis闲时(如凌晨12点)调用bgrewriteaof。
这里需要解释一下,为什么开启了主从复制,可以实现数据的热备份,还需要设置持久化呢?因为在一些特殊情况下,主从复制仍然不足以保证数据的安全,例如:
- master和slave进程同时停止:考虑这样一种场景,如果master和slave在同一栋大楼或同一个机房,则一次停电事故就可能导致master和slave机器同时关机,Redis进程停止;如果没有持久化,则面临的是数据的完全丢失。
- master误重启:考虑这样一种场景,master服务因为故障宕掉了,如果系统中有自动拉起机制(即检测到服务停止后重启该服务)将master自动重启,由于没有持久化文件,那么master重启后数据是空的,slave同步数据也变成了空的;如果master和slave都没有持久化,同样会面临数据的完全丢失。需要注意的是,即便是使用了哨兵(关于哨兵后面会有文章介绍)进行自动的主从切换,也有可能在哨兵轮询到master之前,便被自动拉起机制重启了。因此,应尽量避免“自动拉起机制”和“不做持久化”同时出现。
(4)异地灾备:上述讨论的几种持久化策略,针对的都是一般的系统故障,如进程异常退出、宕机、断电等,这些故障不会损坏硬盘。但是对于一些可能导致硬盘损坏的灾难情况,如火灾地震,就需要进行异地灾备。例如对于单机的情形,可以定时将RDB文件或重写后的AOF文件,通过scp拷贝到远程机器,如阿里云、AWS等;对于主从的情形,可以定时在master上执行bgsave,然后将RDB文件拷贝到远程机器,或者在slave上执行bgrewriteaof重写AOF文件后,将AOF文件拷贝到远程机器上。一般来说,由于RDB文件文件小、恢复快,因此灾难恢复常用RDB文件;异地备份的频率根据数据安全性的需要及其他条件来确定,但最好不要低于一天一次。
fork阻塞:CPU的阻塞
在Redis的实践中,众多因素限制了Redis单机的内存不能过大,例如:
- 当面对请求的暴增,需要从库扩容时,Redis内存过大会导致扩容时间太长;
- 当主机宕机时,切换主机后需要挂载从库,Redis内存过大导致挂载速度过慢;
- 以及持久化过程中的fork操作,下面详细说明。
首先说明一下fork操作:
- 父进程通过fork操作可以创建子进程;子进程创建后,父子进程共享代码段,不共享进程的数据空间,但是子进程会获得父进程的数据空间的副本。
- 在操作系统fork的实际实现中,基本都采用了写时复制技术,即在父/子进程试图修改数据空间之前,父子进程实际上共享数据空间;但是当父/子进程的任何一个试图修改数据空间时,操作系统会为修改的那一部分(内存的一页)制作一个副本。
- 虽然fork时,子进程不会复制父进程的数据空间,但是会复制内存页表(页表相当于内存的索引、目录);父进程的数据空间越大,内存页表越大,fork时复制耗时也会越多。
在Redis中,无论是RDB持久化的bgsave,还是AOF重写的bgrewriteaof,都需要fork出子进程来进行操作。如果Redis内存过大,会导致fork操作时复制内存页表耗时过多;而Redis主进程在进行fork时,是完全阻塞的,也就意味着无法响应客户端的请求,会造成请求延迟过大。
对于不同的硬件、不同的操作系统,fork操作的耗时会有所差别,一般来说,如果Redis单机内存达到了10GB,fork时耗时可能会达到百毫秒级别(如果使用Xen虚拟机,这个耗时可能达到秒级别)。因此,一般来说Redis单机内存一般要限制在10GB以内;不过这个数据并不是绝对的,可以通过观察线上环境fork的耗时来进行调整。观察的方法如下:执行命令info stats,查看latest_fork_usec的值,单位为微秒。
为了减轻fork操作带来的阻塞问题,除了控制Redis单机内存的大小以外,还可以适度放宽AOF重写的触发条件、选用物理机或高效支持fork操作的虚拟化技术等,例如使用Vmware或KVM虚拟机,不要使用Xen虚拟机。
AOF追加阻塞:硬盘的阻塞
前面提到过,在AOF中,如果AOF缓冲区的文件同步策略为everysec,则:在主线程中,命令写入aof_buf后调用系统write操作,write完成后主线程返回;fsync同步文件操作由专门的文件同步线程每秒调用一次。
这种做法的问题在于,如果硬盘负载过高,那么fsync操作可能会超过1s;如果Redis主线程持续高速向aof_buf写入命令,硬盘的负载可能会越来越大,IO资源消耗更快;如果此时Redis进程异常退出,丢失的数据也会越来越多,可能远超过1s。
为此,Redis的处理策略是这样的:主线程每次进行AOF会对比上次fsync成功的时间;如果距上次不到2s,主线程直接返回;如果超过2s,则主线程阻塞直到fsync同步完成。因此,如果系统硬盘负载过大导致fsync速度太慢,会导致Redis主线程的阻塞;此外,使用everysec配置,AOF最多可能丢失2s的数据,而不是1s。
AOF追加阻塞问题定位的方法:
(1)监控info Persistence中的aof_delayed_fsync:当AOF追加阻塞发生时(即主线程等待fsync而阻塞),该指标累加。
(2)AOF阻塞时的Redis日志:
Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.
(3)如果AOF追加阻塞频繁发生,说明系统的硬盘负载太大;可以考虑更换IO速度更快的硬盘,或者通过IO监控分析工具对系统的IO负载进行分析,如iostat(系统级io)、iotop(io版的top)、pidstat等。
info命令与持久化
前面提到了一些通过info命令查看持久化相关状态的方法,下面来总结一下。
info Persistence
执行结果如下:
其中比较重要的包括:
- rdb_last_bgsave_status:上次bgsave 执行结果,可以用于发现bgsave错误
- rdb_last_bgsave_time_sec:上次bgsave执行时间(单位是s),可以用于发现bgsave是否耗时过长
- aof_enabled:AOF是否开启
- aof_last_rewrite_time_sec: 上次文件重写执行时间(单位是s),可以用于发现文件重写是否耗时过长
- aof_last_bgrewrite_status: 上次bgrewrite执行结果,可以用于发现bgrewrite错误
- aof_buffer_length和aof_rewrite_buffer_length:aof缓存区大小和aof重写缓冲区大小
- aof_delayed_fsync:AOF追加阻塞情况的统计
info stats
其中与持久化关系较大的是:latest_fork_usec,代表上次fork耗时,可以参见前面的讨论。
总结
本文主要内容可以总结如下:
- 持久化在Redis高可用中的作用:数据备份,与主从复制相比强调的是由内存到硬盘的备份。
- RDB持久化:将数据快照备份到硬盘;介绍了其触发条件(包括手动出发和自动触发)、执行流程、RDB文件等,特别需要注意的是文件保存操作由fork出的子进程来进行。
- AOF持久化:将执行的写命令备份到硬盘(类似于MySQL的binlog),介绍了其开启方法、执行流程等,特别需要注意的是文件同步策略的选择(everysec)、文件重写的流程。
- 一些现实的问题:包括如何选择持久化策略,以及需要注意的fork阻塞、AOF追加阻塞等。