1. 前言
Redis是先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。
Redis是”写后”日志, 而大多数的数据库采用的是写前日志(WAL),例如Mysql,通过写前日志和两阶段提交,实现数据和逻辑的一致性。
AOF日志采用写后日志,即:先写内存,后写日志
为什么采用写后日志?
Redis要求高性能,采用后写日志有两方面的好处:
避免额外的检查开销:Redis向AOF里面记录日志的时候,并不会先去会这些命令进行语法检查,如果先记日志再执行命令,日志可能会记录错误的命令,在使用日志恢复数据的时候,就可能会报错。
不会阻塞当前的写操作
但是这种方式潜在的风险:
如果命令执行完成,写日志之前宕机了,会丢失数据
主线程写磁盘压力大,导致写磁盘满,阻塞后续操作
2. 如何实现AOF
AOF 日志记录Redis的每个写命令,步骤分为:命令追加append、文件写入write和文件同步sync
命令追加:当AOF持久功能打开后,服务器在执行完一个命令之后,会以协议格式将被执行的写命令追加到服务器的
aof_buf
缓冲区文件写入和同步:关于何时将
aof_buf
缓冲区的内容写入AOF文件中,redis提供了3中写回策略:配置项 写回时机 优点 缺点 Always 同步写回 可靠性高,基本不会丢失 每个写命令都要落盘,性能影响大 Everysec 每秒写回 性能适中 宕机时丢失1秒内的数据 No 操作系统控制的写回 性能好 宕机时丢失的数据较多 Always
: 同步写回:每个写命令执行完,立马同步地将日志写回磁盘Everysec
: 每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘No
: 操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
2.1 三种写回策略的优缺点
上面的三种写回策略体现了一个重要原则:trade-off取舍,在性能和可靠性之间做取舍
关于AOF的同步策略是涉及到操作系统的 write 函数和 fsync 函数的,在《Redis设计与实现》中是这样说明的:
为了提高文件写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区的空间被填满或超过了指定时限后,才真正将缓冲区的数据写入到磁盘里。
这样的操作虽然提高了效率,但也为数据写入带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失。为此,系统提供了fsync、fdatasync同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保写入数据的安全性。
3. redis.conf配置AOF
默认情况下,Redis是没有开启AOF的,可以通过配置redis.conf文件来开启AOF持久化,关于AOF的配置如下:
1 | # appendonly参数开启AOF持久化 |
以下是Redis中关于AOF的主要配置信息:
- appendonly:默认情况下AOF功能是关闭的,将该选项改为yes以便打开Redis的AOF功能。
- appendfilename:这个参数项很好理解了,就是AOF文件的名字。
- appendfsync:这个参数项是AOF功能最重要的设置项之一,主要用于设置“真正执行”操作命令向AOF文件中同步的策略。什么叫“真正执行”呢?还记得Linux操作系统对磁盘设备的操作方式吗? 为了保证操作系统中I/O队列的操作效率,应用程序提交的I/O操作请求一般是被放置在linux Page Cache中的,然后再由Linux操作系统中的策略自行决定正在写到磁盘上的时机。而Redis中有一个fsync()函数,可以将Page Cache中待写的数据真正写入到物理设备上,而缺点是频繁调用这个fsync()函数干预操作系统的既定策略,可能导致I/O卡顿的现象频繁 。与上节对应,appendfsync参数项可以设置三个值,分别是:always、everysec、no,默认的值为everysec。
- no-appendfsync-on-rewrite:always和everysec的设置会使真正的I/O操作高频度的出现,甚至会出现长时间的卡顿情况,这个问题出现在操作系统层面上,所有靠工作在操作系统之上的Redis是没法解决的。为了尽量缓解这个情况,Redis提供了这个设置项,保证在完成fsync函数调用时,不会将这段时间内发生的命令操作放入操作系统的Page Cache(这段时间Redis还在接受客户端的各种写操作命令)。
- auto-aof-rewrite-percentage:上文说到在生产环境下,技术人员不可能随时随地使用“BGREWRITEAOF”命令去重写AOF文件。所以更多时候我们需要依靠Redis中对AOF文件的自动重写策略。Redis中对触发自动重写AOF文件的操作提供了两个设置:auto-aof-rewrite-percentage表示如果当前AOF文件的大小超过了上次重写后AOF文件的百分之多少后,就再次开始重写AOF文件。例如该参数值的默认设置值为100,意思就是如果AOF文件的大小超过上次AOF文件重写后的1倍,就启动重写操作。
- auto-aof-rewrite-min-size:参考auto-aof-rewrite-percentage选项的介绍,auto-aof-rewrite-min-size设置项表示启动AOF文件重写操作的AOF文件最小大小。如果AOF文件大小低于这个值,则不会触发重写操作。注意,auto-aof-rewrite-percentage和auto-aof-rewrite-min-size只是用来控制Redis中自动对AOF文件进行重写的情况,如果是技术人员手动调用“BGREWRITEAOF”命令,则不受这两个限制条件左右。#
4. 深入理解AOF重写
AOF会记录每个写命令到AOF文件,随着时间越长,AOF文件会越来越大,如果不加以控制,会对Redis服务器,甚至操作系统造成影响,而且AOF文件越大,数据恢复越慢,
为了解决AOF文件体积膨胀的问题,Redis提供AOF文件重写机制来对AOF文件进行”瘦身”。
4.1. 图例
Redis通过创建一个新的AOF文件来替换现有的AOF,新旧两个AOF文件保存的数据相同,但新的文件没有冗余的命令
4.2. AOF重写会阻塞吗?
AOF重写过程是由后台进程bgrewriteaof
来完成的。 主线程fork出后台的bgrewriteaof
子进程,fork会把主线程的内存拷贝一份给bgrewriteaof
子进程,
这里面就包含来数据库的最新数据,然后,bgrewriteaof
子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
所以aof在重写是,在fork进程时会阻塞主线程的。
4.3. AOF日志何时会重写
有两个配置项控制AOF重写的触发:
auto-aof-rewrite-min-size
: 表示运行AOF重写时文件的最小大小,默认是64MBauto-aof-rewrite-percentage
: 这个值的计算方式是,当前aof文件大小和上一次重写aof文件的大小差值,再除以上一次重写后的aof文件大小,也就是当前aof文件比上一次重写aof文件的增量大小,和上一次重写后aof后文件大小的比值1
简单公式:(current - before) / before
4.4. 重写日志时,有新的数据写入
重写过程总结为:”一个拷贝,两次日志”。在fork出子进程时的拷贝,以及在重写时,如果有新的数据写入。主线程就会将命令记录到两个aof日志内存缓冲区中,如果AOF写回策略配置的时always
,则直接将命令写回旧的日志文件,并且保存一份命令至AOF重写缓冲区,这些操作对新的日志文件时不存在影响的。
旧日志文件:主线程使用的日志文件
新日志文件:
bgrewriteaof
进程使用的日志文件
而在bgrewriteaof
子进程完成日志重写操作后,会提示主线程,主线程会将AOF重写缓冲区中的命令追加到新的日志文件后面,这时在高并发的情况下,AOF重写缓冲区积累可能会很大,这样就会造成阻塞,
Redis后来通过Linux管道技术让aof重写期间就能同时进行回放,这样aof重写结束后只需回放少量剩余的数据即可。
最后通过修改文件名的方式,保证文件切换的原子性。
在AOF重写日志期间发生宕机的话,因为日志文件还没有切换,所以恢复数据时,用的还是旧的日志文件。
总结操作:
主线程fork出子进程重写aof日志
子进程重写日志完成后,主线程追加aof日志缓冲
替换日志文件
温馨提示:
这里的进程和线程的概念有点混乱。因为后台的bgreweiteaof进程就只有一个线程在操作,而主线程是Redis的操作进程,也是单独一个线程。这里想表达的是Redis主进程在fork出一个后台进程之后,后台进程的操作和主进程是没有任何关联的,也不会阻塞主线程
4.5. 主线程fork出子进程的是如何复制内存数据的?
fork采用操作系统提供的写时复制(copy on write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成阻塞。fork子进程时,子进程时会拷贝父进程的页表,即虚实映射关系(虚拟内存和物理内存的映射索引表),而不会拷贝物理内存。这个拷贝会消耗大量cpu资源,并且拷贝完成前会阻塞主线程,阻塞时间取决于内存中的数据量,数据量越大,则内存页表越大。拷贝完成后,父子进程使用相同的内存地址空间。
但主进程是可以有数据写入的,这时候就会拷贝物理内存中的数据。如下图(进程1看做是主进程,进程2看做是子进程):
在主进程有数据写入时,而这个数据刚好在页c中,操作系统会创建这个页面的副本(页c的副本),即拷贝当前页的物理数据,将其映射到主进程中,而子进程还是使用原来的的页c。
4.6. 在重写日志整个过程时,主线程有哪些地方会被阻塞?
- fork子进程时,需要拷贝虚拟页表,会对主线程阻塞。
- 主进程有
bigkey
写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞。 - 子进程重写日志完成后,主进程追加aof重写缓冲区时可能会对主线程阻塞。
4.7. 为什么AOF重写不复用原AOF日志?
父子进程写同一个文件会产生竞争问题,影响父进程的性能。
如果AOF重写过程中失败了,相当于污染了原本的AOF文件,无法做恢复数据使用。
5. RDB和AOF混合方式(4.0版本)
Redis4.0帮本提出了一个混合使用AOF日志和内存快照的方法,简单来说,内存快照以一定的频率执行,在两次快照期间,使用AOF日志记录这期间的所有命令操作。
这样依赖,快照不用很频繁的执行,这就避免了频繁fork对主线程的影响,而且AOF日志只用记录两次快照间的操作,不需要记录所有的操作,避免来文件过大的情况,也避免来重写开销。
如下图所示,T1和T2时刻的修改,用AOF日志记录,等到第二次做全量快照时,就可以清空AOF日志,因为此时的修改已经记录到快照中,恢复时就不再用日志来。
这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势, 实际环境中用的很多。
6. 从持久化中恢复数据
数据的备份、持久化做完来,我们如何从这些持久化文件中恢复数据呢?如果一台服务器上既有RDB文件,又有AOF文件,该加载谁呢?
其实想要从这些文件中恢复数据,只需要重新启动Redis即可。通过以下图了解流程:
redis重启时判断是否开启aof,如果开启了aof,那么就优先加载aof文件;
如果aof存在,那么就去加载aof文件,加载成功的话redis重启成功,如果aof文件加载失败,那么会打印日志表示启动失败,此时可以去修复aof文件后重新启动;
若aof文件不存在,那么redis就会转而去加载rdb文件,如果rdb文件不存在,redis直接启动成功;
如果rdb文件存在就会去加载rdb文件恢复数据,如加载失败则打印日志提示启动失败,如加载成功,那么redis重启成功,且使用rdb文件恢复数据;
那么为什么会优先加载AOF呢?因为AOF保存的数据更完整,通过上面的分析我们知道AOF基本上最多损失1s的数据。
7. 性能与实践
RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。
降低fork的频率,比如可以手动来触发RDB生成快照、与AOF重写;
控制Redis最大使用内存,防止fork耗时过长;
使用更牛逼的硬件;
合理配置Linux的内存分配策略,避免因为物理内存不足导致fork失败。
在线上我们到底该怎么做?我提供一些自己的实践经验。
- 如果Redis中的数据并不是特别敏感或者可以通过其它方式重写生成数据,可以关闭持久化,如果丢失数据可以通过其它途径补回;
- 自己制定策略定期检查Redis的情况,然后可以手动触发备份、重写数据;
- 单机如果部署多个实例,要防止多个机器同时运行持久化、重写操作,防止出现内存、CPU、IO资源竞争,让持久化变为串行;
- 可以加入主从机器,利用一台从机器进行备份处理,其它机器正常响应客户端的命令;
- RDB持久化与AOF持久化可以同时存在,配合使用
感谢原博文Redis进阶 !!!