概述
持久化的功能:Redis是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将Redis中的数据以某种形式(数据或命令)从内存保存到硬盘;当下次Redis重启时,利用持久化文件实现数据恢复。除此之外,为了进行灾难备份,可以将持久化文件拷贝到一个远程位置。
Redis持久化分为:
- RDB持久化
- AOF持久化
RDB将当前数据保存到硬盘,AOF则是将每次执行的写命令保存到硬盘(类似于MySQL的binlog);一般生产中,并不会只是用一种,而是两种持久化方式联合使用保证数据不会丢失
RDB 持久化
RDB持久化又称为内存快照,所谓内存快照,顾名思义就是给内存拍个照,在某个时刻把内存中的数据记录下来,以文件的形式保存到硬盘上,这样即使宕机,数据依然存在。在服务器重启后只需要把“照片”中的数据恢复即可
RDB 文件创建写入
RDB持久化就是把当前进程的数据在某个时刻生成快照(一个压缩的二进制文件)保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发
- 自动触发
通过设置redis.conf配置文件save即可:
#Redis 的默认配置
# save “”
save 900 1
save 300 10
save 60 10000
PS:
那么只要满足如下条件中的一个bgsave命令就会被执行:
- 服务器在900秒内对数据库进行了至少1次修改
- 服务器在300秒内对数据库进行了至少10次修改
- 服务器在60秒内对数据库进行了至少10000次修改
- 手动触发命令:save m n参数说明:指定当m秒内发生n次变化时,会触发bgsavesave命令会阻塞当前Redis服务器,直到RDB过程完成为止。在服务器进程阻塞期间,服务器不能处理任何命令请求。因此,当save命令正在执行时,客户端发送的所有命令都会被拒绝,直到save命令执行完毕注意:Redis的单线程模型就决定了,我们要尽量避免所有会阻塞主线程的操作,由于Save命令执行期间阻塞服务器进程,对于内存比较大的实例会造成长时间阻塞,因此线上环境不建议使用save m n的实现原理redis.conf中save配置解释例如:如果Redis执行了set mykey helloworld,则dirty值会+1;如果执行了sadd myset v1 v2 v3,则dirty值会+3;注意dirty记录的是服务器进行了多少次修改,而不是客户端执行了多少修改数据的命令。lastsave时间戳也是Redis服务器维持的一个状态,记录的是上一次成功执行save/bgsave的时间。save m n的原理如下:每隔100ms,执行serverCron函数;在serverCron函数中,遍历save m n配置的保存条件,只要有一个条件满足,就进行bgsave。对于每一个save m n条件,只有下面两条同时满足时才算满足:(1)当前时间-lastsave > m(2)dirty >= nbgsave命令会派生出一个子进程(而不是线程),由子进程进行RDB文件创建,而父进程继续处理命令 127.0.0.1:6379> bgsave
Background saving started//直接返回,由子进程进行RDB文件创建
127.0.0.1:6379> //主进程正常执行命令子进程持久化文件过程:- save命令触发
- Redis的save m n,是通过serverCron函数、dirty计数器、和lastsave时间戳来实现的。
- serverCron是Redis服务器的周期性操作函数,默认每隔100ms执行一次;该函数对服务器的状态进行维护,其中一项工作就是检查 save m n 配置的条件是否满足,如果满足就执行bgsave。
- dirty计数器是Redis服务器维持的一个状态,记录了上一次执行bgsave/save命令后,服务器状态进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。
- bgsave命令触发
执行步骤:
- Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(后面会详细介绍该命令)的子进程,如果在执行则bgsave命令直接返回。bgsave/bgrewriteaof的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。
- 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令
- 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令
- 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换
- 子进程发送信号给父进程表示完成,父进程更新统计信息
注意:
- 在bgsave命令执行的时候,为了避免父进程与子进程同时执行两个rdbSave的调用而产生竞争条件,客户端发送的save命令会被服务器拒绝。
- 如果bgsave命令正在执行,bgrewriteaof(aof重写)命令会被延迟到bgsave命令之后执行,如果bgrewriteaof命令正在执行,那么客户端发送的bgsave命令会被服务器拒绝。
- 虽然bgsave命令是由子进程进行RDB文件的生成,但是fork()创建子进程的时候会阻塞父进程(详情请往下看)。
RDB 文件格式
RDB文件是经过压缩的二进制文件
存储路径:
- 配置:dir配置指定目录,dbfilename指定文件名。默认是Redis根目录下的dump.rdb文件。动态设定:Redis启动后也可以动态修改RDB存储路径,在磁盘损害或空间不足时非常有用;执行命令为config set dir {newdir}和config set dbfilename {newFileName}。如下所示(Windows环境):
RDB文件格式如下图所示(图片来源:《Redis设计与实现》):
PS:字段解释
- REDIS:常量,保存着”REDIS”5个字符。
- db_version:RDB文件的版本号,注意不是Redis的版本号。
- SELECTDB 0 pairs:表示一个完整的数据库(0号数据库),同理SELECTDB 3 pairs表示完整的3号数据库;只有当数据库中有键值对时,RDB文件中才会有该数据库的信息(上图所示的Redis中只有0号和3号数据库有键值对);如果Redis中所有的数据库都没有键值对,则这一部分直接省略。其中:SELECTDB是一个常量,代表后面跟着的是数据库号码;0和3是数据库号码;pairs则存储了具体的键值对信息,包括key、value值,及其数据类型、内部编码、过期时间、压缩信息等等。
- EOF:常量,标志RDB文件正文内容结束。
- check_sum:前面所有内容的校验和;Redis在载入RBD文件时,会计算前面的校验和并与check_sum值比较,判断文件是否损坏
压缩:
Redis默认采用LZF算法对RDB文件进行压缩。虽然压缩耗时,但是可以大大减小RDB文件的体积,因此压缩默认开启;可以通过命令关闭:
需要注意的是,RDB文件的压缩并不是针对整个文件进行的,而是对数据库中的字符串进行的,且只有在字符串达到一定长度(20字节)时才会进行。
RDB文件的载入
在Redis启动的时候,只要检测到RDB文件的存在,就会自动加载RDB文件。需要注意的是
- 因为AOF文件的更新频率通常比RDB文件的更新频率高,所以如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态【保证最新数据】
- 只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态
注意:服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止
内存快照的问题
1. 快照的时候数据可以修改吗?
Redis RDB持久化是对某一时刻的内存中的全量数据进行拍照。这让我们不得不思考,快照的时候数据可以修改吗?
首先,如果我们使用save命令做持久化,那么由于Redis单线程模型的原因,在持久化的过程中会阻塞,是不能执行其它命令的。也许有人会说可以使用bgave命令,但使用bgsave就没有问题了吗?
我们在拍照的时候,通常摄影师是不让我们动的,因为一动可能照片就模糊了。在Redis 进行内存快照的时候也会如此。如果我们持久化的过程中,有些数据被修改了。那么就会破坏快照的正确性与完整性。
比如在t时刻,我们对内存进行快照,此时我们希望的是记录下来t时刻内存中所有的数据,假设我们的RDB操作需要10s的时间,而t+2s我们执行了一个修改操作把Key1的值由A修改成了B,而此时RDB操作却还没有把Key1的值写入磁盘。在t+5s的时候读取到key1的值写入磁盘。那么此次快照记录的Key1的值就是B,而不是t时刻的A。这样就破坏了RDB文件的正确性。
RDB文件的生成是需要时间的,如果快照执行期间数据不能被修改,对于业务系统来说不能接受的。那么Redis 是如何解决这个问题的呢?
Redis 借助了操作系统提供的写时复制技术(Copy-On-Write, COW),可以让在执行快照的同时,正常处理写操作。简单来说,bgsave fork子进程的时候,并不会完全复制主进程的内存数据,而是只复制必要的虚拟数据结构,并不为其分配真实的物理空间,它与父进程共享同一个物理内存空间。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,此时会给子进程分配一块物理内存空间,把要修改的数据复制一份,生成该数据的副本到子进程的物理内存空间。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
2. 可以频繁进行快照操作吗?
假设我们在t 时刻做了一次快照,然后又在 t+n 时刻做了一次快照,而在这期间,发生了数据修改。而此时宕机了,那么,只能按照 t 时刻的快照进行恢复。那么这n秒的数据就彻底丢失无法恢复了。
所以,要想尽可能恢复数据,就只能缩短快照执行的时间间隔,间隔的时间越小,丢失数据也就越少。那么可以频繁的执行快照操作吗?
我们知道bgsave 执行时并不阻塞主线程,但是这不代表可以频繁执行快照操作。
一方面,持久化是一个写入磁盘的过程,频繁将全量数据写入磁盘,会给磁盘带来很大压力,频繁执行快照也容易导致前一个快照还没有执行完,后一个又开始了,这样多个快照竞争有限的磁盘带宽,容易造成恶性循环。
再者,bgsave所fork出来的子进程执行操作虽然并不会阻塞父进程的操作,但是fork出子进程的操作却是由主进程完成的,会阻塞主进程,fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间也就越久。
也许有人会想到是否可以做增量快照呢?也就是只对上一次快照之后的数据做快照。
首先思路肯定是可以,但是增量快照要求记住哪些数据上一次快照之后产生的。这就需要额外的元数据来记录这些信息,会引入额外的空间消耗
RDB 优缺点
优势:
- RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
- 对于大规模数据的恢复,且对于数据恢复的完整性不是非常敏感的场景,RDB的恢复速度要比AOF方式更加的高效。
- 生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
劣势:
- fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。
- 当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不6会被保存,可能丢失数据。
- 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改。
- 兼容性差:由于Redis更新换代的过程中RDB文件的格式一直在变化,老的版本Redis 可能无法恢复新版本的RDB文件
RDB 常用配置
AOF 持久化
Redis内存快照进行持久化,在t 时刻做了一次快照,然后又在 t+n 时刻做了一次快照,此时期间如果宕机,则会丢失在此期间内修改的数据。但又不能频繁的进行内存快照,那么有什么办法能够尽可能的减少这种数据丢失呢?Redis提供了另一种持久化的方式——AOF日志(Append Only File)。
AOF日志持久化
与内存快照保存当前内存中的数据所不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。即每执行一个命令,就会把该命令写到日志文件里。
需要注意的是写日志的操作在Redis执行命令将数据写入内存之后,如下图所示:
这样做的好处就是不会阻塞当前操作,也可以避免额外的检查开销,如果是在命令执行前进行写日志的操作,一旦命令语法是错误的,不进行检查的话就会导致写入到日志文件中的命令是错误的,在使用日志文件恢复数据的时候就会出错。而在命令执行后在进行日志的写入则不会有这个问题
AOF两个问题:
- AOF 虽然避免了对当前命令的阻塞,但却可能会给下一个操作带来阻塞风险。因为,AOF 日志是在主进程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了
如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了
AOF 缓冲区
针对上面两个问题,Redis提供了缓冲区的方式进行AOF日志的记录,以达到尽可能的避免阻塞和数据丢失的问题。
即Redis在执行完命令进行持久化的时候,并非直接写入磁盘日志文件,而是先写入AOF缓冲区内,之后再通过某种策略写到磁盘
关于文件重写的流程,有两点需要特别注意:重写由父进程fork子进程进行;
- 重写期间Redis执行的写命令,需要追加到新的AOF文件中,为此Redis引入了aof_rewrite_buf缓存。
上图,文件重写的流程如下:
- Redis父进程首先判断当前是否存在正在执行 bgsave/bgrewriteaof的子进程,如果存在则bgrewriteaof命令直接返回,如果存在bgsave命令则等bgsave执行完成后再执行。前面曾介绍过,这个主要是基于性能方面的考虑。
- 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的。
- 父进程fork后,bgrewriteaof命令返回”Background append only file rewrite started”信息并不再阻塞父进程,并可以响应其他命令。Redis的所有写命令依然写入AOF缓冲区,并根据appendfsync策略同步到硬盘,保证原有AOF机制的正确。
- 由于fork操作使用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然在响应命令,因此Redis使用AOF重写缓冲区(图中的aof_rewrite_buf)保存这部分数据,防止新AOF文件生成期间丢失这部分数据。也就是说,bgrewriteaof执行期间,Redis的写命令同时追加到aof_buf和aof_rewirte_buf两个缓冲区。
- 子进程根据内存快照,按照命令合并规则写入到新的AOF文件。
子进程写完新的AOF文件后,向父进程发信号,父进程更新统计信息,具体可以通过info persistence查看。
Always(同步写回): 命令写入 AOF缓冲区后调用系统 fsync操作同步到AOF文件, fsync完成后线程返回
- Everysec(每秒写回): 命令写人 AOF缓冲区后调用系统 write操作, write完成后线程返回。fsync同步文件操作由专门线程每秒调用一次
- No(操作系统自动写回): 命令写入 AOF缓冲区后调用系统 write操作,不对AOF文件做 fsync同步,同步硬盘操作由操作系统负责,通常同步周期最长30秒
三种回写策略问题:
- 配置为 always时,每次写入都要同步AOF文件,硬盘的写入速度无法与内存相提并论,显然与 Redis髙性能特性背道而驰
- 配置为no,由于操作系统每次同步AOF文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但数据安全性无法保证。
- 配置为 everysec,是建议的同步策略,也是默认配置,虽然能做到兼顾性能和数据安全性。但极端情况下一会造成1秒内的数据丢失。
在真正使用中,我们可以根据具体对性能和数据完整性的要求,分析这三种回写策略,选择适合的策略来进行持久化。
回写策略 | 优点 | 缺点 |
---|---|---|
Always(同步写回) | 可靠性高、数据基本不丢失 | 性能较差 |
Everysec(每秒写回) | 性能适中 | 宕机时丢失1秒内的数据 |
No(操作系统自动写回) | 性能好 | 宕机时丢失数据较多 |
AOF重写机制
问题:AOF日志有一个问题,当日志文件越来越大的时候,怎么处理?
因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大,过大的AOF文件不仅追加命令会变慢,而且可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多
这个时候就要用到AOF重写机制了
redis> set testKey testValue
OK
redis> set testKey testValue1
OK
redis> del testKey
OK
redis> set testKey hello
OK
redis> set testKey world
OK
- AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令
- 如上示例,我们执行完命令后,Redis会在AOF里面追加5条命令。但实际上只需要set testKey world一条命令就够了
- AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入
- 比如说,当读取了键值对“testkey”: “world”之后,重写机制会记录 set testkey world这条命令。这样,当需要恢复时,可以重新执行该命令,实现“testkey”: “world”的写入。
这样,重写后的日志,从5条变成了1条,而对于可能被修改过成百上千次的键值对来说,重写能节省的空间就更大了。
虽然 AOF重写后,日志文件会缩小,但是,要把整个数据库的最新数据的操作日志都写回磁盘,仍然是一个非常耗时的过程。这时,我们不得不关注:重写会不会导致阻塞?这就要看看AOF重写的过程是怎么样的
文件重写触发方式:
- 手动触发:直接调用bgrewriteaof命令,该命令的执行与bgsave有些类似:都是fork子进程进行具体的工作,且都只有在fork时阻塞。
自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数,以及aof_current_size和aof_base_size状态确定触发时机。
因为AOF重写也是一个非常耗时的过程,又因为Redis单线程的特性,同内存快照一样,AOF重写的过程也是由父进程fork出bgrewriteaof子进程来完成的.
- 使用子进程(而不是开启一个线程)进行AOF重写虽然可以避免使用锁的情况下,保证数据安全性,但是会带来子进程和父进程一致性问题。
- 例如在开始重写之后父进程又接收了新的键值对此时子进程是无法知晓的,当子进程重写完成后的数据库和父进程的数据库状态是不一致的。
如下表:
时间 | 服务器进程(父进程) | 子进程 |
---|---|---|
T1 | 执行命令 SET K1 V1 | |
T2 | 执行命令 SET K1 V1 | |
T3 | 创建子进程,执行AOF文件重写 | 开始AOF重写 |
T4 | 执行命令 SET K2 V2 | 执行重写 |
T5 | 执行命令 SET K3 V3 | 执行重写 |
T6 | 执行命令 SET K4 V4 | 完成AOF重写 |
在T6时刻服务器进程有了4个键,而子进程却只有1个键
为了解决这种不一致性,Redis设置了一个AOF重写缓冲区。
在子进程执行AOF重写期间。服务器进程需要执行以下3个动作:
- 执行客户端命令
- 执行后追加到AOF缓冲区
- 执行后追加到AOF重写缓冲区
子进程完成AOF重写后,它向父进程发送一个信号,父进程收到信号后会调用一个信号处理函数,该函数把AOF重写缓冲区的命令追加到新AOF文件中然后替换掉现有AOF文件。父进程处理完毕后可以继续接受客户端命令调用,可以看出在AOF后台重写过程中只有这个信号处理函数会阻塞服务器进程。下表是完整的AOF后台重写过程:
时间 | 服务器进程(父进程) | 子进程 |
---|---|---|
T1 | 执行命令 SET K1 V1 | |
T2 | 执行命令 SET K1 V1 | |
T3 | 创建子进程,执行AOF文件重写 | 开始AOF重写 |
T4 | 执行命令 SET K2 V2 | 执行重写 |
T5 | 执行命令 SET K3 V3 | 执行重写 |
T6 | 执行命令 SET K4 V4 | 完成AOF重写,向父进程发送信号 |
T7 | 接收到信号,将T5 T6 T7 服务器的写命令追加到新的AOF文件末尾 | |
T8 | 用新的AOF替换旧的AOF |
这样就可以保证重写日志期间的所有操作也都会写入新的AOF文件。
需要注意的是, T7 T8执行的任务会阻塞服务器处理命令。
总的来说,就是每次 AOF 重写时,Redis 会先fork出一个子进程用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。
AOF文件恢复
在Redis 服务器重启后,会优先去载入AOF日志文件。因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。
而由于Redis命令只能在客户端上下文中执行,Redis会创建一个没有网络连接的伪客户端来执行AOF文件中的内容。
AOF 文件出错
服务器可能在程序正在对 AOF 文件进行写入时崩溃(这个不应该破坏数据的一致性), Redis不会装载已破坏的AOF文件。当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:
- 为现有的 AOF 文件创建一个备份。
- 使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复。
- $ redis-check-aof —fix
- (可选)使用 diff -u 对比修复后的 AOF 文件和原始 AOF 文件的备份,查看两个文件之间的不同之处。
重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复。
AOF 优缺点
优点:
数据保证:我们可以根据需要设置合适的回写策略,来保障数据尽可能少的丢失
- 易于解析:相对于RDB文件,AOF文件更易于理解和解析,且没有兼容性问题。
缺点:
- 性能相对较差:它的操作模式决定了它会对redis的性能有所损耗
- 体积相对更大:尽管是将aof文件重写了,但是毕竟是操作过程和操作结果仍然有很大的差别,体积也毋庸置疑的更大。
恢复速度较慢:由于恢复的时候要逐条解析命令并写入,相对于RDB文件的恢复比较慢。
AOF 常用配置总结
下面是AOF常用的配置项,以及默认值;前面介绍过的这里不再详细介绍。
appendonly no:是否开启AOF
- appendfilename “appendonly.aof”:AOF文件名
- dir ./:RDB文件和AOF文件所在目录
- appendfsync everysec:fsync持久化策略
- no-appendfsync-on-rewrite no:AOF重写期间是否禁止fsync;如果开启该选项,可以减轻文件重写时CPU和硬盘的负载(尤其是硬盘),但是可能会丢失AOF重写期间的数据;需要在负载和安全性之间进行平衡
- auto-aof-rewrite-percentage 100:文件重写触发条件之一
- auto-aof-rewrite-min-size 64mb:文件重写触发提交之一
- aof-load-truncated yes:如果AOF文件结尾损坏,Redis启动时是否仍载入AOF文件
混合持久化【Redis4.0】
仅使用RDB快照方式恢复数据,由于快照时间粒度较大时,会丢失大量数据。
仅使用AOF重放方式恢复数据,日志性能相对 rdb 来说要慢。在 Redis 实例很大的情况下,启动需要花费很长的时间。
为了解决这个问题,Redis4.0开始支持RDB和AOF的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。RDB 文件的内容和增量的 AOF 日志文件存在一起,这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小
- 大量数据使用粗粒度(时间上)的rdb快照方式,性能高,恢复时间快。
- 增量数据使用细粒度(时间上)的AOF日志方式,尽量保证数据的不丢失。
在Redis重启时,进行AOF重写的时候就直接把RDB的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB和 AOF 的优点,快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是AOF 格式,可读性较差。
另外,可以使用下面这种方式:
Master使用AOF,Slave使用RDB快照,master需要首先确保数据完整性,它作为数据备份的第一选择;slave提供只读服务或仅作为备机,它的主要目的就是快速响应客户端read请求或灾切换。
Redis选择持久化方式
了解,RDB和AOF持久化,但是实际使用中,我们并不会只使用一种持久化方式,为保证数据尽可能少的丢失,一般会采用混合两种持久化方式结合的方案,保证数据的高可用
持久化策略选择
首先要知道无论是RDB还是AOF,持久化的开启都是要付出性能方面代价的:
- 对于RDB持久化,一方面是bgsave在进行fork操作时Redis主进程会阻塞,另一方面,子进程向硬盘写数据也会带来IO压力;
- 对于AOF持久化,向硬盘写数据的频率大大提高(everysec策略下为秒级),IO压力更大,甚至可能造成AOF追加阻塞问题,此外,AOF文件的重写与RDB的bgsave类似,会有fork时的阻塞和子进程的IO压力问题。相对来说,由于AOF向硬盘中写数据的频率更高,因此AOF持久化对Redis主进程性能的影响会更大
在实际生产环境中,根据数据量、应用对数据的安全要求、预算限制等不同情况,会有各种各样的持久化策略;如完全不使用任何持久化、使用RDB或AOF的一种,或同时开启RDB和AOF持久化等。此外,持久化的选择必须与Redis的主从策略一起考虑,因为主从复制与持久化同样具有数据备份的功能,而且主机master和从机slave可以独立的选择持久化方案
在选择持久化策略的时候,需要根据实际业务场景,进行持久化策略方案设计:
- 如果Redis中的数据完全丢弃也没有关系(如Redis完全用作DB层数据的cache),那么无论是单机,还是主从架构,都可以不进行任何持久化。
- 在单机环境下(对于个人开发者,这种情况可能比较常见),如果可以接受十几分钟或更多的数据丢失,选择RDB对Redis的性能更加有利;如果只能接受秒级别的数据丢失,应该选择AOF。
- 但在多数情况下,我们都会配置主从环境,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之前,便被自动拉起机制重启了。因此,应尽量避免“自动拉起机制”和“不做持久化”同时出现。
- 在这种情况下,一种可行的做法是:
异地灾备:上述讨论的几种持久化策略,针对的都是一般的系统故障,如进程异常退出、宕机、断电等,这些故障不会损坏硬盘。但是对于一些可能导致硬盘损坏的灾难情况,如火灾地震,就需要进行异地灾备。例如对于单机的情形,可以定时将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追加阻塞问题定位的方法:
- 监控infoPersistence中的aof_delayed_fsync:当AOF追加阻塞发生时(即主线程等待fsync而阻塞),该指标累加。
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.
如果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 statslatest_fork_usec:代表上次fork耗时
混合持久化方案
在Redis 4.0之后提供了混合持久化的方式,顾名思义就是把RDB持久化和AOF持久化结合起来的一种方式。
混合持久化就是快照以一定的频率执行,而在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
如图所示,在第一次执行快照之后,将后续命令写入AOF文件,直到第二次执行快照。而在第二次执行快照的时候会清除AOF文件的内容,循环往复。
这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作,也就不会出现文件过大的情况,同时可以避免AOF重写的开销。
但世界上并没有完全两全其美的事情,即使鱼和熊掌兼得,一起吃的时候也容易串了味儿。RDB混合持久化固然兼顾了性能与数据完整性,但也有其缺点。
- 兼顾了性能与数据的同时也牺牲了部分性能
- AOF 文件中添加了RDB 格式的内容,使可读性变差,并且由于混合了RDB的内容,与RDB文件相同具有兼容性的问题