《Redis开发与运维》读书笔记(五)持久化

第五章 持久化

Redis支持两种持久化机制(RDB和AOF),避免进程退出导致的数据丢失问题,下次启动时利用持久化文件进行数据恢复。

5.1 RDB

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

5.1.1 触发机制

手动触发命令:

  • save命令:阻塞当前Redis服务器,直到RDB过程完成。对于内存较大的实例会造成长时间阻塞,线上环境不建议使用。

    运行save命令对应的日志:

    1
    * DB saved on disk
  • bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般很短。是save的优化版本。

    运行bgsave命令对应的日志:

    1
    2
    3
    4
    * Background saving started by pid 3151
    * DB saved on disk
    * RDB: 0 MB of memory used by copy-on-write
    * Background saving terminated with success

自动触发场景:

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

5.1.2 流程说明

bgsave是主流的触发RDB持久化方式。

  1. 执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB或AOF子进程,若存在则直接返回。
  2. 父进程执行fork操作创建子进程,fork操作过程父进程会阻塞,通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。
  3. 父进程fork完成后,bgsave命令返回”Background saving started“信息并不阻塞父进程,可以继续响应其他命令。
  4. 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的时间,对应info统计的rdb_last_save_time选项。
  5. 进程发送信号给父进程表示完成,父进程更新统计信息,具体见info Persistence下的 rdb_* 相关选项。

5.1.3 RDB文件的处理

  • 保存:RDB文件保存在dir配置指定的目录下,文件名通过 dbfilename 配置指定。可以通过执行 config set dir {newDir}config set dbfilename {newFileName} 运行期动态执行,当下次运行时RDB文件会保存到新目录。当遇到坏盘或磁盘写满的情况时,可以通过此命令在线修改文件路径到可用的磁盘,之后执行bgsave进行磁盘切换,也适用于AOF持久化文件

  • 压缩:Redis默认使用LZF算法对生成的RDB文件进行压缩处理,默认开启,可以通过参数 config set rdbcompression {yes|no} 动态修改。

  • 校验:如果Redis加载损坏的RDB文件时拒绝启动,并打印如下日志,此时可以使用Redis提供的 redis-check-dump 工具检测RDB文件并获取对应的错误报告。

    1
    # Short read or OOM loading DB. Unrecoverable error, aborting now.

5.1.4 RDB的优缺点

优点:

  • RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份压缩,全量复制等场景。比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或文件系统中(如hdfs),用于灾难恢复。
  • Redis加载RDB文件恢复数据要远远比AOF快。

缺点:

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

AOF用来解决实时持久化问题。


5.2 AOF

AOF(append only file)持久化:以独立日志 的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决数据持久化的实时性,目前以成功主流的Redis持久化方案。

5.2.1 使用AOF

相关配置:

  • 开启AOF功能需要配置:appendonly yes,默认不开启。
  • AOF文件名通过 appendfilename 配置,默认名为 appendonly.aof
  • AOF保存路径配置和RDB一致,通过dir配置指定。

AOF工作流程:

  1. 命令写入(append):所有写入命令会追加到aof_buf(缓冲区)中。
  2. 文件同步(sync):AOF缓冲区根据对应策略向磁盘做同步操作。
  3. 文件重写(rewrite):随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
  4. 重启加载(load):当Redis服务器重启时,加载AOF文件进行数据恢复。

5.2.2 命令写入

AOF命令写入的内容直接是文本协议格式。例如set hello world命令,在AOF缓冲区追加文本 *3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n

  • 为什么AOF直接采用文本协议格式?

    • 较好的兼容性

    • 开启AOF后,所有写入命令都包含追加操作,直接采用协议格式,避免了二次处理开销

    • 较好的可读性,方便直接修改和处理

  • 为什么命令追加到aof_buf中?

    • Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,性能会完全取决于当前硬盘负载。
    • Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性间做出平衡。

5.2.3 文件同步

Redis提供了多种AOF缓冲区同步文件策略,由参数 appendfsync 控制。

可配置值 说明
always 命令写入aof_buf后调用系统fsync操作同步到AOF文件,fsync完成后线程返回
everysec 命令写入aof_buf后调用系统write操作,write完成后线程返回,fsync同步文件操作由专门线程每秒调用一次
no 命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步,同步硬盘操作由操作系统负责,通常同步周期最长30秒
  • write操作会触发延迟写机制。Linux在内核提供页缓冲区来提高磁盘IO性能。write操作写入系统缓冲区后直接返回,同步硬盘操作依赖系统调度机制(如缓冲区页空间写满或达到特定时间周期)。同步文件之前,若系统故障宕机,缓冲区内的数据会丢失。
  • fsync针对单个文件操作(如AOF文件),做强制硬盘同步,fsync将阻塞直到写入硬盘后返回,保证了数据持久化。
  • 除此两个外,linux还提供sync、fdatasync操作。

说明:

  • 配置为always时,每次写入都要同步AOF文件,在一般的SATA硬盘上,Redis只能支持大约几百TPS写入,不建议使用此配置。
  • 配置为no时,由于操作系统每次同步AOF文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提高了性能,但数据安全性无法保证。
  • 配置为everysec时,做到了兼顾性能与安全。理论上只会在系统突然宕机时丢失1秒左右的数据。是默认配置,也是建议配置。

5.2.4 重写机制

重写机制为了解决命令不断写入AOF文件,导致文件越来越大的问题。把Redis进程内的数据转化为写命令同步到新的AOF文件,从而压缩文件体积。

为什么重写后的文件体积会缩小:

  • 进程内已经超时的数据不再写入文件。
  • 旧的AOF文件含有无效命令,如del key1、hdel key2、srem keys、set a 111、set a 222等。重写使用进程内数据直接生成,新的文件只保留最终数据的写入命令。
  • 多条写命令可以整合为一个,如lpush list a、lpush list b、lpush list c转换为lpush list a b c。单条语句限制最大64个元素。

重写机制减小文件体积后,可以更快的被Redis加载从而提高性能。

重写过程分为:

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

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

    • auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB。
    • auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值,这两项可以在info persistence统计中查看。

    自动触发时机 = aof_current_size > auto-aof-rewrite-min-size && (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage

重写流程:

  1. 执行AOF重写请求。

    • 如果当前进程正在执行AOF重写,请求不执行并返回如下响应:

      1
      ERR Background append only file rewriting already in progress
    • 如果当前进程正在执行bgsave操作,重写命令延迟到其结束后再执行,返回如下响应:

      1
      Background append only file rewriting scheduled
  2. 父进程执行fork创建子进程,开销等于bgsave过程。

    1. 主进程fork操作完成后,继续响应其它命令。所有修改命令依然写入AOF缓冲区并根据appendfsync策略同步到硬盘,保证原有AOF机制正确性。
    2. 由于fork操作运用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然响应命令,Redis使用AOF重写缓冲区保存这部分新数据,防止新AOF文件生成期间丢失数据。
  3. 子进程根据内存快照,按照命令合并规则写入到新的AOF文件。每次批量写入硬盘数据量由配置 aof-rewrite-incremental-fsync 控制,默认为32MB,防止单次刷盘数据过多导致硬盘阻塞。

    1. 新AOF文件写入完成后,子进程发送信号给父进程,父进程更新统计信息,参见info persistence下的 aof_* 相关。
    2. 父进程把AOF重写缓冲区的数据写入到新的AOF文件中。
    3. 使用新的AOF文件替换老文件,完成AOF重写。

5.2.5 重启加载

AOF和RDB都可以在服务重启时进行数据恢复,流程如下:

相关日志:

1
2
3
4
# 加载AOF文件
DB loaded from append only file: 5.841 seconds
# 加载RDB文件
DB loaded from disk: 5.586 seconds

5.2.6 文件校验

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

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

此时要先备份文件,然后采用redis-check-aof –fix命令进行修复,修复后使用diff -u对比数据差异,找出丢失的数据,有些可以通过人工修改补齐。

AOF文件可能因为机器断电导致文件尾部写入不全,Redis提供了aof-load-truncated配置(默认开启)来兼容该类情况。此时遇到该情况会忽略并继续启动,打印日志:

1
2
3
# !!! Warning: short read while loading the AOF file !!!
# !!! Truncating the AOF at offset 397856725 !!!
# AOF loaded anyway because aof-load-truncated is enabled

5.3 问题定位与优化

Redis的持久化功能是影响Redis性能的高发地。

5.3.1 fork操作

无论是RDB还是AOF,fork操作都是必不可少的。大部分操作系统的fork都是重量级操作。子进程不需要拷贝父进程的物理内存空间,但需要复制父进程的空间内存页表。如10GB的进程要复制大概20MB的内存页表,fork操作耗时和进程总内存直接相关,虚拟化技术会导致耗时增加。

高流量的Redis实例OPS可能会达到5万以上,fork操作如果耗时达到秒级别会拖慢几万条命令的执行,造成很明显的延迟现象。正常情况下fork操作耗时应该在20毫秒左右。可以查看info stats的latest_fork_usec获取最近一次fork操作耗时,单位是微秒。

优化fork操作耗时:

  1. 优先使用物理机或高效支持fork操作的虚拟化技术,避免使用Xen。
  2. 控制Redis实例最大可用内存,fork耗时与内存成正比,线上环境建议单个Redis实例不要超过10GB。
  3. 合理配置Linux内存分配策略,避免物理内存不足导致fork操作失败。
  4. 降低fork操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制等。

5.3.2 子进程开销监控和优化

子进程负责AOF或者RDB文件的重写,涉及到CPU、内存和硬盘三部分开销。

(1)CPU

  • 分析:子进程负责把进程内的数据分批写入文件,这个过程属于CPU密集操作,通常子进程对单核CPU利用率接近90%。
  • 优化:
    • Redis是CPU密集型服务,不要做绑定单核CPU操作。由于子进程非常消耗CPU,会和父进程产生单核资源竞争。
    • 不要和其他CPU密集型服务部署在一起,造成CPU过度竞争。
    • 如果部署多个Redis实例,尽量保证同一时刻只有一个子进程执行重写工作。

(2)内存

RDB和AOF消耗内存情况:

  • RDB重写时,打印日志:

    1
    2
    3
    4
    * Backgroud saving started by pid 7692
    * DB save on disk
    * RDB: 5 MB of memory used by copy-on-write
    * Backgroud saving terminated with success

    如果重写过程存在内存修改操作,父进程负责创建修改内存页的副本,日志可以看到这部分内存消耗了5MB。

  • AOF重写时,打印日志:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    * Backgroud append only file rewriting started by pid 8937
    * AOF rewrite child asks to stop sending diffs.
    * Parent agreed to stop sending diffs. Finalizing AOF...
    * Concatenating 0.00 MB of AOF diff received from parent.
    * SYNC append only file rewrite performed
    * AOF rewrite: 53 MB of memory used by copy-on-write
    * Background AOF rewrite terminated with success
    * Residual parent diff successfully flushed to the rewriteen AOF (1.49MB)
    * Background AOF rewrite finished successfully

    父进程同样维护页副本消耗内存,但AOF需要重写缓冲区,根据日志预估内存消耗:54.49MB。

  • 分析:子进程通过fork操作产生,占用内存大小等同于父进程,理论上需要两倍的内存空间来完成持久化操作。但Linux有写时复制机制,父子进程共享相同的物理内存页,当父进程处理请求时会把要修改的页创建副本,子进程则在fork操作期间共享整个父进程内存快照。
  • 优化:
    1. 同CPU优化相同,部署多个Redis实例,尽量保证同一时刻只有一个子进程工作。
    2. 避免在大量写入时做子进程重写操作,会导致父进程维护大量页副本,造成内存消耗。
    3. Linux kernel在2.6.38内核增加了Transparent Huge Pages(THP)。支持huge pages(2MB)的页分配,默认开启。当开启时可以降低fork创建子进程的时间,但执行fork之后,如果开启THP,复制页单位从4KB变为2MB,会大幅增加重写期间父进程的内存损耗。所以建议关闭此功能:sudo echo never > /sys/kernel/mm/transparent_hugepage/enabled

(3)硬盘

  • 分析:子进程主要把AOF和RDB文件写入硬盘持久化,造成硬盘写入压力。根据Redis重写AOF/RDB的数据量,结合系统工具如 sar、iostat、iotop等,分析出重写期间硬盘负载情况。
  • 优化:
    1. 不要和其他高硬盘负载的服务部署在一起,如存储服务、消息队列服务等。
    2. AOF重写时会消耗大量硬盘IO,可以开启配置no-appendfsync-on-rewrite,默认关闭。表示AOF重写期间不做fsync操作,极端情况下会导致丢失整个AOF重写期间的数据,请根据数据安全性是否配置。
    3. 当开启AOF功能的Redis用于高流量写入场景时,如果使用普通机械磁盘,写入吞吐一般在100MB/s左右,这时Redis实例的瓶颈主要在AOF同步硬盘上。
    4. 单机部署多个Redis实例时,可以配置不同的实例分盘存储AOF文件,分摊硬盘写入压力。

5.3.3 AOF追加阻塞

AOF常用的同步硬盘策略是everysec,这种方式下Redis使用另一条线程每秒执行fsync同步硬盘,当系统硬盘繁忙时,会造成Redis主线程阻塞。

阻塞流程分析:

  1. 主线程负责写入AOF缓冲区。
  2. AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。
  3. 主线程负责对比上次AOF同步时间:
    • 如果距上次同步成功时间小于2秒,主线程直接返回。
    • 如果距上次同步成功时间超过2秒,主线程将会阻塞,直到同步操作完成。

分析可知:

  1. everysec最多会丢失2秒数据,而不是1秒。
  2. 如果系统fsync缓慢,会导致Redis主线程阻塞影响效率。

阻塞问题定位:

  1. 发生AOF阻塞时,打印如下日志,用于记录AOF fsync阻塞导致拖慢Redis服务的行为:

    1
    Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete,this may slow down Redis
  2. 每当发生AOF追加阻塞事件发生时,在info Persistence统计中,aof_delayed_fsync指标会累加,查看这个指标方便定位AOF阻塞问题。

  3. AOF同步最多允许2秒的延迟,当延迟发生时说明硬盘存在高负载问题,可以通过监控工具如iotop,定位消耗硬盘IO资源的进程。

优化AOF追加阻塞问题主要是优化系统硬盘负载,彼此之间会产生对CPU和IO的竞争。


5.4 多实例部署

Redis单线程架构无法充分利用CPU多核特性,通常的做法是在一台机器上部署多个Redis实例,多个实例开启AOF重写后,彼此之间会产生对CPU和IO的竞争。需要采用一种措施将子进程工作隔离,Redis在info persistence中提供了监控子进程运行状况的度量指标。

属性名 属性值
rdb_bgsave_in_progress bgsave子进程是否正在运行
rdb_current_bgsave_time_sec 当前运行bgsave的时间,-1表示未运行
aof_enabled 是否开启AOF功能
aof_rewrite_in_progress AOF重写子进程是否正在运行
aof_rewrite_scheduled 在bgsave结束后是否运行AOF重写
aof_current_rewrite_time_sec 当前运行AOF重写的时间,-1表示未运行
aof_current_size AOF文件当前字节数
aof_base_size AOF上次重写rewrite的字节数

可以通过外部程序轮询控制AOF重写操作的执行,如图:

流程说明:

  • 外部程序定时轮询监控机器(machine)上所有Redis实例。
  • 对于开启AOF的实例,查看 (aof_current_size - aof_base_size) / aof_base_size 确认增长率。
  • 当增长率超过特定阈值(如100%),执行bgrewriteaof命令手动触发当前实例的AOF重写。
  • 运行期间循环检查aof_rewrite_in_progress和aof_current_rewrite_time_sec指标,直到AOF重写结束。
  • 确认实例AOF重写完成后,再检查其他实例并重复2到4步操作。从而保证机器内所有Redis实例AOF重写串行化执行。

参考:

🔗 《Redis开发与运维》