Redis 持久化
Redis持久化主要两大机制,AOF日志和RDB快照。
AOF日志
特点
AOF日志是写后日志,先执行完命令然后在写入内存,然后才记录日志。是因为先让系统执行命令,只有命令执行成功才会被写到日志中。这样避免出现记录错误命令。
AOF还有一个好处: 它是命令执行后才记录日志,不会阻塞当前的写操作。
两个潜在风险点:
- 刚执行完命令,没有记日志就宕机,这个命令就会有丢失风险
- AOF避免当前命令阻塞,但是会给下个操作带来阻塞风险。AOF日志也是在主线程执行的。
三种写回策略
AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendfsync 的三个可选值。
- Always,同步写回:每个命令执行完,立马写回将日志写回磁盘;
- Everysec,美妙写回:每个命令执行完,只是先把日志写到AOF日志文件的内存缓冲区,每隔一秒把缓冲区的内容写入磁盘;
- NO,操作系统控制写回:每个命令执行完,只是先把日志写到AOF日志文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
针对避免主线程阻塞和减少数据丢失问题,这三种写回策略都无法做到两全其美。我们来分析下其中的原因。
- “同步写回”可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能;
- 虽然“操作系统控制的写回”在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在 Redis 手中了,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了;
- “每秒写回”采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中。
总结一下就是:想要获得高性能,就选择 No 策略;如果想要得到高可靠性保证,就选择 Always 策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择 Everysec 策略。
AOF重写机制
解决问题
主要解决AOF文件太大问题。AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。
特性
重写机制具有“多变一”功能。所谓的“多变一”,也就是说,旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。是根据这个键值对当前的最新状态,为它生成对应的写入命令。
AOF重写会阻塞线程吗?
AOF重写不会阻塞线程,AOF重写是由后台的bgrewriteaof子进程完成的,不会阻塞主线程。
因为记录重写日志并不会阻塞主线程,所以会出现记录重写日志时主线又有新的写操作重写日志没有办法记录。所以使用一处拷贝,两处日志的操作。
一处拷贝
每次执行重写时候,主线程fork出后台bgrewriteaof子进程(会阻塞主线程),fork会把主线程内存(包含最新数据)copy一份给bgrewriteaof子进程,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
两处日志
两处日志是指的AOF主线程正在使用AOF日志和新的AOF重写日志。
AOF日志
有新来的操作时Redis会把最新的操作记录在主线程的AOF日志中,这样保证宕机可以恢复到最新的状态
新的AOF重写日志
新来操作写入主线程的AOF日志时也会在写到重写日志的缓冲区,这样重写日志也不会丢失最新的操作。等拷贝数据的所有操作记录重新完成后,重写日志记录这些最新的操作记录也会写到新的AOF日志文件,以保证数据库最新的状态。然后我们就用最新的AOF日志文件替代旧的文件。
Q&A
AOF 日志重写的时候,是由 bgrewriteaof 子进程来完成的,不用主线程参与,我们今天说的非阻塞也是指子进程的执行不阻塞主线程。但是,你觉得,这个重写过程有没有其他潜在的阻塞风险呢?如果有的话,会在哪里阻塞?
Redis采用fork子进程重写AOF文件时,潜在的阻塞风险包括:fork子进程 和 AOF重写过程中父进程产生写入的场景:
- fork子进程这个瞬间一定会阻塞主线程。fork采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成长时间阻塞问题,但是fork子进程需要拷贝必要的数据结构,其中就有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝操作会大量消耗cpu资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。那什么时候父子进程才会真正内存分离呢?“写实复制”顾名思义,就是在写发生时,才真正拷贝内存真正的数据,这个过程中,父进程也可能会产生阻塞的风险,就是下面介绍的场景。
- fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。因为内存分配是以页为单位进行分配的,默认4k,如果父进程此时操作的是一个bigkey,重新申请大块内存耗时会变长,可能会产阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page,页面大小2M),那么父进程申请内存时阻塞的概率将会大大提高,所以在Redis机器上需要关闭Huge Page机制。Redis每次fork生成RDB或AOF重写完成后,都可以在Redis log中看到父进程重新申请了多大的内存空间。
AOF 重写也有一个重写日志,为什么它不共享使用 AOF 本身的日志呢?
AOF重写不复用AOF本身的日志,一个原因是父子进程写同一个文件必然会产生竞争问题,控制竞争就意味着会影响父进程的性能。二是如果AOF重写过程中失败了,那么原本的AOF文件相当于被污染了,无法做恢复使用。所以Redis AOF重写一个新文件,重写失败的话,直接删除这个文件就好了,不会对原先的AOF文件产生影响。等重写完成之后,直接替换旧文件即可
缺点
AOF日志存储是操作命令,并不是实际的数据。用AOF日志进行故障恢复的时候,需要把操作日志都执行一遍,日志非常多就会导致Redis恢复很慢。
内存快照-RDB
简介
RDB是Redis DataBase的缩写。内存快照就是指的内存中的数据在某一个时刻的状态以文件的形式写到磁盘上。即使宕机,快照也不会丢失,数据的可靠性也得倒保证。在做数据恢复的时候就直接把RDB文件读入到内存,就可以很快的完成恢复。
操作
全量快照: RDB 快照是将内存中的所有数据写入到硬盘。
Redis 提供两个命令生成RDB文件,分别是save和bgsave
- save: 在主线程中执行,会导致阻塞
- bgsave: 创建一个子进程(fork主线程),专门用于写RDB文件,避免阻塞主线程,也是Redis RDB文件生成的默认配置。
bgsave子进程在被fork处理时,与主进程共享同一份内存数据。但bgsave生成快照的时候采用COW机制,确保不会阻塞主线程的数据写入。
RBD频繁写入全量快照也会导致性能问题。(Redis只允许有一个bgsave线程)
优化方案
Redis 4.0提出混合使用AOF日志和内存快照方式。内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
关于 AOF 和 RDB 的选择问题,我想再给你提三点建议:
- 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
- 如果允许分钟级别的数据丢失,可以只使用 RDB;
- 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。