-
Redis的持久化
为了防止数据丢失以及服务重启时能够恢复数据,Redis支持数据的持久化,主要分为两种方式,分别是RDB和AOF.
RDB
RDB持久化是把当前进程数据生成快照保存到磁盘上的过程,由于是某一时刻的快照,那么快照中的值要早于或者等于内存中的值。
生成的rdb文件的名称以及存储位置由redis.conf中的dbfilename和dir两个参数控制,默认生成的rdb文件是dump.rdb。
触发方式
触发rdb持久化的方式有2种,分别是手动触发和自动触发。
手动触发
redis客户端执行save命令和bgsave命令都可以触发rdb持久化,但是两者还是有区别的。
1.使用save命令时是使用redis的主进程进行持久化,此时会阻塞redis服务,造成服务不可用直到持久化完成,线上环境不建议使用;
2.bgsave命令是fork一个子进程,使用子进程去进行持久化,主进程只有在fork子进程时会短暂阻塞,fork操作完成后就不再阻塞,主进程可以正常进行其他操作。
3.bgsave是针对save阻塞主进程所做的优化,后续所有的自动触发都是使用bgsave进行操作。
自动触发
在以下4种情况时会自动触发
-
redis.conf中配置save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件;
-
主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点;
-
执行debug reload命令重新加载redis时也会触发bgsave操作;
-
默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作;
关闭rdb持久化
如果要关闭rdb持久化可以用两种方法:
- 执行以下命令(redis-cli):
- 修改配置文件
流程
rdb持久化的流程图如下所示:
具体流程如下:
- redis客户端执行bgsave命令或者自动触发bgsave命令;
- 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;
- 如果不存在正在执行的子进程,那么就fork一个新的子进程进行持久化数据,fork过程是阻塞的,fork操作完成后主进程即可执行其他操作;
- 子进程先将数据写入到临时的rdb文件中,待快照数据写入完成后再原子替换旧的rdb文件;
- 同时发送信号给主进程,通知主进程rdb持久化完成,主进程更新相关的统计信息(info Persitence下的rdb_*相关选项)。
优缺点
优点
- RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;
- Redis加载RDB文件恢复数据要远远快于AOF方式;
缺点
- RDB方式实时性不够,无法做到秒级的持久化;
- 每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;
- RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全;
- 版本兼容RDB文件问题;
AOF
aof方式持久化是使用文本协议将每次的写命令记录到aof文件中,经过文件重写后记录最终的数据生成命令,在redis启动时,通过执行aof文件中的命令恢复数据。
aof方式主要解决了数据实时性持久化的问题,aof方式对于兼顾数据安全性和性能非常有帮助。
开启aof
开启aof模式持久化需要修改redis.conf文件中的如下配置:
也可以在redis客户端使用命令行的方式开启或者关闭aof
aof持久化流程
-
append aof文件只记录写命令,不记录读命令,当服务端接收到写命令后,redis会将命令写入到aof缓冲区中,之所以写入缓冲区而不直接写入aof文件中是因为如果每次都将命令直接写入到文件中,那么redis的性能将完全取决于硬盘的读写能力,这与redis性能至上的理念不符,另外,写入缓冲区中也便于使用不同的同步策略。
-
sync 文件同步,即将aof缓冲区中的命令同步到aof文件中,redis提供三种策略以供选择,由参数appendfsync控制,三种策略分别是:
always: 表示命令append到缓冲区以后调用系统fsync操作同步到aof文件中,fsync操作完成后主线程返回;
no: 表示命令写入aof缓冲区后调用操作系统write操作,不对aof文件做fsync同步,同步到硬盘操作由操作系统负责,通常同步周期最长30秒;
everysec: 表示命令写入aof缓冲区后调用操作系统write操作,write操作完成后主线程返回,由专门的线程每秒去进行fsync同步文件操作
默认使用everysec,兼顾性能和安全性,很显然,使用always时每次都要等同步完成后才能返回,这个性能是很低的;同理使用no时,虽然不用每次都同步aof文件,但是同步操作周期不可控,数据安全性得不到保障,因此还是使用默认的everysec兼顾安全性和性能,每一秒同步一次,也就是在突发状况下最多丢失1秒的数据。
- 重写(rewrite)
随着写命令越来越多,aof文件的体积也越来越大,此时就需要重写机制来按照特定的机制清除或者合并命令从而达到减小文件体积,便于redis重启加载的目的。
重写机制
重写规则
- 进程内已经过期的数据不再写入文件;
- 只保存最终数据的写入命令,如set a 1, set a 2, set a 3,此时只保留最终的set a 3;
- 多条写命令合并为一条命令,如lpush list 1, lpush list 2, lpush list 3合并为lpush list 1,2,3,同时为了防止单条命令过大,对于list、set、zset、hash等以64个元素为界限拆分为多条命令;
触发
-
手动触发 手动执行
bgrewriteaof
命令即可触发aof重写 -
自动触发 自动触发与redis.conf中的
auto-aof-rewrite-min-size
和auto-aof-rewrite-percentage
配置有关,默认配置如下:
auto-aof-rewrite-min-size: 表示触发aof重写时aof文件的最小体积,默认64m auto-aof-rewrite-percentage: 表示当前aof文件空间和上一次重写后aof文件空间的比值,默认是aof文件体积翻倍时触发重写
auto-aof-rewrite-percentage的计算方法:
auto-aof-rewrite-percentage =(当前aof文件体积 - 上次重写后aof文件体积)/ 上次重写后aof文件体积 * 100%
自动触发的条件: (当前aof文件体积 > auto-aof-rewrite-min-size) && (auto-aof-rewrite-percentage的计算值 > 配置文件中配置的auto-aof-rewrite-percentage值)
重写流程
- 手动或者自动触发文件重写后主进程需要先判断当前是否有子进程存在,如果存在则直接返回,不存在则fork子进程;
- fork操作完成后,主进程即可响应其他命令,在子进程生成新的aof文件过程中,主进程仍然维持原来的流程以保证原有aof机制的正确性;
- 在子进程生成新的aof文件过程中主进程执行的新命令同时会被写入到aof重写缓冲区中,当新aof文件生成后再将这一部分命令写入到新aof文件中,防止数据丢失;
-
子进程根据内存快照,根据重写规则生成新的aof文件,每次批量写入硬盘数据量由配置
aof-rewrite-incremental-fsync
控制,默认为32MB,防止单次刷盘数据过多造成硬盘阻塞; - 父进程把aof重写缓冲区的数据写入到新的aof文件中;
- 使用新aof文件替换旧的aof文件并发送信号给主进程表示重写完成。
优缺点
优点
- 数据安全性较高,每隔1秒同步一次数据到aof文件,最多丢失1秒数据;
- aof文件相比rdb文件可读性较高,便于灾难恢复;
缺点
- 虽然经过文件重写,但是aof文件的体积仍然比rdb文件体积大了很多,不便于传输且数据恢复速度也较慢
- aof的恢复速度要比rdb的恢复速度慢
fork以及copy_on_write
fork
不论RDB方式去创建一个新的rdb文件还是AOF方式重写aof文件,都需要fork一个子进程去处理以便在不阻塞主进程的情况下完成rdb文件的生成以及aof文件的重写,下面我们简单了解一下什么是fork以及使用到的copy_on_write写时复制技术。
何为fork?简而言之就是创建一个主进程的副本,创建的子进程除了进程id,其余任何内容都和主进程完全一致,这就是fork。
fork创建的子进程独立于主进程而存在,虽然两个进程内存空间的内容完全一致,但是对于内存的写入、修改以及文件的映射都是独立的,两个进程不会相互影响。
通过fork技术完美的解决了快照的问题,只需要某个时间点的内存中的数据,而父进程可以继续对自己的内存进行修改、写入而不会影响子进程的内存,这既不会阻塞主进程也不影响生成快照。
通过fork子进程的方式虽然能够完美解决不阻塞的情况下创建快照的问题,但是又会引入以下的问题:
子进程和主进程拥有相同的内存空间,就相当于瞬间将内存的使用量提高了一倍,假设服务器是16GB内存,主进程占用10GB,那么此时再创建子进程还需奥10GB,很明显超过了总内存,这很显然是存在很大问题的,即使不超过总内存,fork时将内存使用量提高一倍也是不可取的。
COW
写时拷贝(COW)就是为了解决这个问题而出现,那么什么是COW呢?
COW的主要作用就是将拷贝推迟到写操作真正发生时,这也就避免了大量无意义的拷贝。
什么意思呢?
意思是说在fork子进程时,父子进程会被内核分配到不同的虚拟内存空间中,对于父子进程来说它们访问的是不同的内存空间,但是两个虚拟内存空间映射的仍然是相同的物理内存,也就是说在fork完成后未发生任何修改时,父子进程对应的物理内存是同一份。
如果此时主进程执行了修改或者写入操作?因为有了修改或写入操作,此时父子进程内存就会出现不一致的情况,由于是主进程进行的修改,因此内核会为主进程要修改的内存块创建一个副本供主进程进行修改而不改变子进程的内存,也就是谁发生了修改就要为谁创建相应的副本。
linux中内存的复制是以内存页为单位的(4KB),也就是会为发生改变的内存页创建副本。
COW技术弥补了fork进程时内存翻倍的情况,fork操作为子进程访问父进程提供了支持,COW减少了额外的开销,这两者是Redis能够使用子进程进行快照持久化的核心。
COW原理:
fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会 把触发的异常的页复制一份,于是父子进程各自持有独立的一份。
COW优点:
- 减少不必要的资源分配,只有在发生改变时才创建修改的内存页的副本,而不是创建整个内存的副本;
- 减少fork子进程的时间,因为cow的存在,fork子进程时只需要复制主进程的空间内存页表即可,而不需要复制物理内存,因此大大提高了fork子进程的速度。
COW缺点:
- 如果fork之后,父子进程都需要进行大量修改,那么就会出现大量的分页错误(页异常中断page-fault),这就有点得不偿失了。
但是对于redis来说,子进程只是用来生成快照的,并不会进行修改或者写入操作,也就不存在上述所说的问题了。
重启加载
Redis支持单独启动RDB或者单独启用AOF,也支持同时启用RDB和AOF,redis重启时加载流程如下所示:
- redis重启时判断是否开启aof,如果开启了aof,那么就优先加载aof文件;
- 如果aof存在,那么就去加载aof文件,加载成功的话redis重启成功,如果aof文件加载失败,那么会打印日志表示启动失败,此时可以去修复aof文件后重新启动;
- 若aof文件不存在,那么redis就会转而去加载rdb文件,如果rdb文件不存在,redis直接启动成功;
- 如果rdb文件存在就会去加载rdb文件恢复数据,如加载失败则打印日志提示启动失败,如加载成功,那么redis重启成功,且使用rdb文件恢复数据;
持久化过程中需要注意的问题
aof追加阻塞
aof追加阻塞是指在开启aof持久化时,默认使用的是everysec同步策略,此时有一个额外的线程同步aof缓冲区中的内容到磁盘上的aof文件,如果在同步过程中由于磁盘io过高导致的redis主进程阻塞;
出现aof阻塞的根本原因是磁盘负载过高,redis主进程会监控同步线程每次同步aof缓冲区内容到aof文件所耗费的时间,如果距离上次同步成功的时间在2s内,那么主线程就直接返回,如果距离上次同步成功的时间超过2s,redis主进程就会阻塞,直到同步完成。
具体的流程图如下所示: 发生aof追加阻塞时会严重影响redis的性能,造成该现象的主要原因是磁盘高负载,那么相应的解决方案也要从磁盘负载上来解决。
解决方案:
- redis尽量不要与其他高磁盘消耗的服务部署在一起,如rabbitmq等消息队列,mysql等数据库服务;
- 配置开启no-appendfsync-on-rewrite=yes,表示在重写期间不做fsync操作;
- 单机配置多个redis实例的情况下,不同实例分盘存储aof文件以减轻单个磁盘的压力;
fork阻塞耗时问题
无论生成rdb文件还是重写aof文件,都会使用fork创建一个子进程来处理,这样就不会阻塞主进程了,虽然fork出来的子进程不会阻塞主进程,但是fork的过程中还是会阻塞主进程。也就是说子进程创建过程中还是会阻塞主进程影响redis对外提供服务。
前面提过fork过程中使用写时复制技术,并不会真正的复制物理内存,但是会复制主进程的空间内存页表,例如主进程为10G内存,大概要复制20M的空间页表,也就是说主进程内存越大,需要复制的空间内存页表越大,fork所需的时间越长,redis阻塞的时间越长,因此fork操作的优化点在于主进程的内存大小,另外有的虚拟化技术也会加大fork的时间,如Xen虚拟机。
因此从主进程内存和虚拟机化技术两个方面来优化fork阻塞耗时问题:
- 尽量使用物理机或者高效支持fork的虚拟化技术;
- 控制Redis实例最大可用内存,fork耗时和redis主进程内存量成正比;
- 降低fork操作的频率,如调高auto-aof-rewrite-min-size的值以减少aof重写的次数,或者主从复制时减少全量复制等;
- 合理配置linux内存分配策略,防止由于物理内存不足导致的fork失败;
__EOF__