AOF日志
Redis每执行一条命令的时候,就以追加的方式写入到一个文件里,当宕机或其它原因需重启Redis的时候,先去读取这个文件里的命令并且执行它,从而恢复数据
这种保存写操作到日志的持久化方式,就是Redis中的AOF(Append Only File)
注意:不会记录读操作,因为没意义
AOF默认不开启,可通过修改redis.conf的参数开启
为什么先执行写命令,再写日志?
- 避免额外的检查开销(写成功的一定是正确语法,写日志时无需再校验)
- 不阻塞当前写操作命令执行(写成功的才会写日志)
AOF潜在风险
- 丢失数据的风险(当命令执行成功,第二步写日志前,服务器发生宕机,就没来得及落磁盘)
- 阻塞下一条命令的风险(执行命令和写日志均在主进程执行,如果硬盘I/O压力大导致落盘速度很慢,就可能阻塞主进程,影响后续命令的执行)
回写策略
通过redis.conf配置文件中的参数appendfsync控制,有3种选项,三种策略其实就是调用fsync()函数的时机不同:
- Always
每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘
写回时机:同步写回,每次写入AOF日志后就立即执行fsync()
优点:可靠性高,最大程度保证数据不丢失
缺点:性能开销大
- Everysec
每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区数据写回硬盘
写回时机:每秒写回,创建一个异步任务执行fsync()
优点:性能开销适中,较好保证数据不丢失
缺点:发生宕机会丢失1秒内的数据
- No
不由Redis控制写回磁盘的时机,转交操作系统控制,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后由操作系统决定何时写回硬盘
写回时机:操作系统决定,不执行fsync()
优点:性能好
缺点:发生宕机可能丢失较多数据
重写机制
背景
随着写操作命令越来越多,AOF文件可能越来越大,当重启Redis时,恢复数据的过程就比较慢,存在性能问题
解决方案
压缩AOF文件,启用AOF重写机制
原理
读取当前数据库中的所有键值对,对每个key只保留最新的value,然后用一条命令记录到一个新的AOF文件,等全部记录完成后,替换旧文件
为什么不复用现有的AOF文件
如果AOF重写过程失败了,复用现有的AOF文件,可能会造成污染
而采用新文件的话,一旦失败,直接删除这个文件即可,可以回滚到旧文件
后台子进程bgrewriteaof
AOF重写过程其实是很耗时的,需要读取所有缓存的键值对,并为每个键值对生成一条最新状态的命令,写入到新文件中,然后全部写完后替换旧文件。因此不能放在主进程里操作,而是由后台子进程执行
- 避免阻塞主进程:子进程在进行AOF重写期间,主进程可以继续处理命令
- 子进程带有主进程的数据副本:
- 如果是使用线程,多线程之间会共享内存,那么在修改内存数据时,需要加锁保护数据安全,反而会降低性能;
- 而使用子进程,父子进程是共享内存数据的,不过这个共享只能是以只读的方式,当父子进程任意一方修改了内存数据,会发生写时复制(只负责被修改的部分,没修改的继续共享),于是父子进程就有了独立的数据副本,不需要加锁。
主进程通过系统调用 fork生成 bgrewriteaof子进程时,操作系统会把主进程的页表复制一份给子进程,这个页表记录着虚拟地址和物理地址的映射关系,而不是直接复制物理内存,也就是说,两者的虚拟内存空间不同,但对应的物理空间是同一个。页表对应的页表项的属性会被标记该指向的物理内存权限为只读,这样一来,子进程就共享了父进程的物理内存数据,能够节省内存资源。
读时共享,写时复制
上面讲到子进程可以共享父进程的内存数据,但权限为只读
不过,当父进程或子进程向这个内存空间发起写操作,CPU会触发写保护中断,这是由于违法了权限,导致操作系统会在写保护中断处理函数里进行物理内存复制,并重新设置二者的内存映射关系,将父子进程的物理内存权限设置为可读写,最后才会对内存进行写操作。
这个技术称为写时复制(Copy on write),顾名思义,在发生写操作时,操作系统才会去复制物理内存,这是为了防止fork创建子进程时,由于物理内存数据过大,复制时间过长,导致阻塞父进程。
当然,fork过程复制页表的时候,父进程也是处于阻塞的,只不过页表相对物理内存小很多,所以复制页表的过程一般是很快的。
触发重写机制后,主进程就会创建子进程,此时父子进程共享物理内存,重写子进程只会对该内存只读,会读取所有数据,并逐一把键值对转为最新状态的一条命令,再记录到新的AOF文件中。
重写期间,主进程可以继续正常处理命令。如果此时主进程修改了现有的key-value,会发生写时复制(注意:这里只会复制被修改的那部分物理内存数据,没修改的部分还是和子进程共享),如果这个被修改的是一个大key(数据量比较大的键值对),这时复制的物理内存过程就比较耗时,有阻塞主进程的风险。
AOF重写缓冲区
还有个问题,在重写AOF期间,如果主进程修改了一个已经存在的键值对,那么这个key-value在子进程和父进程的内存数据就不一致了,为了解决这个问题,Redis设置了一个AOF重写缓冲区,在创建bgrewriteaof子进程之后开始使用。
重写期间,当执行完一个命令后,会同时将其写入到AOF缓冲区和AOF重写缓冲区,主进程执行以下操作;
- 执行客户端发来的命令
- 将执行后的写命令追加到 AOF缓冲区
- 将执行后的写命令追加到 AOF重写缓冲区
当子进程完成重写后,会向主进程发送一条信号(进程间通讯的一种方式,异步的),主进程收到后会调用一个信号处理函数,做以下操作:
- 将AOF重写缓冲区的所有内容追加到新的AOF文件中,使得新旧两个文件所保存的数据一致
- rename新的AOF文件名,覆盖现有的AOF文件
信号处理函数执行完毕,主进程就可以继续正常处理其它命令,因此该函数也会对父进程造成阻塞
总结一下,整个AOF重写过程,可能会阻塞主进程的阶段:
- fork创建子进程期间,页表越大,阻塞时间越长
- 发生写时复制,这期间拷贝物理内存,内存越大,阻塞时间越长
- 收到子进程重写完毕的信号,执行信号处理函数,执行越久,阻塞时间越长
参考
小林coding