Redis原理之_未雨绸缪_持久化
前言
Github:https://github.com/HealerJean
1、引入
问题1:Redis为什么要有持久化?
答案:如下
Redis
的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制 来保证 Redis
的数据不会因为故障而丢失,这种机制就是 Redis
的持久化机制。 持久化功能有效避免进程退出造成的数据丢失问题。下次重启的时候利用之前持久化的文件即可实现数据恢复。
问题2:Redis
的持久化机制有两种,哪俩种?有什么区别?
答案:如下(包含下图)
1、快照:快照是内存数据的二进制序列化形式,在存储上非常紧,快照是一次全量备份
2、 AOF
日志,AOF
日志是连续的增量备份,AOF
日志记录的是内存数据修改的指令记录文本。AOF
日志在长期的运行过程中会 变的无比庞大,数据库重启时需要加载 AOF
日志进行指令重放,这个时间就会无比漫长。 所以需要定期进行 AOF
重写,给 AOF
日志进行瘦身
2、RDB
RDB
持久化是将当前进程数据生成快照保存到硬盘的过程
RDB
持久化的过程分为手动触发和自动触发
1.1、快照原理
问题1:Redis是单线程程序在RDB
快照持久化方面有什么困难?
答案: Redis
是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写。在服务线上请求的同时,Redis
还需要进行内存快照,内存快照要求 Redis
必须进行文件 IO
操作,可文件 IO
操作是不能使用多路复用 API
。
问题2:除了不能使用多路复用还有什么困难因素呢?
答案:RDB
快照这意味着单线程同时在服务线上的请求还要进行文件 IO
操作,文件 IO
操作会严重拖垮服务器请求的性能。还有个重要的问题是为了不阻塞线上的业务,就需要边持久化边响应客户端请求。持久化的同时,内存数据结构还在改变,比如一个大型的 hash
字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完呢,这尼玛要怎么搞?
问题3:怎么解决上面提到的Hash
字段正在持久化,还没持久化完成,就给删掉的问题呢?
答案:Redis
使用操作系统的多进程 COW
(Copy On Write
) 机制来实现快照持久化,这个机制 很有意思,也很少人知道。多进程 COW
也是鉴定程序员知识广度的一个重要指标。
问题4:如果上面提到的Hash
在RDS
快照持久化之后删掉了,那是丢数据了吗?
答案:是的,因为RDS
是快照,只能持久化那一时刻的数据,如果子进程持久化中,父进程将数据修改了,子进程是感知不到的,这个时间宕机了,会存在脏的Hash
数据。同样,如果是父进程添加缓存,如果宕机了,也会出现RDS
快照文件缓存没有持久化导致丢失数据。所以,可以很明显的看到,RDB有它的不足,就是一旦出现问题,那么我们的RDB
文件中保存的数据并不是全新的。
1.2.1、fork
(多进程)
Redis
在持久化时会调用glibc
的函数fork
产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这时你可以将父子进程想像成一个连体婴儿,共享身体。这是
Linux
操作系统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的增长几乎没有明显变化。
问题1:描述一下这种fork
操作?
答案:用 Python
语言描述进程分离的逻辑如下。fork
函数会在父子进程同时返回,在父进程 里返回子进程的 pid
,在子进程里返回零。如果操作系统内存资源不足,pid
就会是负数,表 示 fork
失败。
pid = os.fork()
if pid > 0:
handle_client_requests() # 父进程继续处理客户端请求
if pid == 0:
handle_snapshot_write() # 子进程处理快照写磁盘
if pid < 0:
# fork error
问题2:父子进程怎么分工工作的的?
答案:子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。
这个时候就会使用操作系统的 COW
机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成 当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的, 还是进程产生时那一瞬间的数据,子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis
的持久化叫「快照」的原因。接下来子进程就可以非常安 心的遍历数据了进行序列化写磁盘了。
问题3:父进程一直复制内存,内存会一直增长吗?
答案:随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的 2
倍大小。另外一个 Redis
实例里冷数据占的比例 往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页 面。每个页面的大小只有 4K
,一个 Redis
实例里面一般都会有成千上万的页面。
1.1.3、bgsave
命令流程
1、执行bgsave
命令,Redis
父进程判断当前是否存在正在执行的子进程,如RDB
/AOF
子进程,如果存在bgsave
命令直接返回。
2、父进程执行fork
操作创建子进程,fork
操作过程中父进程会阻塞,通过info stats
命令查看latest_fork_usec
选项,可以获取最近一个fork
操作的耗时,单位为微秒。
3、父进程fork
完成后,bgsave
命令返,“Background saving started
信息并不再阻塞父进程,可以继续响应其他命令
4、子进程创建RDB
文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave
命令可以获取最后一次生成RDB
的时间,对应info
统计的rdb_last_save_time
选项。
127.0.0.1:6379> lastsave
(integer) 1523876908
127.0.0.1:6379>
5、进程发送信号给父进程表示完成,父进程更新统计信息,具体见info Persistence
下的rdb_*
相关选项
1.2、触发机制
1.2.1、手动触发
1.2.1.1、save
命令(已经被丢弃)
save
命令,阻塞当前的Redis
服务器,直到RDB
过程完成为止,对于内存比较大的实例会造成长时间阻塞。线上环境不建议使用,save
命令已经被丢弃。运行
save
命令Redis
日志如下:
DB saved on disk
1.2.1.2、bgsave
命令
Redis
进程执行fork
操作创建子进程,RDB
持久化的过程由子进程负责,完成后自动结束,阻塞只发送在fork
阶段,一般时间很短。因此Redis
内部所有涉及RDB
的操作都是bgsave
命令的方式,save
命令已经被丢弃。
Background saving started
1.2.2、自动触发时机
1.2.2.1、执行shutdown
命令的时候
执行
shutdown
命令的时候,如果没有开启AOF
持久化,则自动执行bgsave
1.2.2.2、执行debug relod
执行
debug relod
命令的时候也会自动触发save
操作
1.2.2.3、从节点执行全量复制时
如果从节点执行全量复制操作,主节点自动执行
bgsave
生成RDB
文件并发送给从节点
1.2.2.4、配置自动触发
自动触发
bgsave
,使用save
相关配置,如“save m n
”,表示m
秒内数据集存在n
次修改时,
#Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
1.3、RDB
文件存储位置
RDB
文件保存在dir
配置指定的目录下,文件名通过dbfilename
配置指定。可以通过执行
config set dir{newDir}
和config set dbfilename{newFileName}
运行期动态执行,当下次运行时RDB
文件会保存到新目录。
127.0.0.1:6379> config set dir /usr/local/redis-4.0.8/myrdb
OK
127.0.0.1:6379> config set dbfilename myrdb.rdb
OK
127.0.0.1:6379> bgsave
Background saving started
127.0.0.1:6379>
1.4、RDB
的优缺点
优点:
1、RDB
是一个紧凑压缩的二进制文件,代表某个时间点的数据快照,远远小于内存大小,非常适用于备份,全量复制等场景,比如每6个小时执行bgsave
备份,并把RDB
文件拷贝到远程机器等位置,用于灾难恢复
2、Redis
加载RDB
恢复数据比AOF
快多了
缺点:
1、RDB
无法做到实时持久化/秒级持久化(无法做到秒级,这样一旦宕机,在持久化期间数据的修改就会丢失)
2、每次bgsave
都要执行fork
操作创建子进程,属于重量级操作,频繁执行成本过高
3、AOF
3.1、AOF
原理
问题1:有了RDB
为什么会出现AOF
?
答案:AOF
(append only file
)持久化,AOF
主要就是解决数据持久化的实时性,RDB
快照无法做到秒级持久化。
问题2:AOF
是如何实现秒级持久化的?
答案: AOF
日志存储的是 Redis
服务器的顺序指令序列,AOF
日志只记录对内存进行修改的 指令记录。重启时再重新执行AOF
文件中的命令达到数据恢复的目的
问题3:AOF
怎么在重启的时候恢复数据?
答案:假设 AOF
日志记录了自 Redis
实例创建以来所有的修改性指令序列,那么就可以通过 对一个空的 Redis
实例顺序执行所有的指令,也就是「重放」,来恢复 Redis
当前实例的内 存数据结构的状态。
问题4:AOF
是如何保存命令和恢复数据的?
答案:Redis
会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文本存储到 AOF
日志中,也就是先存到磁盘,然后再执行指令。这样即使遇到突发宕机,已经存储到 AOF
日志的指令进行重放一下就可以恢复到宕机前的状态。
问题5:一直往AOF的日志文件中追加指令,内存变得很大了怎么处理?
答案:Redis
在长期运行的过程中,AOF
的日志会越变越长。如果实例宕机重启,重放整个 AOF
日志会非常耗时,导致长时间 Redis
无法对外提供服务。所以需要对 AOF
日志瘦 身。
3.2、开启AOF
功能(默认是关闭)
开启
AOF
功能需要进行配置,默认是不开启的,appendonly yes
,观察redis.conf
文件,默认文件名appendonly.aof
# 指定 AOF 文件名
appendfilename appendonly.aof
#修改为守护模式
daemonize yes
#设置进程锁文件
pidfile /usr/local/redis-4.0.8/redis.pid
#端口
port 6379
#客户端超时时间
timeout 300
#日志级别
loglevel debug
#日志文件位置
logfile /usr/local/redis-4.0.8/log-redis.log
#设置数据库的数量,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id
databases 16
##指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
#save <seconds> <changes>
#Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
#指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,
#可以关闭该#选项,但会导致数据库文件变的巨大
rdbcompression yes
#指定本地数据库文件名
dbfilename dump.rdb
#指定本地数据库路径
dir /usr/local/redis-4.0.8/db/
#指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能
#会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有
#的数据会在一段时间内只存在于内存中
appendonly no
#指定更新日志条件,共有3个可选值:
#no:表示等操作系统进行数据缓存同步到磁盘(快)
#always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
#everysec:表示每秒同步一次(折衷,默认值)
appendfsync everysec
3.2.1、命令写入AOF
日志流程
1、所有写命令会被追加aof_buf
(缓冲区)中
2、AOF
缓冲区根据对应的策略向硬盘做同步操作
3、随着AOF
文件越来越大,需要定期对AOF
文件进行重写,达到压缩的目的
4、当redis
服务器进行重启时,可以加载AOF
文件进行数据恢复
3.2.2、为什么要用文本协议格式
AOF命令写入的内容直接是文本协议格式,例如
set hello world
这条命令,在AOF
缓存中会追加如下文本
1、文本协议有很好的兼容性。可读性,,方便直接修改和处理
2、开启AOF
后,所有写入命令都包含追加操作,直接采用文本协议格式,避免了字符转换带来的二次开销。
3.2.3、AOF
为什么会把命令放到aoc_buf
缓存中
1、Redis
使用单线程响应命令,如果每次AOF
文件命令都直接追加到硬盘,也就是说对文件进行IO
操作,而对文件IO
操作不支持多路复用的,文件 IO
操作会严重拖垮服务器请求的性能
2、Redis
提供了多种缓冲区到硬盘的同步的策略。在性能和安全性方面做出平衡
3.3、AOF
重写机制
随着
AOF
文件越来越大Redis
引入AOF
重写机制,压缩文件体积。AOF
文件重写就是讲Redis
进程内的数据,转化为写命令同步到新的AOF
文件的过程
3.2.1、瘦身原理:
Redis
提供了bgrewriteaof
指令用于对AOF
日志进行瘦身。其原理就是1、开辟一个子进程对内存进行遍历转换成一系列
Redis
的操作指令,序列化到一个新的AOF
日志文件中(个人感觉有点类似于快照)。2、序列化完毕后再将操作期间发生的增量
AOF
日志追加到这个新的AOF
日志文件中3、追加 完毕后就立即替代旧的
AOF
日志文件了,瘦身工作就完成了。
3.3.2、重写后的AOF
文件为什么可以变小
1、进程内已经超时的数据不再写入文件
2、旧的AOF
文件含有无效命令,如del key1
,hdel key2
等,重写使用进程内数据直接生成,这样新的AOF
文件只保留最终数据的写入命令,不会包含这些无用的命令。
3、多条写命令可以合并成一个,如:lpush list a
,lpush list b
,转化为 lpush list a b
,同时防止单挑命令多大造成客户端缓冲区溢出。
1.3.3、AOF重写过程的触发
1.3.3.1、手动触发:bgerwriteaof
命令
直接调用
bgerwriteaof
命令,不管有没有开启AOF
都会执行
127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started
127.0.0.1:6379>
1.3.3.2、自动触发:达到阈值
设置参数值 服务器在
AOF
功能开启的情况下,会维持以下三个变量:1、记录最后一次
AOF
重写之后,AOF
文件大小的变量aof_rewrite_base_size
。2、记录当前
AOF
文件大小的变量aof_current_size
。3、增长百分比变量
aof_rewrite_perc
。
以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:
1、没有bgsave
命令(RDB
持久化)/AOF
持久化在执行;
2、没有bgrewriteaof
命令在进行;
3、当前AOF
文件大小要大于server.aof_rewrite_min_size
(默认为1MB
),或者在redis.conf
配置了auto-aof-rewrite-min-size
大小。当前AOF
文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage
参数,不设置默认为 100%
)
3.4、fsync
AOF
日志是以文件的形式存在的,当程序对AOF
日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将数据刷回到磁盘 的。
问题1:这就意味着如果机器突然宕机,AOF
日志内容可能还没有来得及完全刷到磁盘中,这个 时候就会出现日志丢失。那该怎么办?
答案:Linux
的 glibc
提供了 fsync(int fd)
函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要 Redis
进程实时调用 fsync
函数就可以保证 aof
日志不丢失。
问题2:真的要实时调用fsync
,会有什么问题吗?
答案:是 fsync
是一个 磁盘 IO
操作,它很慢!,AOF
的 fsync
是一个耗时的 IO
操作,它会降低 Redis
性能,同时也会增加系
统 IO
负担。如果 Redis
执行一条指令就要 fsync
一次,那么 Redis
高性能的 地位就不保了。
问题3:你说的要实时,现在又不能每次执行执行就调用一次,那该怎么调用fsync
这个函数呢?
答案:生产环境的服务器中,Redis
通常是每隔 1s
左右执行一次 fsync
操作,周期 1s
是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能使得数据少丢失,是建议的同步策略,也是默认配置,做到监控性能,理论上只有在系统死机的情况下丢失1秒的数据。
问题4:除了fsync
,还有其他策略吗?
答案:Redis
同样也提供了另外两种策略
1、永不 fsync
——让操作系统来决定合适同步磁盘,很不安全
2、来一个指令就 fsync
一次——非常慢。但是在生产环境基本不会使 用,了解一下即可。
3.4.1、缓冲区到硬盘的同步的策略
Redis
提供了AOF
多种缓冲区同步文件策略,由参数appendfsync
控制,不同值的含义如下
可配置值 | 说明 |
---|---|
always : |
来一个指令就 fsync 一次(慢,安全) |
everysec : |
每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置(折衷,默认值) |
no : |
永不 fsync ——让操作系统来决定合适同步磁盘(快,不安全) |
4、重启选择RDB
文件还是AOF
文件
AOF
和RDB
文件都可以用于服务器重启时的数据恢复
1、AOF
持久化开启且存在AOF
文件的时,优先加载AOF
文件,打印如下日志:
* DB loaded from append only file : 5.841 seconds
2、AOF
关闭或者 AOF
持久化虽然开启但是AOF
文件不存在的时候,加载RDB
文件,打印如下日志
* DB loaded from disk : 5586 seconds
3、加载 AOF
或者RDB
成功之后,Redis
启动成功,如果AOF
或者RDB
文件存在错误,则Redis
启动失败并报错
5、运维
快照是通过开启子进程的方式进行的,它是一个比较耗资源的操作。 运维难度
1、遍历整个内存,大块写磁盘会加重系统负载
2、
AOF
的fsync
是一个耗时的IO
操作,它会降低Redis
性能,同时也会增加系统IO
负担
1、持久化在从节点执行:通常 Redis
的主节点是不会进行持久化操作,持久化操作主要在从节点进行。从节点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛。
2、做好实时监控和多加几个从节点,防止从节点故障:如果出现网络分区,从节点长期连不上主节点,就会出现数据不一致的问题,特别是在网络分区出现的情况下又不小心主节点宕机了,那么数据就会丢失,所以在生产环境要做好实时监控工作,保证网络畅通或者能快速修复。另外还应该再增加一个从节点以降低网络分区的概率,只要有一个从节点数据同步正常,数据也就不会轻易丢失
6、Redis 4.0
混合持久化
问题1:为什么用混合的持久化呢?
答案:之前的版本中,重启 Redis
时,我们很少使用 rdb
来恢复内存状态,因为会丢失大量数据。我们通常 使用 AOF
日志重放,但是重放 AOF
日志性能相对 rdb
来说要慢很多,这样在 Redis
实 例很大的情况下,启动需要花费很长的时间。
问题2:Redis 4.0
是怎么解决的?
答案:Redis 4.0
为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb
文 件的内容和增量的 AOF
日志文件存在一起。这里的 AOF
日志不再是全量的日志,而是自 RDB
持久化开始到持久化结束的这段时间发生的增量 AOF
日志,通常这部分 AOF
日志很小。
于是在 Redis
重启的时候,可以先加载 rdb
的内容,然后再重放增量 AOF
日志就可 以完全替代之前的 AOF
全量文件重放,重启效率因此大幅得到提升。