持久化:解决单机备份问题。Redis是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,所以定期将Redis中的数据或写命令从内存保存到硬盘,重启时利用持久化文件实现数据恢复。
Redis持久化方式有两种:
- RDB持久化:保存当前内存数据到硬盘;
- AOF持久化:保存执行的每条写命令到硬盘(类似于MySQL的binlog);AOF持久化的实时性高,当进程意外退出时丢失的数据更少,因此AOF是目前主流的持久化方式。
一、RDB持久化(Redis DataBase)
RDB持久化是将当前进程中的全量数据生成快照保存到硬盘,也称作快照持久化,保存的文件后缀是rdb;当Redis重新启动时,可以读取快照文件恢复数据。
1、触发条件
RDB持久化的触发分为两种:手动触发、自动触发。
手动触发:
1.save命令:阻塞Redis服务器主进程,服务器不能处理任何命令请求,直到RDB文件创建完。
2.bgsave命令:会创建一个子进程,由子进程负责创建RDB文件,父进程(即Redis主进程)继续处理请求。只有创建子进程时会阻塞服务器,后面过程不阻塞;自动触发RDB持久化时,Redis也是使用bgsave。
命令 | save | bgsave | IO类型 | 同步 | 异步 | 阻塞 | 整个流程阻塞 | 只有在fork子进程时阻塞 | 复杂度 | O(n) | O(n) | 优点 | 不会消耗额外内存 | 不阻塞客户端命令 | 缺点 | 阻塞客户端命令 | 需要fork,消耗内存 |
自动触发:
1.在redis.conf配置文件中配置触发条件save m n(当m秒内发生n次变化时则会触发bgsave)。可配置多个,当满足任意一个条件时都会触发bgsave。
https://github.com/redis/redis/blob/3.2/redis.conf#L202
# save <seconds> <changes>
// 可配置多个
// 当时间到900秒时,如果redis数据发生了至少1次变化,则执行bgsave
save 900 1
save 300 10
save 60 10000
2.主从复制场景下,从节点需要全量复制,则主节点会执行bgsave命令,并将rdb文件发送给从节点;
3.debug reload提供debug级别的重启,不清空内存的一种重启,这种方式也会触发RDB文件的生成;
4.执行shutdown命令时,自动执行rdb持久化,如下图日志:
save m n 配置的实现原理:
通过serverCron函数、dirty计数器、和lastsave时间戳来实现的。Redis会每隔100ms执行serverCron函数,会检查所有save m n的配置,只要满足一个save m n则执行bgsave。
- 当前时间-lastsave > m
- dirty >= n
serverCron函数:Redis服务器的周期性操作函数,默认每隔100ms执行一次,来对各种状态进行维护,如检查save m n配置是否满足条件,满足就执行bgsave;判断当前user_memory和user_memory_peak大小来设置内存使用峰值等等;函数源码地址:redis/server.c at 3.2 · redis/redis · GitHub
dirty计数器:Redis服务器维持的一个状态,记录执行bgsave/save命令后,进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。
// dirty记录的是服务器进行了多少次修改,而不是客户端执行了多少条修改数据的命令。
set mykey helloworld
// 则dirty+1
sadd myset v1 v2 v3
// 则dirty+3
lastsave时间戳:也是Redis服务器维持的一个状态,记录上一次成功执行save/bgsave的时间。
2、执行流程
Redis源码剖析(十)--RDB持久化 - 不学习就没有梦想 - 博客园
- Redis父进程判断:当前是否有正在执行save/bgsave/bgrewriteaof(后面会详细介绍该命令)的子进程,如果在执行则bgsave命令直接返回。bgsave/bgrewriteaof 的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。
- 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令
- 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令
- 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换
- 子进程发送信号给父进程表示完成,父进程更新统计信息
3、RDB文件
RDB文件是经过压缩的二进制文件。
存储路径:
RDB文件的存储路径既可以在启动前配置,也可以通过命令动态设定。
// 配置地址:https://github.com/redis/redis/blob/3.2/redis.conf#L236
// redis.conf文件里面配置,默认是Redis根目录下的dump.rdb文件
// dir配置指定目录
# Note that you must specify a directory here, not a file name.
dir ./
// dbfilename指定文件名
# The filename where to dump the DB
dbfilename dump.rdb
// 动态修改RDB存储路径,在磁盘损害或空间不足时非常有用
config set dir {newdir}
config set dbfilename {newFileName}
?RDB文件格式:
- REDIS:5字节常量,保存着 "REDIS" 五个字符;
- db_version:4字节,RDB文件的版本号,非Redis版本号;
- database 0:一个完整的数据库(0号数据库),同理database 3表示3号数据库。只有当数据库中有键值对时,RDB文件中才会有该数据库的信息(上图所示的Redis中只有0号和3号数据库有键值对);
- SELECTDB:1字节常量,代表后面跟着的是数据库号码;
- db_number:数据库号码;
- key_value_pairs:具体键值对数据,含过期时间的键值对会带有 EXPIRETIME_MS 和过期时间。
- key_value_pairs键值对中的 TYPE 属性:记录类对象的编码类型,程序会根据 TYPE 属性来决定如何读入和解释value数据。
- EOF:常量,RDB文件结束标志。
- check_sum:前面所有内容的校验和(CRC64),通过比较计算来比较,用来检查RDB文件是否出错或损坏。
默认采用LZF算法对RDB文件进行压缩。虽然压缩耗时,但是可以大大减小RDB文件的体积。RDB文件的压缩并不是针对整个文件,而是对数据库中的字符串进行的,且只有在字符串达到一定长度(20字节)时才会进行。
// https://github.com/redis/redis/blob/3.2/redis.conf#L225
// redis.conf配置中压缩默认开启
rdbcompression yes
// 通过命令关闭rdb压缩
config set rdbcompression no
??
4、启动时加载
- RDB文件的载入是服务器启动时自动执行的,没有专门的命令;
- 但AOF的优先级更高,当AOF开启时,Redis会优先载入AOF文件恢复数据;只有当AOF关闭时,才会自动载入RDB文件;
- 服务器载入RDB文件期间处于阻塞状态,直到载入完成为止;
- 载入RDB文件时,会对RDB文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败。
5、RDB常用配置总结
// RDB文件和AOF文件所在目录
dir ./
// RDB文件名
dbfilename dump.rdb
// bgsave自动触发的条件;如果没有save m n配置,相当于自动的RDB持久化关闭,不过此时仍可以通过其他方式触发
save <seconds> <changes>
// 当bgsave出现错误时,Redis是否停止执行写命令;
// 设置为yes,当硬盘出现问题时,可以及时发现,避免数据的大量丢失;
// 设置为no,则Redis无视bgsave的错误继续执行写命令,当对Redis服务器的系统(尤其是硬盘)使用了监控时,该选项考虑设置为no
stop-writes-on-bgsave-error yes:
// 是否开启RDB文件压缩
rdbcompression yes
// 是否开启RDB文件的校验,在写入文件和读取文件时都起作用;
// 关闭checksum在写入文件和启动文件时大约能带来10%的性能提升,但是数据损坏时无法发现。
rdbchecksum yes
四、AOF持久化(Append-only File)
AOF持久化即Append Only File,将Redis执行的写命令记录到AOF日志文件中(有点像MySQL的binlog),Redis重启时再次执行AOF文件中的命令来恢复数据。与RDB相比,AOF实时性更好,因此已成为主流的持久化方案。
1、开启AOF
Redis服务器默认开启RDB,关闭AOF;要开启AOF,需要在配置文件中配置;
// 配置地址:https://github.com/redis/redis/blob/3.2/redis.conf#L593
// aof开关
appendonly no
// aof文件名
appendfilename "appendonly.aof"
2、执行流程
由于需要记录Redis的每条写命令,因此AOF不需要触发,AOF的执行流程:
- 命令追加(append):将Redis的写命令追加到缓冲区aof_buf;
- 文件写入(write)和文件同步(sync):根据不同的同步策略将aof_buf中的内容同步到硬盘;
- 文件重写(rewrite):定期重写AOF文件,达到压缩的目的。
命令追加(append):
- 将写命令追加到缓冲区,而不是直接写入文件,避免每次有写命令的磁盘IO,影响性能;
- 命令追加的格式是Redis命令请求的协议格式,它是一种纯文本格式,简单易读易操作;
- 在AOF文件中,除了用于指定数据库的select命令(如select 0 为选中0号数据库)是由Redis添加的,其他都是客户端发送来的写命令;
文件写入(write)和文件同步(fsync):
Redis提供了多种AOF缓存区的同步策略,策略涉及到操作系统的write函数和fsync函数。
- write函数:将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里;虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失;
- fsync、fdatasync等同步函数:可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。
// 同步策略配置,https://github.com/redis/redis/blob/3.2/redis.conf#L622
// 通过appendfsync来配置
appendfsync everysec
同步策略各种配置值的作用:
- always:命令写入aof_buf后立即调用系统fsync操作同步到AOF文件,fsync完成后线程返回;写命令都进行磁盘IO,Redis只能支持大约几百TPS写入,严重降低了Redis的性能。即便使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会降低SSD的寿命。
- no:命令写入aof_buf后调用系统write操作;同步由操作系统负责,通常同步周期为30秒。文件同步时间不可控,缓冲区堆积数据多,数据安全无法保证。
- everysec:命令写入aof_buf后调用系统write操作,write完成后线程返回;fsync同步文件操作由专门的线程每秒调用一次。everysec是前述两种策略的折中,是性能和数据安全性的平衡,因此是Redis的默认和推荐配置。
命令 | alwarys | everysec | no | 优点 | 不丢失数据 | 每秒一次fsync | | 缺点 | IO开销大,一般的sata磁盘只支持击败TPS | 最多丢1s的数据 | 时间不可控、数据安全不可保证 |
3、文件重写(rewrite)
随着时间积累,AOF文件的写命令越来越多,文件体积也越来越大;过大的AOF文件不仅会影响服务器的运行,也会导致数据恢复时间过长。AOF重写是把Redis进程内的数据转化为写命令,同步到新的AOF文件;不会对旧的AOF文件进行任何读取、写入操作!
- AOF重写会减少AOF文件的体积
- 加速服务恢复速度
文件重写为什么能压缩AOF文件体积?
- 过期数据不再写入文件
- 无效的命令不再写入文件:如数据多次重复设置(set key v1,set key v2)、删除数据(sadd myset v1, del myset)等等;
- 多条命令合并:如sadd myset v1, sadd myset v2, sadd myset v3可以合并为sadd myset v1 v2 v3。为了防止单条命令过大造成客户端缓冲区溢出,list、set、hash、zset类型的key,并不一定只使用一条命令;而是以某个常量为界将命令拆分为多条。这个常量在redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD中定义,不可更改,3.0版本中值是64。
文件重写的触发
触发分为手动触发和自动触发;
手动触发:调用bgrewriteaof命令,该命令的执行与bgsave有些类似:都是fork子进程进行具体的工作,且都只有在fork时阻塞。
自动触发:根据配置auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数,以及aof_current_size和aof_base_size状态确定触发时机。
auto-aof-rewrite-min-size 64mb:AOF文件大小超过64m时重写;
auto-aof-rewrite-percentage 100:当前AOF大小(即aof_current_size)和上一次重写时AOF大小(aof_base_size)的比值超过100%时进行重写。
当auto-aof-rewrite-min-size和auto-aof-rewrite-percentage同时满足时,才会自动触发AOF重写,即bgrewriteaof操作。
?文件重写的流程:
- Redis父进程首先判断当前是否存在正在执行 bgsave/bgrewriteaof的子进程,如果存在则bgrewriteaof命令直接返回,如果存在bgsave命令则等bgsave执行完成后再执行。基于性能方面的考虑。
- 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的。
- 父进程fork后,bgrewriteaof命令返回”Background append only file rewrite started”信息并不再阻塞父进程,并可以响应其他命令。Redis的所有写命令依然写入AOF缓冲区,并根据appendfsync策略同步到硬盘,保证原有AOF机制的正确。
- fork操作使用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然在响应命令,因此Redis使用AOF重写缓冲区(图中的aof_rewrite_buf)保存这部分数据,为了防止新AOF文件生成期间丢失这部分数据,bgrewriteaof执行期间,Redis的写命令同时追加到aof_buf和aof_rewirte_buf两个缓冲区。
- 子进程根据内存快照,按照命令合并规则写入到新的AOF文件。
- 子进程写完新的AOF文件后,向父进程发信号,父进程更新统计信息,具体可以通过info persistence查看。
- 父进程把AOF重写缓冲区的数据写入到新的AOF文件,这样就保证了新AOF文件所保存的数据库状态和服务器当前状态一致。
- 新的AOF文件替换老文件,完成AOF重写。
在AOF重写的过程中,如果有新的写命令到来了,会影响AOF重写吗?
不会,新的写命令不仅会记录AOF缓冲区,还会记录到AOF重写缓冲区,这样当AOF重写结束后,把AOF重写缓冲区数据写到新AOF文件,就不会丢失了。
4、启动时加载
当AOF配置开启时,Redis会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会使用RDB文件恢复数据。
文件校验:与载入RDB文件类似,载入AOF文件时,也会进行校验,如果文件损坏,则日志中打印错误,Redis启动失败。但如果是AOF文件结尾不完整(机器突然宕机等容易导致文件尾部不完整),且aof-load-truncated参数开启,则日志中会输出警告,Redis忽略掉AOF文件的尾部,启动成功。aof-load-truncated参数默认是开启的。
伪客户端:因为AOF文件存储的是Redis命令,且命令只能在客户端上下文中执行;Redis服务器在载入AOF文件之前,会创建一个没有网络连接的客户端,来执行AOF文件中的命令,命令执行的效果与带网络连接的客户端完全一样,恢复数据。
5. AOF常用配置总结
// 是否开启AOF,默认关闭
appendonly no
// AOF文件名
appendfilename "appendonly.aof"
// RDB文件和AOF文件所在目录
dir ./
// fsync持久化策略
appendfsync everysec
// AOF重写期间是否禁止fsync;
// 启该选项,可以减轻文件重写时CPU和硬盘的负载(尤其是硬盘),但是可能会丢失AOF重写期间的数据;需要在负载和安全性之间进行平衡。
no-appendfsync-on-rewrite no
// 文件重写触发条件之一,当前AOF大小(即aof_current_size)和上一次重写时AOF大小(aof_base_size)的比值超过100%时进行重写。
auto-aof-rewrite-percentage 100
// 文件重写触发提交之一, AOF文件大小超过64m时重写
auto-aof-rewrite-min-size 64mb
// 如果AOF文件结尾损坏,Redis启动时是否仍载入AOF文件
aof-load-truncated yes
五、方案选择与常见问题
1、RDB和AOF优缺点
- RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快很多。当然,与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。
- RDB文件的缺点在于其数据快照的持久化方式决定了实时差,数据的大量丢失无法接受只能选择AOF持久化。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。
- 与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好;缺点是文件大、恢复速度慢、对性能影响大。
持久化 | RDB | AOF | 启动优先级 | 低 | 高 | 体积 | 小 | 大 | 恢复速度 | 快 | 慢 | 数据安全性 | 丢数据 | 可以高实时性,根据策略 | 轻重 | 重 | 轻 |
2、持久化策略选择
无论是RDB还是AOF,持久化的开启都是要付出性能的代价。
- RDB持久化,bgsave进行fork操作时阻塞主进程;子进程向硬盘写数据也会带来IO压力;
- AOF持久化,向硬盘写数据的频率大大提高(everysec策略下为秒级),IO压力更大。可能造成AOF追加阻塞问题。
- Redis完全用作DB层数据的cache层,那么无论是单机还是主从,都可以不进行任何持久化。
- 在单机环境下(对于个人开发者),如果可以接受十几分钟或更多数据丢失,可以选择RDB持久化;只能接受秒级别的数据丢失,应该选择AOF。
- 多数生产环境下会配置主从或集群,slave既可以实现数据热备,也可以进行读写分离,以及在master宕掉后继续提供服务。可以:
- master:完全关闭持久化(包括RDB和AOF),让master的性能达到最好;
- slave:关闭RDB,开启AOF(对数据安全要求不高,开启RDB关闭AOF也可以),定时对持久化文件进行备份。关闭AOF的自动重写,通过定时任务在每天Redis闲时(如凌晨12点)调用bgrewriteaof。
- 异地灾备:同机房断电、火宅导致磁盘损坏。所有需要将定时将RDB文件或重写后的AOF文件拷贝到远程机器上。一般,由于RDB文件文件小、恢复快,因此灾难恢复常用RDB文件;异地备份的频率根据数据安全性的需要及其他条件来确定,但最好不要低于一天一次。
Redis重启数据加载逻辑
3、fork阻塞:CPU的阻塞
在实践中,为什么Redis的单机内存不能过大?
- 当请求暴增,需要从库扩容时,Redis内存过大会导致扩容时间太长;
- 持久化过程中的fork操作时,生成内存页时间变长;
- 当主机宕机时,主从切换,Redis内存过大导致挂载速度过慢;
fork操作细节:
fork采用操作系统提供的写时复制机制(copy-on-write,COW),避免一次性拷贝大量内存数据给主进程造成阻塞问题,fork时会拷贝进程必要的数据结构,如内存页表(虚拟内存和物理内存的映射索引表)。拷贝过程会消耗大量CPU资源,拷贝完成之前进程是阻塞的,所有阻塞时间取决于整个内存大小,实例越大,内存页表越大,fork阻塞时间越长。
拷贝完内存页表后,子进程与父进程指向相同的内存地址空间。也就是说,子进程没有复制内存,而是共享内存,通过内存页表指向同一内存地址。
但是kenel会把所有内存页设置为read-only只读权限,当主线程进行写操作时,会触发页异常中断(page-fault),页中断处理会copy复制出新的内存页。所以当没有数据改变时,主进程和子进程共享内存数据,只有数据变更时,才进行copy操作。即copy-on-warite。
RDB持久化的bgsave和AOF重写的bgrewriteaof都会fork出子进程操作,如果redis内存过大,会导致fork操作时复制内存页表耗时过多;而fork会堵塞主进程。Redis单机内存达到了10GB,fork时耗时可能会达到百毫秒级别(如果使用Xen虚拟机,这个耗时可能达到秒级别),可通过命令info stats,查看latest_fork_usec的值,单位为微秒。
4、AOF追加阻塞:硬盘的阻塞
AOF的everysec策略流程图:
- 文件写入:主线程中,命令写入aof_buf后调用系统write操作,write完成后主线程返回。
- 文件同步:由专门的文件同步线程每秒调用系统的fsync同步硬盘
- 主线程每次AOF会对比上次fsync成功的时间,距上次不到2s,主线程直接返回;超过2s,则主线程阻塞直到fsync同步完成。
因此系统硬盘负载过大导致fsync速度太慢,会导致Redis主线程的阻塞;使用everysec配置,AOF最多可能丢失2s的数据,而不是1s。
这样处理的好处是Redis主线程高速的想aof_buf写入命令,磁盘负载可能越来越大,IO资源占用越来越多,如果Redis异常退出,丢失的数据也会越多。
AOF追加阻塞问题定位的方法:
- info Persistence中的aof_delayed_fsync:AOF追加阻塞发生的累积次数(即主线程等待fsync而阻塞)。
- AOF阻塞时的Redis日志
- 频繁发生AOF阻塞时,说明系统的硬盘负载太大,可以更换IO速度更快的硬盘,或通过IO监控分析工具对系统的IO负载进行分析,如iostat(系统级io)、iotop(io版的top)、pidstat等。
5、info Persistence命令
看持久化相关状态的方法
rdb_last_bgsave_status:上次bgsave 执行结果,可以用于发现bgsave错误
rdb_last_bgsave_time_sec:上次bgsave执行时间(单位是s),可以用于发现bgsave是否耗时过长
aof_enabled:AOF是否开启
aof_last_rewrite_time_sec: 上次文件重写执行时间(单位是s),可以用于发现文件重写是否耗时过长
aof_last_bgrewrite_status: 上次bgrewrite执行结果,可以用于发现bgrewrite错误
aof_buffer_length和aof_rewrite_buffer_length:aof缓存区大小和aof重写缓冲区大小
aof_delayed_fsync:AOF追加阻塞情况的统计
6、混合持久化
重启Redis很少使用RDB来恢复内存,因为会丢失大量数据;通常使用AOF回放,但相对于RDB会慢很多,启动花费时间长。
Redis4.0提出了混合使用AOF日志和内存快照的方法。内存快照以一定的频率执行,在两次快照之间,还用AOF日志记录着期间的所有命令操作。这样可以避免频繁的生成快照;AOF只记录两次快照间记录,减少文件大小,避免重写开销。
在Redis重启时,可以先加载RDB的内容,然后再重放增量AOF日志就可以完全替代之前AOF全量文件重放,重启效率的到提升。
总结:
1、持久化在Redis高可用中的作用:数据备份,与主从复制相比强调的是由内存到硬盘的备份。
2、RDB持久化:将数据快照备份到硬盘;介绍了其触发条件(包括手动出发和自动触发)、执行流程、RDB文件等,特别需要注意的是文件保存操作由fork出的子进程来进行。
3、AOF持久化:将执行的写命令备份到硬盘(类似于MySQL的binlog),介绍了其开启方法、执行流程等,特别需要注意的是文件同步策略的选择(everysec)、文件重写的流程。
4、一些现实的问题:包括如何选择持久化策略,以及需要注意的fork阻塞、AOF追加阻塞等。
|