Redis是一个内存数据库,当机器重启之后内存中的数据都会被丢失。所以持久化的显得尤其重要。Redis持久化的方式有两种
RDB(Redis DataBase):保存某个时间点之前的数据。
AOF(Append-only file):保存Redis服务器端执行的每一条命令。
RDB
RDB也会被说成是内存快照,就是把某一时刻的状态以文件的形式写在磁盘上,也就是快照。即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。在数据恢复时,可以直接把RDB文件读入内存,很快的完成恢复。
但,需要考虑两个关键问题:
对哪些数据做快照?关系到快照的执行效率问题。
在做快照时,数据还能被增删改吗?关系到Redis是否被阻塞,能否正常处理请求。
对哪些文件做快照?
Redis的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照。也就是说,把内存中的所有数据都记录到磁盘中。但是,我们知道全量的数据越多,RDB文件就越大,往磁盘上写数据的时间开销也越大。而对于Redis来说,它的单线程模型就决定了,要尽量避免所有会阻塞主线程的操作。因此,会提出另外一个问题:RDB文件的生成会阻塞主线程吗?
Redis提供了两个命令来生成RDB文件:分别是save和bgsave
save:在主线程中执行,会导致阻塞。
bgsave:创建一个子进程,专门用于写入RDB文件,避免了主线程的阻塞,也是Redis RDB文件生成的默认配置。
可以通过bgsave命令来执行全量快照,这既提供了数据的可靠性保证,也避免了对Redis性能的影响。
触发时机
除了上述sabe和bgsave命令之外,Redis内部还存在自动触发RDB的持久化机制,有以下几个场景:
1)使用save配置,如“save m n ”:表示m秒内数据集存在n次修改时,自动触发bgsave。默认的配置有
save 900 1 //15分钟内有1个修改
save 300 10 //5分钟内有10个修改
save 60 10000 //1分钟内有1W修改
2)如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
3)执行debug reload命令重新加载Redis时,也会自动触发save操作。
4)默认情况下执行shutdown命令,如果没有开启AOF持久化功能,也会自动执行bgsave。
快照时能修改数据吗?
如果快照执行期间,数据不能修改是会有问题的。假设1GB的数据做快照需要5s,如果这段时间不能操作这些数据,Redis就不能处理这些数据的写操作,无疑给业务服务造成巨大的影响。或许会说,可以用bgsave避免阻塞。但避免阻塞和正常处理写操作不是一回事。主线程虽然没有阻塞,可以正常接收请求,但是为了保证快照完整性,只能处理读操作,因为不能修改正在执行的快照的数据。
为了快照而暂停写操作,肯定是不能接收的。因此,Redis会借助操作系统提供的写时复制技术(Copy-On-Write,COW),在执行快照的时候,可以正常处理写操作。简单来说就是,bgsave子进程是由主线程fork生成的,可以共享主线程的所有内存数据。bgsave子进程运行后,开始读取主线程的内存数据,并把它们写入RDB文件。
这样既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。
多久做一次快照?
我们知道,快照的间隔时间越短,即使是某一时刻发生宕机了,因为上一时刻快照刚执行,丢失的数据也不会太多。但是,如果频繁的执行全量快照,也会带来两方面的开销。
1)频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
2)bgsave子进程需要通过fork操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是fork这个创建过程本身会阻塞主线程、而且主线程的内存越大,阻塞时间越长。如果频繁fork子进程,会频繁阻塞主线程。
AOF
AOF日志如何实现?
Redis先执行命令,把数据写入内存,然后才记录日志。传统数据库的日志,比如redo log(重做日志),记录的是修改后的数据,而AOF记录的是Redis收到的每一条命令,这些命令以文本形式保存。为了避免额外的检查开销,Redis在向AOF里面记录日志的时候,并不会先对这些命令进行语法检查。后写日志的方式,让Redis先执行命令,只有命令执行成功,才会被记录到日志中,否则系统会直接向客户端报错。
所以,AOF日志可以避免出现记录错误命令的情况。另外一个好处,它是在命令执行后才记录日志,所以不会阻塞当前的写操作。
但是,AOF也有两个潜在问题。
1)如果刚执行完一个命令,还没来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果Redis用于缓存,还可以从数据库重新恢复。但如果直接用Redis作为数据库,因为命令没有记入日志,所以无法用日志进行恢复。
2)AOF虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞的风险。因为AOF也是在主线程中执行的,如果把日志文件写入磁盘是,磁盘写压力大,就会导致写磁盘很慢,进而导致后续的操作也无法执行。
三种写回策略
针对上述问题,AOF机制提供了三个选择,也就是AOF配置项appendfsync的三个可选值。
Always,同步写回:每个写命令执行完,立马将日志写回磁盘。
Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区的内容写入磁盘。
No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
针对避免主线程阻塞和减少数据丢失问题,三种策略都无法做到两全其美。
Always可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能。
No 在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不再Redis手里,只要AOF记录没有写回磁盘,一旦宕机对应的数据就会丢失。
Everysec 避免了Always的性能开销,虽然较少了对系统性能的影响,但如果发生宕机,上一秒未落盘的命令仍然会丢失,只能在避免影响主线程性能和避免数据丢失两者之间取个折中。
配置项 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 可靠性高、数据基本不丢失 | 每个写命令都要落盘,性能影响较大 |
Everysec | 每秒写回 | 性能适中 | 宕机时丢失1秒内的数据 |
No | 操作系统控制写回 | 性能好 | 宕机时丢失数据较多 |
AOF重写
随着接收到的写命令越来越多,AOF文件会越来越大,也就意味着,要小心AOF文件过大带来的性能问题。主要有以下三个方面:
文件系统本身对文件大小有限制,无法保存过大的文件。
如果文件过大,追加命令记录,效率也会变低。
如果发生宕机,AOF中的记录的命令要一个个被重新执行,用于故障恢复,如果日志文件过大,整个恢复过程可能会很缓慢,会影响到Redis的正常使用。
因此,需要AOF重写机制。简单来说,AOF重写机制就是在重写时,Redis根据数据库的现状创建一个新的AOF文件。也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。
为什么重写机制可以把文件变小呢?因为旧日志中的多条命令,在重写后的新日志中变成了一条命令。我们知道AOF文件是以追加的方式,逐一记录接收到的写命令的,当一个键值对被多条写命令反复修改时,AOF文件会记录相应的多条命令。但是在重写时,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样,一个键值对在重写日志中只用一条命令就行了。
AOF重写会阻塞吗?
虽然,AOF重写后,日志文件会缩小,但是,要把整个数据库的最新数据的操作日志都写回磁盘,仍然是一个非常耗时的过程。重写是否会阻塞主线程?
和AOF日志由主线程写回不同,重写过程是由子进程bgrewriteaof来完成,也是为了避免阻塞主线程,防止数据库性能下降。
重写的过程可以总结为:一个拷贝,两处日志。
一个拷贝是指,每次执行重写时,主线程fork出后台bgrewriteaof子进程。此时,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里包含数据库的最新数据。然后bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据携程操作,记入重写日志。
第一处日志,因为主线程未阻塞,仍然可以处理新的操作。如果有写操作,则记录到AOF日志,Redis会把这个操作写到它的缓冲区。这样即使宕机,AOF日志仍然齐全,可以用于恢复。
第二处日志,指新的AOF重写日志。这个操作也会被写到重写日志的缓冲区(与AOF日志缓存区不是同一个)。这样,重写日志也不会丢失最新的操作。等到拷贝的左右操作记录重写完成后,重写日志的这些最新操作也会写入新的AOF文件,以保证数据库最新状态的记录。此时,就可以用新的AOF文件替代旧文件了。
总的来说,每次AOF重写时,Redis会先执行一个内存拷贝,用于重写。然后使用两个日志保证在重写过程中,新写入的数据不会丢失。而且因为Redis采用额外的线程进行数据重写,这个过程不会阻塞主线程。
AOF重写触发时机
手动触发:可以通过命令bgrewriteaof命令,进行对AOF重写。
自动触发:根据配置参数
auto-aof-rewrite-min-size:表示运行AOF重写时文件最小。默认64MB
auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。默认100。
自动触发时机需要两个参数结合:(默认配置下)AOF文件超过64MB,且当前AOF文件大小比上一次重写AOF文件(如果没有重写,则是启动时AOF的大小)大小,至少大一倍时,进行重写。
公式:aof_current_size > auto-aof-rewrite-min-size && (aof_current_size -aof_base_size ) / aof_base_size >= auto-aof-rewrite-percentage
混合持久化
重启Redis时,很少使用RDB来恢复内存状态,因为会丢失大量数据。通常使用AOF日志重放,但重放AOF日志性能相对于RDB来说要慢很多,在Redis实例很大的情况下,启动需要花费很长的时间。
如果RDB可以做增量快照,就可以避免每次全量快照的开销。增量快照是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录。
Redis4.0提出了混合使用AOF日志和内存快照的方法。内存快照以一定的频率执行,在两次快照之间,还用AOF日志(稍后介绍)记录着期间的所有命令操作。这样,快照不用很频繁地执行,就避免了频繁fork对主线程的影响。AOF日志也只用记录两次快照间的操作,不需要记录所有操作,因此也不会出现文件过大的情况,也可以避免重写开销了。
在Redis重启时,可以先加载RDB的内容,然后再重放增量AOF日志就可以完全替代之前AOF全量文件重放,重启效率的到提升。
RDB和AOF选择
1)数据不能丢失时,RDB和AOF的缓和使用是一个好的选择。
2)如果允许分钟级别的数据丢失,可以只使用RDB。
3)如果只用AOF,优先使用everysec的配置选项,因为它是可靠性和性能之间的一个平衡。
RDB优缺点
优点 | 缺点 |
---|---|
1. RDB是一个紧凑压缩的二进制文件,非常适用于备份,全量复制。2. Redis加载RDB恢复数据远快于AOF的方式。 | 1. 没办法做到实时持久化/秒级持久化。因为bgsave的fork操作属于重量级操作,频繁执行成本过高; |
AOF优缺点
正好和RDB相反。
附录
fork操作
不论是RDB还是AOF,都会采用fork操作。这里简单阐述一下fork操作的原理。
Redis调用fork函数这瞬间肯定是会阻塞主线程的。当进程调用fork后,当控制转移到内核中的fork代码后,内核会做4件事情。
1)分配新的内存块和内核数据结构给子进程。
2)将父进程部分数据结构内容拷贝子进程。
3)添加子进程到系统进程列表。
4)fork返回后,开始调度器调度。
因此可以知道,fork操作不会一次性拷贝所有内存数据给子进程。
fork采用操作系统提供的写时复制机制,就是为了避免一次性拷贝大量内存数据给子进程造成长时间阻塞问题,但是会拷贝进程必要的数据结构,其中一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表)。这个拷贝过程会消耗大量CPU资源,拷贝完整之前进程是阻塞的,阻塞时间取决于整个内存大小,实例越大,内存页表越大,fork阻塞时间越长。
拷贝完内存页表后,子进程与父进程指向相同的内存地址空间。也就是说,此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。
写时复制原理:父进程与子进程共享页帧。只要页帧被共享,它们就不能被修改,即页帧被保护。无论父进程还是子进程何时试图写一个共享的页帧,就产生一个异常,这是内核就把这个页复制到一个新的页帧中,并标记为可写,原来的页帧仍然是写保护的。当其他进程试图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。
Huge page
Linux kernel在2.6.38内核增加了Transparent Huge Pages(THP),支持huge page(2MB)的页分配,默认开启。Huge page 对提升TLB命中率比较友好,因为在相同内存容量下,使用huge page可以减少页表项,TLB就可以缓存更多的页表项,能较少TLB miss的开销。
对Redis来说,开启后,可以降低fork创建子进程的速度。但是,执行fork之后,复制页单位从原来的4KB变成了2MB,会大幅度加重写期间父进程内存消耗。
可以通过设置“echo never > /sys/kernel/mm/transparent_hugepage/enabled”来关闭THP。
参考资料
《Redis核心技术与实战》——极客时间
《Redis深度历险:核心原理与应用实践》
《Redis开发与运维》
fork写时拷贝实现原理 https://www.codenong.com/cs106630347/
fork函数底层实现原理 https://blog.csdn.net/qq_37964547/article/details/81482001