【Redis学习】2.持久化


1 前言

Redis支持RDBAOF两种持久化机制,持久化功能有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数据恢复。理解掌握持久化机制对于Redis运维非常重要。

2 AOF

AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。

AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。

注意只会记录写操作命令,读操作命令是不会被记录的,因为没意义。

2.1 配置

默认情况,AOF是关闭的,若要开启,需要配置参数,如下所示:

appendonly yes //开启AOF持久化,默认为no,不开启AOF
appendfilename appendonly.aof //配置AOF持久化文件名,默认为appendonly.aof

文件保存路径也可以自定义,默认在Redis服务的主目录./

# 文件保存路径
dir /home/work/app/redis/data/

2.2 流程

Redis是“写后”日志,Redis先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。

先写数据至内存,再写日志至磁盘

这种先写数据至内存,再写日志至磁盘写后日志的方式优点有:

  • 避免额外的检查开销:若先写日志,Redis并不会对这些命令进行语法检查,保存下来的可能是错误的命令,那在恢复过程中就会出问题
  • 不会阻塞当前写操作命令的执行

缺点:

  • 如果命令执行完成,写日志之前宕机了,会丢失数据。
  • 主线程写磁盘压力大,导致写盘慢,阻塞后续操作。

    因为将命令写入到日志的这个操作也是在主进程完成的(执行命令也是在主进程),也就是说这两个操作是同步的

AOF方式涉及的流程包括:命令追加、文件写入与同步、文件重写、重启加载、文件校验。

AOF流程

2.2.1 命令追加

当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以文本协议格式将被执行的写命令追加到服务器的server.aof_buf缓冲区。

文本协议格式源于Redis制定的RESP(REdis Serialization Protocol,Redis序列化协议)[^1],其实现了客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。

例如客户端发送一条set hello world命令给服务端,按照RESP的标准,客户端需要将其封装为如下格式(每行用\r\n分隔):

*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n

实际显示效果可以如下:

*3
$3
SET
$5
hello
$5
world

各行解释可按系列方式对号入座:

*<参数数量> CRLF
$<参数1的字节数量> CRLF
<参数1> CRLF
...
$<参数N的字节数量> CRLF
<参数N> CRLF
  • *3表示当前命令有三个部分
  • $+数字$分隔每个部分、数字表示每个部分的字节数;
  • 紧跟着的为具体的命令,比如SET(命令)、hello(键)、world(值)。

1)AOF为什么直接采用文本协议格式?
可能的理由如下:

  • 文本协议具有很好的兼容性。
  • 开启AOF后,所有写入命令都包含追加操作,直接采用协议格式,避免了二次处理开销。
  • 文本协议具有可读性,方便直接修改和处理。

2)AOF为什么把命令追加到aof_buf中?

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

2.2.2 文件写入与同步

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

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec,每秒写回,默认值:每个写命令执行完,只是先把日志写到AOF文件的内核缓冲区每隔一秒把缓冲区中的内容写入磁盘;
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内核缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

这 3 种写回策略都无法能完美解决主进程阻塞减少数据丢失的问题,因为两个问题是对立的,偏向于一边的话,就会要牺牲另外一边,原因如下:

  • Always 策略的话,可以最大程度保证数据不丢失,但是由于它每执行一条写操作命令就同步将 AOF 内容写回硬盘,所以是不可避免会影响主进程的性能;
  • No 策略的话,是交由操作系统来决定何时将 AOF 日志内容写回硬盘,相比于 Always 策略性能较好,但是操作系统写回硬盘的时机是不可预知的,如果 AOF 日志内容没有写回硬盘,一旦服务器宕机,就会丢失不定数量的数据。
  • Everysec 策略的话,是折中的一种方式,避免了 Always 策略的性能开销,也比 No 策略更能避免数据丢失,当然如果上一秒的写操作命令日志没有写回到硬盘,发生了宕机,这一秒内的数据自然也会丢失。

大家根据自己的业务场景进行选择:

  • 如果要高性能,就选择 No 策略;
  • 如果要高可靠,就选择 Always 策略;
  • 如果允许数据丢失一点,但又想性能高,就选择 Everysec 策略。
写入策略对比

关于AOF的同步策略是涉及到操作系统的 write 函数和 fsync 函数的。

为了提高文件的写入效率, 在现代操作系统中, 当用户调用 write 函数, 将一些数据写入到文件的时候, 操作系统通常会将写入数据暂时保存在一个内存缓冲区里面, 等到缓冲区的空间被填满、或者超过了指定的时限之后, 才真正地将缓冲区中的数据写入到磁盘里面。

这种做法虽然提高了效率, 但也为写入数据带来了安全问题, 因为如果计算机发生停机, 那么保存在内存缓冲区里面的写入数据将会丢失。

为此,系统提供了 fsyncfdatasync 两个同步函数, 它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面, 从而确保写入数据的安全性。

AOF写入的过程可总结为如下图:

AOF写入过程

2.2.3 文件重写[^2][^3]

随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis引入AOF重写机制压缩文件体积。

AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。

这样做的好处主要有两方面:

  • 节省存储空间
  • 更快地加载

为啥AOF文件大小能够压缩、变小。原因如下:

  • 超时的数据不必再写入文件
  • 旧的AOF文件可能还有无效的命令,如del key1、hdel key2、srem keys、set a111、set a222等。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。
  • 多条命令可以合并。如lpush list alpush list blpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出,对于listsethashzset等类型操作,以64个元素为界拆分为多条。

重写过程的简化流程如下图所示:

Redis重写流程
  • 1)执行AOF重写请求,如果当前进程正在执行AOF重写,请求不执行;如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再执行;
  • 2)父进程执行fork创建子进程,开销等同于bgsave过程;
  • 3.1)主进程fork操作完成后,继续响应其他命令。所有修改命令依然写入AOF缓冲区并根据appendfsync策略同步到硬盘,保证原有AOF机制正确性;
  • 3.2)由于fork操作运用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然响应命令,Redis使用AOF重写缓冲区保存这部分新数据,防止新AOF文件生成期间丢失这部分数据;
  • 4)子进程根据内存快照,按照命令合并规则写入到新的AOF文件
  • 5.1)新AOF文件写入完成后,子进程发送信号给父进程,父进程更新统计信息;
  • 5.2)父进程把AOF重写缓冲区的数据写入到新的AOF文件;
  • 5.3)使用新AOF文件替换老文件,完成AOF重写。

写时复制(CopyOnWrite):顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。
信号:是进程间通讯的一种方式,且是异步的

何时触发?

  • 手动触发:直接调用bgrewriteaof命令。
  • 自动触发:根据auto-aof-rewrite-min-sizeauto-aof-rewrite-percentage参数确定自动触发时机。
    • auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB
    • auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。
      即,自动触发时机 =aof_current_size > auto-aof-rewrite-min-size &&(aof_current_size - aof_base_size)/aof_base_size >= auto-aof-rewrite-percentage

哪些地方会导致阻塞父进程?

  • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
  • 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长,特别是bigkey
  • 信号处理函数执行时。

为什么AOF重写不复用原AOF日志?

  • 父子进程写同一个文件会产生竞争问题,影响父进程的性能。
  • 如果AOF重写过程中失败了,相当于污染了原本的AOF文件,无法做恢复数据使用。

总结下父进程的工作内容:

  • 执行客户端发来的命令;
  • 将执行后的写命令追加AOF 缓冲区
  • 将执行后的写命令追加AOF 重写缓冲区

2.2.4 重启加载

Redis持久化文件加载流程

2.2.5 文件校验

加载损坏的AOF文件时会拒绝启动,并打印如下日志:

# Bad file format reading the append only file: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>

两点建议:

  • 对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof--fix命令进行修复,修复后使用diff -u对比数据的差异,找出丢失的数据,有些可以人工修改补全。
  • 对于不完整的情况,Redis为我们提供了aof-load-truncated配置来兼容这种情况,默认开启。加载AOF时,当遇到此问题时会忽略并继续启动,同时打印警告日志。
    #警告日志:
    
    # !!! Warning: short read while loading the AOF file !!!
    # !!! Truncating the AOF at offset 397856725 !!!
    # AOF loaded anyway because aof-load-truncated is enabled

3 RDB

RDB就是Redis DataBase的缩写,中文名为快照/内存快照,RDB持久化是把当前内存中的数据库快照保存到磁盘上的过程。

由于是某一时刻的快照,那么快照中的值要早于或者等于内存中的值。另外在 Redis 恢复数据时, RDB恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。

3.1 触发方式

分为手动触发自动触发两种方式。

3.1.1 手动触发

  • save命令:阻塞当前Redis服务器,直到RDB过程完成为止。
  • bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。
    bgsave流程

bgsave具体流程如下:

  • redis客户端执行bgsave命令或者自动触发bgsave命令
  • 主进程判断当前是否已经存在正在执行的子进程(如RDB/AOF子进程):
    • 如果存在,那么主进程直接返回;
    • 如果不存在正在执行的子进程,那么就fork一个新的子进程进行持久化数据,fork过程是阻塞的,fork操作完成后主进程即可执行其他操作;
  • 子进程先将数据写入到临时的RDB文件中,待快照数据写入完成后再原子替换旧的RDB文件
  • 同时发送信号给主进程,通知主进程RDB持久化完成,主进程更新相关的统计信息。

3.1.2 自动触发

有以下场景:

  • 使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave
  • 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
  • 执行debug reload命令重新加载Redis时,也会自动触发save操作
  • 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave

3.2 RDB优缺点

3.2.1 优点

  • RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份全量复制等场景;
  • Redis加载RDB文件恢复数据要远远快于AOF方式。

3.2.2 缺点

  • RDB方式实时性不够,无法做到秒级的持久化;
  • 每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;
  • RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全;
  • 版本兼容RDB文件问题。

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

4 混合

尽管 RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握:

  • 如果频率太低,两次快照间一旦服务器发生宕机,就可能会比较多的数据丢失;
  • 如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销。

Redis 4.0提出方法叫混合使用 AOF 日志和内存快照,也叫混合持久化。如果想要开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成yes

aof-use-rdb-preamble yes

混合持久化工作在 AOF 日志重写过程

  • 当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件;
  • 然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件;
  • 写入完成后通知主进程将新的含有 RDB 格式AOF 格式的 AOF 文件替换旧的AOF 文件

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。

混合持久化的AOF文件组成

4 参考

[^1] 张益军(付磊). 《Redis 开发与运维》4.1节。
[^2] Redis进阶 - 持久化:RDB和AOF机制详解
[^3] AOF 持久化是怎么实现的?


文章作者: Kezade
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kezade !
评论
  目录