共计 5725 个字符,预计需要花费 15 分钟才能阅读完成。
AOF 日志
试想一下,如果 Redis 每执行一条写操作命令,就把该命令以追加的形式写入到一个文件里,而后重启 Redis 的时候,先去读取这个文件里的命令,并且执行它,这不就相当于复原了缓存数据了吗?
这种保留写操作命令到日志的长久化形式,就是 Redis 里的 AOF(Append Only File) 长久化性能,留神只会记录写操作命令,读操作命令是不会被记录的,因为没意义。
在 Redis 中 AOF 长久化性能默认是不开启的,须要咱们批改 redis.conf
配置文件中的以下参数:
AOF 日志文件其实就是一般的文本,咱们能够通过 cat
命令查看外面的内容,不过外面的内容如果不晓得肯定的规定的话,可能会看不懂。
我这里以「set name xiaolin」命令作为例子,Redis 执行了这条命令后,记录在 AOF 日志里的内容如下图:
我这里给大家解释下。
「*3
」示意以后命令有三个局部,每局部都是以「$+ 数字
」结尾,前面紧跟着具体的命令、键或值。而后,这里的「 数字
」示意这部分中的命令、键或值一共有多少字节。例如,「$3 set
」示意这部分有 3 个字节,也就是「set
」命令这个字符串的长度。
不晓得大家留神到没有,Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,这么做其实有两个益处。
第一个益处,防止额定的查看开销。
因为如果先将写操作命令记录到 AOF 日志里,再执行该命令的话,如果以后的命令语法有问题,那么如果不进行命令语法查看,该谬误的命令记录到 AOF 日志里后,Redis 在应用日志复原数据时,就可能会出错。
而如果先执行写操作命令再记录日志的话,只有在该命令执行胜利后,才将命令记录到 AOF 日志里,这样就不必额定的查看开销,保障记录在 AOF 日志里的命令都是可执行并且正确的。
第二个益处,不会阻塞以后写操作命令的执行,因为当写操作命令执行胜利后,才会将命令记录到 AOF 日志。
当然,AOF 长久化性能也不是没有潜在危险。
第一个危险,执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器产生宕机了,这个数据就会有 失落的危险。
第二个危险,后面说道,因为写操作命令执行胜利后才记录到 AOF 日志,所以不会阻塞以后写操作命令的执行,然而 可能会给「下一个」命令带来阻塞危险。
因为将命令写入到日志的这个操作也是在主过程实现的(执行命令也是在主过程),也就是说这两个操作是同步的。
如果在将日志内容写入到硬盘时,服务器的硬盘的 I/O 压力太大,就会导致写硬盘的速度很慢,进而阻塞住了,也就会导致后续的命令无奈执行。
认真剖析一下,其实这两个危险都有一个共性,都跟「AOF 日志写回硬盘的机会」无关。
三种写回策略
Redis 写入 AOF 日志的过程,如下图:
我先来具体说说:
- Redis 执行完写操作命令后,会将命令追加到
server.aof_buf
缓冲区; - 而后通过 write() 零碎调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,期待内核将数据写入硬盘;
- 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。
Redis 提供了 3 种写回硬盘的策略,管制的就是下面说的第三步的过程。
在 redis.conf
配置文件中的 appendfsync
配置项能够有以下 3 种参数可填:
- Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
- Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,而后每隔一秒将缓冲区里的内容写回到硬盘;
- No,意味着不禁 Redis 管制写回硬盘的机会,转交给操作系统管制写回的机会,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
这 3 种写回策略都无奈能完满解决「主过程阻塞」和「缩小数据失落」的问题,因为两个问题是对抗的,偏差于一边的话,就会要就义另外一边,起因如下:
- Always 策略的话,能够最大水平保证数据不失落,然而因为它每执行一条写操作命令就同步将 AOF 内容写回硬盘,所以是不可避免会影响主过程的性能;
- No 策略的话,是交由操作系统来决定何时将 AOF 日志内容写回硬盘,相比于 Always 策略性能较好,然而操作系统写回硬盘的机会是不可预知的,如果 AOF 日志内容没有写回硬盘,一旦服务器宕机,就会失落不定数量的数据。
- Everysec 策略的话,是折中的一种形式,防止了 Always 策略的性能开销,也比 No 策略更能防止数据失落,当然如果上一秒的写操作命令日志没有写回到硬盘,产生了宕机,这一秒内的数据天然也会失落。
大家依据本人的业务场景进行抉择:
- 如果要高性能,就抉择 No 策略;
- 如果要高牢靠,就抉择 Always 策略;
- 如果容许数据失落一点,但又想性能高,就抉择 Everysec 策略。
我也把这 3 个写回策略的优缺点总结成了一张表格:
大家晓得这三种策略是怎么实现的吗?
深刻到源码后,你就会发现这三种策略只是在管制 fsync()
函数的调用机会。
当应用程序向文件写入数据时,内核通常先将数据复制到内核缓冲区中,而后排入队列,而后由内核决定何时写入硬盘。
如果想要应用程序向文件写入数据后,能立马将数据同步到硬盘,就能够调用 fsync()
函数,这样内核就会将内核缓冲区的数据间接写入到硬盘,等到硬盘写操作实现后,该函数才会返回。
- Always 策略就是每次写入 AOF 文件数据后,就执行 fsync() 函数;
- Everysec 策略就会创立一个异步工作来执行 fsync() 函数;
- No 策略就是永不执行 fsync() 函数;
AOF 重写机制
AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大。
如果当 AOF 日志文件过大就会带来性能问题,比方重启 Redis 后,须要读 AOF 文件的内容以复原数据,如果文件过大,整个复原的过程就会很慢。
所以,Redis 为了防止 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
AOF 重写机制是在重写时,读取以后数据库中的所有键值对,而后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全副记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
举个例子,在没有应用重写机制前,假如前后执行了「set name xiaolin」和「set name xiaolincoding」这两个命令的话,就会将这两个命令记录到 AOF 文件。
然而 在应用重写机制后,就会读取 name 最新的 value(键值对),而后用一条「set name xiaolincoding」命令记录到新的 AOF 文件,之前的第一个命令就没有必要记录了,因为它属于「历史」命令,没有作用了。这样一来,一个键值对在重写日志中只用一条命令就行了。
重写工作实现后,就会将新的 AOF 文件笼罩现有的 AOF 文件,这就相当于压缩了 AOF 文件,使得 AOF 文件体积变小了。
而后,在通过 AOF 日志复原数据时,只用执行这条命令,就能够间接实现这个键值对的写入了。
所以,重写机制的妙处在于,只管某个键值对被多条写命令重复批改,最终也只须要依据这个「键值对」以后的最新状态,而后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这样就缩小了 AOF 文件中的命令数量。最初在重写工作实现后,将新的 AOF 文件笼罩现有的 AOF 文件。
这里说一下为什么重写 AOF 的时候,不间接复用现有的 AOF 文件,而是先写到新的 AOF 文件再笼罩过来。
因为 如果 AOF 重写过程中失败了,现有的 AOF 文件就会造成净化,可能无奈用于复原应用。
所以 AOF 重写过程,先重写到新的 AOF 文件,重写失败的话,就间接删除这个文件就好,不会对现有的 AOF 文件造成影响。
AOF 后盾重写
写入 AOF 日志的操作尽管是在主过程实现的,因为它写入的内容不多,所以个别不太影响命令的操作。
然而在触发 AOF 重写时,比方当 AOF 文件大于 64M 时,就会对 AOF 文件进行重写,这时是须要读取所有缓存的键值对数据,并为每个键值对生成一条命令,而后将其写入到新的 AOF 文件,重写完后,就把当初的 AOF 文件替换掉。
这个过程其实是很耗时的,所以重写的操作不能放在主过程里。
所以,Redis 的 重写 AOF 过程是由后台子过程 bgrewriteaof 来实现的,这么做能够达到两个益处:
- 子过程进行 AOF 重写期间,主过程能够持续解决命令申请,从而防止阻塞主过程;
- 子过程带有主过程的数据正本(数据正本怎么产生的前面会说),这里应用子过程而不是线程,因为如果是应用线程,多线程之间会共享内存,那么在批改共享内存数据的时候,须要通过加锁来保证数据的平安,而这样就会升高性能。而应用子过程,创立子过程时,父子过程是共享内存数据的,不过这个共享的内存只能以只读的形式,而当父子过程任意一方批改了该共享内存,就会产生「写时复制」,于是父子过程就有了独立的数据正本,就不必加锁来保障数据安全。
子过程是怎么领有主过程一样的数据正本的呢?
主过程在通过 fork
零碎调用生成 bgrewriteaof 子过程时,操作系统会把主过程的「页表」复制一份给子过程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。
这样一来,子过程就共享了父过程的物理内存数据了,这样可能 节约物理内存资源 ,页表对应的页表项的属性会标记该物理内存的权限为 只读。
不过,当父过程或者子过程在向这个内存发动写操作时,CPU 就会触发 缺页中断 ,这个缺页中断是因为违反权限导致的,而后操作系统会在「缺页异样处理函数」里进行 物理内存的复制 ,并从新设置其内存映射关系,将父子过程的内存读写权限设置为 可读写 ,最初才会对内存进行写操作,这个过程被称为「 写时复制(Copy On Write)」。
写时复制顾名思义,在产生写操作的时候,操作系统才会去复制物理内存,这样是为了避免 fork 创立子过程时,因为物理内存数据的复制工夫过长而导致父过程长时间阻塞的问题。
当然,操作系统复制父过程页表的时候,父过程也是阻塞中的,不过页表的大小相比理论的物理内存小很多,所以通常复制页表的过程是比拟快的。
不过,如果父过程的内存数据十分大,那天然页表也会很大,这时父过程在通过 fork 创立子过程的时候,阻塞的工夫也越久。
所以,有两个阶段会导致阻塞父过程:
- 创立子过程的途中,因为要复制父过程的页表等数据结构,阻塞的工夫跟页表的大小无关,页表越大,阻塞的工夫也越长;
- 创立完子过程后,如果子过程或者父过程批改了共享数据,就会产生写时复制,这期间会拷贝物理内存,如果内存越大,天然阻塞的工夫也越长;
触发重写机制后,主过程就会创立重写 AOF 的子过程,此时父子过程共享物理内存,重写子过程只会对这个内存进行只读,重写 AOF 子过程会读取数据库里的所有数据,并逐个把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。
然而子过程重写过程中,主过程仍然能够失常解决命令。
如果此时 主过程批改了曾经存在 key-value,就会产生写时复制,留神这里只会复制主过程批改的物理内存数据,没批改物理内存还是与子过程共享的。
所以如果这个阶段批改的是一个 bigkey,也就是数据量比拟大的 key-value 的时候,这时复制的物理内存数据的过程就会比拟耗时,有阻塞主过程的危险。
还有个问题,重写 AOF 日志过程中,如果主过程批改了曾经存在 key-value,此时这个 key-value 数据在子过程的内存数据就跟主过程的内存数据不统一了,这时要怎么办呢?
为了解决这种数据不统一问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创立 bgrewriteaof 子过程之后开始应用。
在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会 同时将这个写命令写入到「AOF 缓冲区」和「AOF 重写缓冲区」。
也就是说,在 bgrewriteaof 子过程执行 AOF 重写期间,主过程须要执行以下三个工作:
- 执行客户端发来的命令;
- 将执行后的写命令追加到「AOF 缓冲区」;
- 将执行后的写命令追加到「AOF 重写缓冲区」;
当子过程实现 AOF 重写工作(扫描数据库中所有数据,逐个把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主过程发送一条信号,信号是过程间通信的一种形式,且是异步的。
主过程收到该信号后,会调用一个信号处理函数,该函数次要做以下工作:
- 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保留的数据库状态统一;
- 新的 AOF 的文件进行改名,笼罩现有的 AOF 文件。
信号函数执行完后,主过程就能够持续像平常一样解决命令了。
在整个 AOF 后盾重写过程中,除了产生写时复制会对主过程造成阻塞,还有信号处理函数执行时也会对主过程造成阻塞,在其余时候,AOF 后盾重写都不会阻塞主过程。
总结
这次小林给大家介绍了 Redis 长久化技术中的 AOF 办法,这个办法是每执行一条写操作命令,就将该命令以追加的形式写入到 AOF 文件,而后在复原时,以逐个执行命令的形式来进行数据恢复。
Redis 提供了三种将 AOF 日志写回硬盘的策略,别离是 Always、Everysec 和 No,这三种策略在可靠性上是从高到低,而在性能上则是从低到高。
随着执行的命令越多,AOF 文件的体积天然也会越来越大,为了防止日志文件过大,Redis 提供了 AOF 重写机制,它会间接扫描数据中所有的键值对数据,而后为每一个键值对生成一条写操作命令,接着将该命令写入到新的 AOF 文件,重写实现后,就替换掉现有的 AOF 日志。重写的过程是由后台子过程实现的,这样能够使得主过程能够持续失常解决命令。
用 AOF 日志的形式来复原数据其实是很慢的,因为 Redis 执行命令由单线程负责的,而 AOF 日志复原数据的形式是程序执行日志里的每一条命令,如果 AOF 日志很大,这个「重放」的过程就会很慢了。
参考资料
- 《Redis 设计与实现》
- 《Redis 核心技术与实战 - 极客工夫》
- 《Redis 源码剖析》