Redis 持久化,超详细吐血总结(7)

我们熟知redis是内存数据库,它将自己的数据存储在内存里面,如果如图redis进程退出或突然宕机,数据就会全部丢失,因此必须有一种机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。

Redis 的持久化机制有三种,第一种是快照(RDB),第二种是 AOF 日志,第三种是混合持久化。快照是一次全量备份,AOF 日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本。混合持久化综合了上面两种的优点。

RDB原理

RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发

触发机制

  1. save命令:阻塞当前redis服务器,直RDB过程完成为止。(如果内存比较大会造成redis长时间阻塞,这样显然不是我们想要的。线上禁止使用)
  2. bgsave命令:redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般很短(和实例数据大小有关系)

除了执行命令手动触发之外,redis内部还存在自动触发RDB的持久化机制,如下

  1. 使用save相关配置,如“save m n” 表示m秒内数据集存在n次修改时(可以配置多组条件,其中一个达标就触发),自动触发bgsave
  2. 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点(具体复制细节见下一篇)
  3. 执行debug reload命令重写加载redis时,
  4. 默认情况下执行shutdown命令时,如果没有开启aof持久化则自动执行bgsave

bgsvae运作流程如下

  1. 执行bgsave命令,redis父进程判断是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回(生成RDB、AOF要浪费大量磁盘io资源,如果开启aof,磁盘io有可能成为redis的瓶颈)
  2. 父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过info stats 命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时(单位ms)(具体阻塞时间和内存大小有关)
  3. 父进程fork完成后,bgsave命令返回“Background saving started”信息并不在阻塞父进程,子进程创建RDB文件,根据父进程内存生成的临时快照文件,完成后对原有的文件进行院子替换。执行lastsave命令可以获取最后一次生成RDB的时间
  4. 子进程发送信号给父进程表示完成,父进程更新统计信息

至此RDB的生成过程大概讲完了,我们知道Redis为了不阻塞主线程,调用 glibc 的函数fork产生一个子进程来生成RDB备份文件,试想一个问题,如果一个大型的 hash 字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完呢,这尼玛要怎么搞??

Copy On Write

Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段.这是 Linux 操作系统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的增长几乎没有明显变化。子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。

这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成(一页4k),当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。

子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。

RDB的载入

和使用save或者bgsave命令不同,RDB的载入是在服务器启动的时候自动执行的,所以Redis并没有专门用于载入RDB的文件命令。值得一提的是:

  1. 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库
  2. 数据库在主从复制时候会触发RDB加载(下一篇细聊)

RDB文件的处理

保存:RDB文件保存在dir配置的指定目录下,文件名通过dbfilename配置指定。可以通过执行 config set dir{new Dir} 和 config set dbfilename{newFileName} 运行期动态执行。

压缩:redis 默认采用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远小于内存大小,默认开启,可以通过参数config set rdbcompression{yes|no}动态修改

RDB的优缺点

优点:

  1. RDB是一个紧凑压缩的二进制文件,某个时间点的上的快照。适合全量复制
  2. redis加载RDB恢复数据远快于AOF的方式
  3. 如果Redis加载损坏的RDB文件时拒绝启动,并打印如下日志:# Short read or OOM loading DB. Unrecoverable error, aborting now.    这时可以使用Redis提供的redis-check-dump工具检测RDB文件并获取对应的错误报告。

缺点:

  1. RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
  2. RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。

针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决。


AOF原理

AOF的主要作用是解决了数据持久化的实时性,AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。

Redis 会在收到客户端修改指令后,进行参数校验进行逻辑处理后,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先执行指令才将日志存盘。这点不同于mysql、hbase等存储引擎,它们都是先存储日志再做逻辑处理。(所以单机redis不能做到一条数据也不丢失)

使用AOF

开启AOF功能需要设置配置:appendonly yes,默认不开启。AOF文件名通过appendfilename配置设置,默认文件名是appendonly.aof。保存路径同RDB持久化方式一致,通过dir配置指定。AOF的工作流程操作:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)。

命令写入and文件同步

服务器在执行完一个写命令后(如 set k v,lpush k v 等),会把写入命令会追加到aof_buf(缓冲区)中。后续防止丢失aof_buf中的数据,在调用linux的glibc提供的fsync函数将aof_buf中的数据强制刷新到磁盘。

AOF为什么把命令追加到aof_buf中?Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘(调用fsync),那么性能完全取决于当前硬盘负载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。

Redis提供了多种AOF缓冲区同步文件策略,由参数appendfsync控制,不同值的含义如下

  1. always: 命令写入aof_buf后调用系统fsync操作同步到aof文件,fsync完成后线程返回(性能最差,完全取决于磁盘速度。即便如此redis也不能保证一条数据也不丢)
  2. everysec: 命令写入aof_buf后调用系统write操作,write完成后线程返回。fsync同步文件操作由专门的线程每秒调用一次(默认配置。理论上会丢失1s的数据。(严格来说丢弃1s的数据不准确,下文有讲))
  3. no:命令写入aof_bug后调用系统的write操作,不对AOF文件做fsync同步,同步硬盘操作由操作系统负责,通常同步周期30s

系统调用write和fsync说明:

  1. write :Linux在内核提供页缓冲区用来提高硬盘IO性能。write操作在写入系统缓冲区后直接返回。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
  2. fsync : 将指定文件的内容强制从内核缓存刷到磁盘

AOF重写机制

我们知道AOF持久化是通过保存被执行的写命令来实现的,随着命令不断写入AOF,文件会越来越大。为了解决这个问题,Redis引入AOF重写机制压缩文件体积。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF

重写后为什么变小?

  1. 已经过期的数据不再写入文件。
  2. 旧的AOF文件含有无效命令,如set key1、del key1 。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。
  3. 多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条其实这里直接遍历的是当前内存数据,如直接把一个大个的list的生成一个个lpush list a1 a2 ..a60 ,步子大了容易扯着蛋,所以默认以64个元素为一组
  4. 更小的AOF文件可以被Redis更快的加载

AOF重写触发条件

手动触发:直接调用bgrewriteaof命令。

自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机。

  1. auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB。
  2. auto-aof-rewrite-percentage(设为x):代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的百分比如auto-aof-rewrite-percentage 100 当为两倍大小是重写

自动触发条件:aof_current_size > auto-aof-rewrite-min-size && (x+1)*aof_base_size

其中aof_current_size和aof_base_size可以在info Persistence统计信息中查看。

AOF重写Redis做了哪些事情呢?

AOF重写功能作为一个辅助功能,redis肯定不希望阻塞主进程的执行,所以redis把AOF重写放到一个子进程去执行。上文讲到frok运用cow(写时复制技术)技术,所以子进程只能看到fork那一瞬间产生的镜像数据。为了解决这一个问题redis设置了一个 AOF重写缓存区(aof_rewrite_bug) 用来存储AOF重写期间产生的命令,等子进程重写完成后通知父进程,父进程把重写缓存区的数据追加到新的AOF文件(注:这里值得注意,AOF重写期间如果有大量的写入,父进程在把aof_rewrite_buf写到新的aof文件时会造成大量的写盘操作,会造成性能的下降,redis 4.0以后增加管道机制来优化这里(把aof_rewrite_buf追加工作交给子进程去做),感兴趣的同学可以自行查阅)

所以说在AOF重写期间主进程做了下面几件事

  1. 执行客户端发来的命令
  2. 将执行后的写命令追加到AOF缓冲区
  3. 将执行后的写明了追加到AOF重写缓冲区

值得注意的是,redis每次重写写入磁盘的大小由aof-rewrite-incremental-fsync控制,默认为32MB,防止单次刷盘数据过多造成硬盘阻塞

过程如图:

我们知道AOF重写期间会消耗大量磁盘IO,可以开启配置no-appendfsync-on-rewrite yes,表示在AOF重写期间不做fsync操作,默认为no。但是如此一来某些情况下会丢掉重写期间的所有数据。慎重啊,铁子

重启加载

讲RDB时提到,如果开启AOF优先加载AOF文件,否则执行RDB文件


AOF追加阻塞

当开启AOF持久化时,默认以及常用的同步硬盘策略是everysec(每s一刷),对于这种方式,Redis使用另一个线程每s执行fsync同步磁盘。试想一个问题,假设硬盘资源繁忙,fsync刷盘缓慢,主线程该如何做?

主线程写入AOF缓冲区后会对比上次AOF同步时间

  1. 如果距上次同步成功时间在2S内,主线程直接返回
  2. 如果距上次同步成功时间超过2s,主线程阻塞,直到同步操作完成

发现两个问题:

  1. everysec配置最多可能丢失2s数据,不是1s
  2. 如果系统fsync慢会阻塞主线程

RDB-AOF混合持久化(redis 4.0+提供)

细细想来aofrewrite时也是先写一份全量数据到新AOF文件中再追加增量只不过全量数据是以redis命令的格式写入。那么是否可以先以RDB格式写入全量数据再追加增量日志呢这样既可以提高aofrewrite和恢复速度也可以减少文件大小还可以保证数据的完毕性整合RDB和AOF的优点那么现在4.0实现了这一特性——RDB-AOF混合持久化。

综上所述RDB-AOF混合持久化体现在aofrewrite时,即在AOF重写时把frok的那个镜像写成RDB,后续AOF重写缓冲里的数据继续追加到该文件中。

 配置为 aof-use-rdb-preamble no  #默认关闭,yes 打开


写在最后:

小结

  1. redis提供两种持久化方式:RDB和aof
  2. RDB使用一次性生成内存快照,产生的文件为二进制序列,非常紧凑,因此加载更快。但是由于其为快照,所以不能做到实时持久化,一般用于冷备和复制传输
  3. save命令会阻塞主线程不建议使用,bgsave通过fork创建子进程生成RDB避免阻塞
  4. AOF通过追加写命令到文件实现持久化,因为需要不断追加写命令,所以AOF需要定期执行重写来降低文件体积
  5. 如果写命令直接写入磁盘势必会造成性能过低,所以redis提供了一个AOF缓冲区。写命令写入到AOF缓冲区后续在调用fsync异步刷盘
  6. AOF子进程执行期间使用copy-on-write机制和父进程共享内存,但是该技术只能看到fork那一瞬间内存的快照,所以需要一个AOF重写缓冲区,保存新的写入命令避免数据丢失
  7. 持久化阻塞主线程的场景有:fork阻塞和AOF追加阻塞,fork阻塞时间跟内存和系统有关,AOF追加阻塞说明磁盘资源紧张

文章涉及到的命令及配置

  1. 命令 :手动备份RDB:save(阻塞) 和 bgsave(fork子进程)
  2. 配置: save m n 表示m秒内数据集存在n次修改时(可以配置多组条件,其中一个达标就触发),自动触发bgsave
  3. 命令 :debug reload  重新加载redis
  4. 日志文件名配置 : 文件保存在dir配置的指定目录下,文件名通过dbfilename配置指定。可以通过执行 config set dir{new Dir} 和 config set dbfilename{newFileName} 运行期动态执行。
  5. 开启aof: appendonly yes
  6.  appendfsync  always|everysec|no : AOF缓冲区同步磁盘,always:总是同步,everysec:1s刷新,no:交给系统(大概30s)
  7. 命令 : bgrewriteaof :手动重写AOF
  8. 配置:auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB。
  9. 配置:auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的百分比(如auto-aof-rewrite-percentage 100 当为两倍大小是重写)。
  10. 配置:aof-rewrite-incremental-fsync:AOF每次重写一次刷盘大小,默认为32MB
  11. 配置:no-appendfsync-on-rewrite :AOF重写期间是否刷新AOF缓冲区,默认为no(刷新)
原创文章 15 获赞 22 访问量 6934

猜你喜欢

转载自blog.csdn.net/qq_31387317/article/details/95315166