Redis详解(3)数据持久化机制

2022-04-14 14:47:31 浏览数 (1)

一、Redis持久化方式

Redis由于支持非常丰富的内存数据结构类型,如何把这些复杂的内存组织方式持久化到磁盘上是一个难题,所以Redis的持久化方式与传统数据库的方式有比较多的差别,Redis一共支持四种持久化方式,分别是:

  • RDB定时快照方式(snapshot): RDB 将数据库的快照(snapshot)以二进制的方式保存到磁盘中。
  • AOF基于语句追加文件的方式:则以协议文本的方式,将所有对数据库进行过写入的命令(及其参数)记录到 AOF 文件,以此达到记录数据库状态的目的。
  • 虚拟内存(vm) (被废弃)
  • Diskstore方式 (被废弃)

在设计思路上,前两种是基于全部数据都在内存中,即小数据量下提供磁盘落地功能,而后两种方式则是作者在尝试存储数据超过物理内存时,即大数据量的数据存储,截止到本文,后两种持久化方式仍然是在实验阶段,并且vm方式基本已经被作者放弃,所以实际能在生产环境用的只有前两种,换句话说Redis目前还只能作为小数据量存储(全部数据能够加载在内存中),海量数据存储方面并不是Redis所擅长的领域。

很多人开始会想象两者是互相结合的,即dump出一个snapshot到RDB文件,然后在此基础上记录变化日志到AOF文件。实际上两者毫无关系,完全独立运行,因为作者认为简单才不会出错。如果使用了AOF,重启时只会从AOF文件载入数据,不会再管RDB文件。 正确关闭服务器:redis-cli shutdown 或者 kill,都会graceful shutdown,保证写RDB文件以及将AOF文件fsync到磁盘,不会丢失数据。 如果是粗暴的Ctrl C,或者kill -9 就可能丢失。

下面分别介绍下这几种持久化方式:

二、RDB定时快照方式(snapshot)

RDB持久化的触发分为手动触发和自动触发两种。

1、手动触发

通过redis的save命令和bgsave命令,都可以生成RDB文件。

一,save保存数据到磁盘的方式:

Redis Save 命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照(snapshot)以 RDB 文件的形式保存到硬盘。

语法:redis 127.0.0.1:6379> SAVE 返回值:保存成功时返回 OK 。

二,BGSAVE保存数据到磁盘的方式:

BGSAVE 命令执行之后立即返回 OK ,然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。

客户端可以通过 LASTSAVE 命令查看相关信息,判断 BGSAVE 命令是否执行成功。

bgsave命令执行过程中,只有fork子进程时会阻塞服务器,而对于save命令,整个过程都会阻塞服务器,因此save已基本被废弃,线上环境要杜绝save的使用;后文中也将只介绍bgsave命令。此外,在自动触发RDB持久化时,Redis也会选择bgsave而不是save来进行持久化;下面介绍自动触发RDB持久化的条件。

2、自动触发

自动触发最常见的情况是在配置文件中通过save m n,指定当m秒内发生n次变化时,会触发bgsave。 例如,查看redis的默认配置文件redis.conf,可以看到如下配置信息:

其中save 900 1的含义是:当时间到900秒时,如果redis数据发生了至少1次变化,则执行bgsave;

当三个save条件满足任意一个时,都会引起bgsave的调用。

3、save m n的实现原理

save m n的含义:当时间到m秒时,如果redis数据发生了至少n次变化,则执行bgsave;

是将数据先存储在内存,然后当数据累计达到某些设定的伐值的时候,就会触发一次DUMP操作,将变化的数据一次性写入数据文件(RDB文件)。

RDB实际是在Redis内部一个定时器事件,每隔固定时间去检查当前数据发生的改变次数与时间是否满足配置的持久化触发的条件,如果满足则通过操作系统fork调用来创建出一个子进程,这个子进程默认会与父进程共享相同的地址空间,这时就可以通过子进程来遍历整个内存来进行存储操作,而主进程则仍然可以提供服务,当有写入时由操作系统按照内存页(page)为单位来进行copy-on-write保证父子进程之间不会互相影响。

具体实现:是通过serverCron函数、dirty计数器、和lastsave时间戳来实现的。

redis.c文件的周期性函数serverCron:

代码语言:javascript复制
/* This is our timer interrupt, called server.hz times per second.
 * Here is where we do a number of things that need to be done asynchronously.
 * For instance:
 *
 * - Active expired keys collection (it is also performed in a lazy way on
 *   lookup).
 * - Software watchdog.
 * - Update some statistic.
 * - Incremental rehashing of the DBs hash tables.
 * - Triggering BGSAVE / AOF rewrite, and handling of terminated children.
 * - Clients timeout of different kinds.
 * - Replication reconnection.
 * - Many more...
 *
 * Everything directly called here will be called server.hz times per second,
 * so in order to throttle execution of things we want to do less frequently
 * a macro is used: run_with_period(milliseconds) { .... }
 */

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {

serverCron是Redis服务器的周期性操作函数,默认每隔100ms执行一次;该函数对服务器的状态进行维护,其中一项工作就是检查 save m n 配置的条件是否满足,如果满足就执行bgsave。

dirty计数器是Redis服务器维持的一个状态,记录了上一次执行bgsave/save命令后,服务器状态进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。

例如,如果Redis执行了set mykey helloworld,则dirty值会 1;如果执行了sadd myset v1 v2 v3,则dirty值会 3;注意dirty记录的是服务器进行了多少次修改,而不是客户端执行了多少修改数据的命令。

lastsave时间戳也是Redis服务器维持的一个状态,记录的是上一次成功执行save/bgsave的时间。

save m n的原理如下: 1)按定时执行:每隔100ms,执行serverCron函数; 2)遍历所有save m n配置:在serverCron函数中,遍历save m n配置的保存条件,只要有一个条件满足,就进行bgsave。 例如:

当三个save条件满足任意一个时,都会引起bgsave的调用。 3)对于每一个save m n条件,只有下面两条同时满足时才算满足: (1)当前时间 - lastsave > m (2)dirty >= n 4)save m n 执行日志

下图是save m n触发bgsave执行时,服务器打印日志的情况:

其他自动触发机制 除了save m n 以外,还有一些其他情况会触发bgsave: 在主从复制场景下,如果从节点执行全量复制操作,则主节点会执行bgsave命令,并将rdb文件发送给从节点 执行shutdown命令时,自动执行rdb持久化,如下图所示:

4、bgsave实现流程:

1)Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(后面会详细介绍该命令)的子进程,如果在执行则bgsave命令直接返回。bgsave/bgrewriteaof 的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。

2)FORK子进程:RDB写入时,会连内存一起Fork出一个新进程(子进程默认会与父进程共享相同的地址空间),遍历新进程内存中的数据写文件,这样就解决了些Snapshot过程中又有新的写入请求进来的问题。这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令 。父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令

3)子进程创建RDB文件:RDB会先写到临时文件,完了再Rename成,这样外部程序对RDB文件的备份和传输过程是安全的。而且即使写新快照的过程中Server被强制关掉了,旧的RDB文件还在。

3)可配置是否进行压缩,压缩方法是字符串的LZF算法,以及将string形式的数字变回int形式存储。 4)动态所有停止RDB保存规则的方法:redis-cli config set save “”

该持久化的主要缺点是定时快照只是代表一段时间内的内存映像,所以系统重启会丢失上次快照与重启之间所有的数据。

5. RDB文件

RDB文件是经过压缩的二进制文件,下面介绍关于RDB文件的一些细节。 存储路径 RDB文件的存储路径既可以在启动前配置,也可以通过命令动态设定。 配置:dir配置指定目录,dbfilename指定文件名。默认是Redis根目录下的dump.rdb文件。 动态设定:Redis启动后也可以动态修改RDB存储路径,在磁盘损害或空间不足时非常有用;执行命令为config set dir {newdir}和config set dbfilename {newFileName}

1) REDIS:常量,保存着”REDIS”5个字符。 2) db_version:RDB文件的版本号,注意不是Redis的版本号。 3) SELECTDB 0 pairs:表示一个完整的数据库(0号数据库),同理SELECTDB 3 pairs表示完整的3号数据库;只有当数据库中有键值对时,RDB文件中才会有该数据库的信息(上图所示的Redis中只有0号和3号数据库有键值对);如果Redis中所有的数据库都没有键值对,则这一部分直接省略。其中:SELECTDB是一个常量,代表后面跟着的是数据库号码;0和3是数据库号码;pairs则存储了具体的键值对信息,包括key、value值,及其数据类型、内部编码、过期时间、压缩信息等等。 4) EOF:常量,标志RDB文件正文内容结束。 5) check_sum:前面所有内容的校验和;Redis在载入RBD文件时,会计算前面的校验和并与check_sum值比较,判断文件是否损坏。 Redis默认采用LZF算法对RDB文件进行压缩。虽然压缩耗时,但是可以大大减小RDB文件的体积,因此压缩默认开启;可以通过命令关闭:

代码语言:javascript复制
config set rdbcompression no

需要注意的是,RDB文件的压缩并不是针对整个文件进行的,而是对数据库中的字符串进行的,且只有在字符串达到一定长度(20字节)时才会进行。

6. 启动时加载

RDB文件的载入工作是在服务器启动时自动执行的,并没有专门的命令。但是由于AOF的优先级更高,因此当AOF开启时,Redis会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会在Redis服务器启动时检测RDB文件,并自动载入。服务器载入RDB文件期间处于阻塞状态,直到载入完成为止。 Redis载入RDB文件时,会对RDB文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败。

7. RDB常用配置总结

下面是RDB常用的配置项,以及默认值;前面介绍过的这里不再详细介绍。

默认是打开RDB持久化 save m n:bgsave自动触发的条件;如果没有save m n配置,相当于自动的RDB持久化关闭,不过此时仍可以通过其他方式触发 stop-writes-on-bgsave-error yes:当bgsave出现错误时,Redis是否停止执行写命令;设置为yes,则当硬盘出现问题时,可以及时发现,避免数据的大量丢失;设置为no,则Redis无视bgsave的错误继续执行写命令,当对Redis服务器的系统(尤其是硬盘)使用了监控时,该选项考虑设置为no rdbcompression yes:是否开启RDB文件压缩 rdbchecksum yes:是否开启RDB文件的校验,在写入文件和读取文件时都起作用;关闭checksum在写入文件和启动文件时大约能带来10%的性能提升,但是数据损坏时无法发现 dbfilename dump.rdb:RDB文件名 dir ./:RDB文件和AOF文件所在目录

8、关闭RDB:

1)*关闭rdb的命令:config set save ""

执行命令后,无需重启服务,即可生效。

2)修改Save配置注释掉,并打开save ""

#Save 900 1

#Save 300 10

#Save 60 10000

Save ""

修改完成后,重启Redis服务即可。

三、AOF基于语句追加方式

AOF方式实际类似mysql的基于语句的binlog方式,即每条会使Redis内存数据发生改变的命令都会追加到一个log文件中,也就是说这个log文件就是Redis的持久化数据。

AOF持久化的触发分为手动触发和自动触发两种。

1、手动触发

使用bgrewriteaof命令:Redis主进程fork子进程来执行AOF重写,这个子进程创建新的AOF文件来存储重写结果,防止影响旧文件。因为fork采用了写时复制机制,子进程不能访问在其被创建出来之后产生的新数据。Redis使用“AOF重写缓冲区”保存这部分新数据,最后父进程将AOF重写缓冲区的数据写入新的AOF文件中然后使用新AOF文件替换老文件。

代码语言:javascript复制
127.0.0.1:6379> bgrewriteoaf
OK

2、自动触发

  和RDB一样,配置在redis.conf文件里:

  • appendonly:是否打开AOF持久化功能,appendonly默认是 no, 改成appendonly yes。
  • appendfilename:AOF文件名称
  • appendfsync:同步频率
  • auto-aof-rewrite-min-size:如果文件大小小于此值不会触发AOF,默认64MB
  • auto-aof-rewrite-percentage:Redis记录最近的一次AOF操作的文件大小,如果当前AOF文件大小增长超过这个百分比则触发一次重写,默认100

启用AOF:appendonly yes

关闭aof:

命令行关闭:关闭aof的命令:config set appendfsync no

配置文件:将appendonly设置为no,默认是 appendonly no )

3、AOF 工作原理

是将数据也是先存在内存,但是在存储的时候会使用调用fsync来完成对本次写操作的日志记录,这个日志文件其实是一个基于Redis网络交互协议的文本文件。AOF调用fsync也不是说全部都是无阻塞的,在某些系统上可能出现fsync阻塞进程的情况,对于这种情况可以通过配置修改,但默认情况不要修改。AOF最关键的配置就是关于调用fsync追加日志文件的平率,有两种预设频率,always每次记录进来都添加,everysecond 每秒添加一次。

同步命令到 AOF 文件的整个过程可以分为三个阶段: 1、命令写入:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。 2、追加AOF缓存:AOF 程序根据接收到的命令数据, 然后每隔一定的时间(比如每隔一秒)将协议内容写入AOF 缓冲区中. 3、文件写入保存:当达到 AOF同步条件,fsync 函数或者 fdatasync 函数会被调用,将 OS Cache 中的数据写入磁盘文件。 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。

第一阶段:命令写入

当一个 Redis 客户端需要执行命令时, 它通过网络连接, 将协议文本发送给 Redis 服务器。 比如说, 要执行命令 SET KEY VALUE , 客户端将向服务器发送文本 "*3rn

比如说, 针对上面的 SET 命令例子, Redis 将客户端的命令指针指向实现 SET 命令的 setCommand 函数, 并创建三个 Redis 字符串对象, 分别保存 SETKEYVALUE 三个参数(命令也算作参数)。

每当命令函数成功执行之后, 命令参数都会被传播到 AOF 程序, 以及 REPLICATION 程序(本节不讨论这个,列在这里只是为了完整性的考虑)。

这个执行并传播命令的过程可以用以下伪代码表示:

代码语言:javascript复制
if (execRedisCommand(cmd, argv, argc) == EXEC_SUCCESS):

    if aof_is_turn_on():
        # 传播命令到 AOF 程序
        propagate_aof(cmd, argv, argc)

    if replication_is_turn_on():
        # 传播命令到 REPLICATION 程序
        propagate_replication(cmd, argv, argc)

第二阶段:追加到AOF缓存

所有的写入命令会追加到aof_buf中 当命令被传播到 AOF 程序之后, 程序会根据命令以及命令的参数, 将命令从字符串对象转换回原来的协议文本。 比如说, 如果 AOF 程序接受到的三个参数分别保存着 SET 、 KEY 和 VALUE 三个字符串, 那么它将生成协议文本 "*3rn

代码语言:javascript复制
struct redisServer {
    // 其他域...
    sds aof_buf;
    // 其他域...
};

至此, 追加命令到缓存的步骤执行完毕。综合起来,整个缓存追加过程可以分为以下三步:

  1. 接受命令、命令的参数、以及参数的个数、所使用的数据库等信息。
  2. 将命令还原成 Redis 网络通讯协议。
  3. 将协议文本追加到 aof_buf 末尾。

第三阶段:文件写入和保存

AOF缓冲区根据对应的策略向硬盘做同步操作。每当服务器常规任务函数被执行、 或者事件处理器被执行时, aof.c/flushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作:系统调用write和fsync说明 WRITE:根据条件,将aof_buf中的缓存写入到 AOF 文件,即系统调用write写入os cache。 SAVE:根据条件,调用fsync或 fdatasync 函数,将AOF文件保存到磁盘中。

两个步骤都需要根据一定的条件来执行, 而这些条件由 AOF 所使用的保存模式来决定, 以下小节就来介绍 AOF 所使用的三种保存模式, 以及在这些模式下, 步骤 WRITE 和 SAVE 的调用条件。

第三阶段:AOF文件重写(Rewrite)

AOF是redis的一种持久化方式,用来记录所有的写操作,但是随着时间增加,aof文件会越来越大,所以需要进行重写,将内存中的数据重新以命令的方式写入aof文件。在重写的过程中,由于redis还会有新的写入,为了避免数据丢失,会开辟一块内存用于存放重写期间产生的写入操作,等到重写完毕后会将这块内存中的操作再追加到aof文件中。

为什么要重写

redis中的数据其实有限的,很多数据可能会自动过期,可能会被用户删除,可能会被redis用缓存清除的算法清理掉。redis中的数据会不断淘汰掉旧的,就一部分常用的数据会被自动保留在redis内存中,所以可能很多之前的已经被清理掉的数据, 随着写操作的不断增加, 对应的写日志还停留在AOF中,AOF日志文件就一个,AOF文件会越来越大。

例如你递增一个计数器100次,那么最终结果就是数据集里的计数器的值为最终的递增结果,但是AOF文件里却会把这100次操作完整的记录下来。而事实上要恢复这个记录,只需要1个命令就行了,也就是说AOF文件里那100条命令其实可以精简为1条。所以Redis支持这样一个功能:在不中断服务的情况下在后台重建AOF文件。

问题1:AOF文件会变得越来越大

AOF文件通过同步 Redis 服务器所执行的命令, 从而实现了数据库状态的记录, 但是, 这种同步方式会造成一个问题: 随着运行时间的流逝, AOF 文件会变得越来越大。举个例子, 如果服务器执行了以下命令:

RPUSH list 1 2 3 4 // [1, 2, 3, 4] RPOP list // [1, 2, 3] LPOP list // [2, 3] LPUSH list 1 // [1, 2, 3]

那么光是记录 list 键的状态, AOF 文件就需要保存四条命令。

问题2:AOF 文件的体积就会急速膨胀

另一方面, 有些被频繁操作的键, 对它们所调用的命令可能有成百上千、甚至上万条, 如果这样被频繁操作的键有很多的话, AOF 文件的体积就会急速膨胀, 对 Redis 、甚至整个系统的造成影响。

为了解决以上的问题:Redis 需要对 AOF文件自动在后台每隔一定时间做rewrite(重写)操作, 创建一个新的 AOF 文件来代替原有的 AOF 文件, 新 AOF 文件和原有 AOF 文件保存的数据库状态完全一样, 但新 AOF 文件的体积小于等于原有 AOF 文件的体积。比如日志里已经存放了针对100w数据的写日志了; redis内存只剩下10万; 基于内存中当前的10万数据构建一套最新的日志,到AOF中; 覆盖之前的老日志; 确保AOF日志文件不会过大,保持跟redis内存数据量一致。

AOF的rewrite实现:

rewrite操作对于老日志文件中对于Key的多次操作,只保留最终的值的那次操作记录到日志文件中,从而缩小日志文件的大小。 考虑这样一个情况, 如果服务器对键 list 执行了以下四条命令:

RPUSH list 1 2 3 4 // [1, 2, 3, 4] RPOP list // [1, 2, 3] LPOP list // [2, 3] LPUSH list 1 // [1, 2, 3]

那么当前列表键 list 在数据库中的值就为 [1, 2, 3]

如果我们要保存这个列表的当前状态, 并且尽量减少所使用的命令数, 那么最简单的方式不是去 AOF 文件上分析前面执行的四条命令, 而是直接读取 list 键在数据库的当前值, 然后用一条 RPUSH 1 2 3 命令来代替前面的四条命令。

AOF的rewrite流程:

作为一种辅佐性的维护手段, Redis 不希望 AOF 重写造成服务器无法处理请求, 所以 Redis 决定将 AOF 重写程序放到(后台)子进程里执行, 这样处理的最大好处是:

  1. 子进程进行 AOF 重写期间,主进程可以继续处理命令请求。
  2. 子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。

不过, 使用子进程也有一个问题需要解决: 因为子进程在进行 AOF 重写期间, 主进程还需要继续处理命令, 而新的命令可能对现有的数据进行修改, 这会让当前数据库的数据和重写后的 AOF 文件中的数据不一致。

为了解决这个问题, Redis 增加了一个 AOF 重写缓冲区, 这个缓存在 fork 出子进程之后开始启用, Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外, 还会追加到这个 AOF 重写缓冲区。

AOF的重写rewrite流程如下:即是 BGREWRITEAOF 命令的工作原理

(1)触发重写:超过AOF的阈值,redis fork一个子进程,进行rewrite操作。子进程基于当前内存中的数据,构建日志,开始往一个新的临时AOF文件中写入日志,:temp-rewriteaof-pid.aof,pid为子进程pid。 (2)开启 AOF 重写缓冲区:同时开启 AOF 重写缓冲区,保存这段时间内新增的写指令。 (3)主进程继续写旧文件:之前的 AOF 操作保持,继续正常工:redis主进程,接收到client新的写操作之后,写入AOF 重写缓冲区,同时新的命令日志也继续写入旧的AOF文件appendonly.aof。 (4)AOF重写缓冲区的指令追加新文件: 重写子进程写入temp-rewriteaof-pid.aof文件结束,redis主进程将将 AOF重写缓冲区的指令追加至新AOF文件 temp-rewriteaof-pid.aof末尾。 (5)文件替换:最后用temp-rewriteaof-pid.aof替换旧AOF文件appendonly.aof。

在一般情况下在整个 AOF 后台重写过程中, 只有最后的 AOF重写缓冲区的指令追加新文件 和改名替换操作会造成主进程阻塞, 在其他时候, AOF 后台重写都不会对主进程造成阻塞, 这将 AOF 重写对性能造成的影响降到了最低。

AOF的rewrite触发条件:

redis 2.4之后,AOF重写可以由用户通过调用 BGREWRITEAOF 手动触发。也可以服务器在AOF功能开启的情况下自动进行, 源码里面会维持以下三个变量:

  • 记录当前 AOF 文件大小的变量 aof_current_size
  • 记录最后一次 AOF 重写之后, AOF 文件大小的变量 aof_rewrite_base_size
  • 增长百分比变量 aof_rewrite_perc

每次当 serverCron 函数执行时, 它都会检查以下条件是否全部满足, 如果是的话, 就会触发自动的 AOF 重写:

  1. 没有 BGSAVE 命令在进行。
  2. 没有 BGREWRITEAOF 在进行。
  3. 当前 AOF 文件大小大于 server.aof_rewrite_min_size (默认值为 1 MB)。
  4. 当前 AOF 文件大小和最后一次 AOF 重写后的大小之间的比率大于等于指定的增长百分比。

在redis.conf中可以配置rewrite策略,两个条件需要同时满足。

auto-aof-rewrite-percentage 100 :当前写入日志文件的大小占到初始日志文件大小的某个百分比时触发Rewrite。Redis会记住自从上一次重写后AOF文件的大小(如果自Redis启动后还没重写过,则记住启动时使用的AOF文件的大小)。如果当前的文件大小比起记住的那个大小超过指定的百分比,则会触发重写。 auto-aof-rewrite-min-size 64mb :本次Rewrite最小的写入数据量。只有大于这个值文件才会重写,以防文件很小,但是已经达到百分比的情况。

默认情况下, 增长百分比为 100% , 也即是说, 如果前面三个条件都已经满足, 并且当前 AOF 文件大小比最后一次 AOF 重写时的大小要大一倍的话, 那么触发自动 AOF 重写。

比如说上一次AOF rewrite 重写之后,是128mb。然后就会接着128mb继续写AOF的日志,如果发现增长的比例,超过了之前的100%,256mb,就可能会去触发一次rewrite但是此时还要去跟min-size,64mb去比较,256mb > 64mb,才会去触发rewrite。

bgrewriteaof机制,在一个子进程中进行aof的重写,从而不阻塞主进程对其余命令的处理,同时解决了aof文件过大问题。

no-appendfsync-on-rewrite参数

由于重写子进程和主进程写aof文件的操作,两者都会操作磁盘,而AOF重写往往会涉及大量磁盘操作,这样就可能造成主进程在写aof文件的时候出现阻塞的情形: 主进程 fsync()/write() 操作被阻塞, 最终导致 Redis 主进程阻塞了.

解决办法:no-appendfsync-on-rewrite yes 设置为yes表示重写期间对新写操作不fsync,暂时存在AOF缓冲区中,等重写完成后再写入,默认为no 。

如果该参数设置为no,是最安全的方式,不会丢失数据,但是要忍受阻塞的问题。如果设置为yes,这就相当于将appendfsync设置为no,这说明并没有执行磁盘操作,只是写入了缓冲区,因此这样并不会造成阻塞(因为没有竞争磁盘),但是如果这个时候redis挂掉,就会丢失数据。丢失多少数据呢?在linux的操作系统的默认设置下,最多会丢失30s的数据。

因此,如果应用系统无法忍受延迟,而可以容忍少量的数据丢失,则设置为yes。如果应用系统无法忍受数据丢失,则设置为no。

5、AOF缓冲区同步文件策略

Redis 目前支持三种 AOF 保存模式,Redis的配置appendfsync 中存在三种同步方式,它们分别是:

1)AOF_FSYNC_NO :不保存。 2)AOF_FSYNC_EVERYSEC :每一秒钟保存一次、该策略为AOF的缺省策略。。 3)AOF_FSYNC_ALWAYS :每执行一个命令保存一次 对应redis配置: appendfsync no:写入aof文件,不等待磁盘同步。 appendfsync always:总是写入aof文件,并完成磁盘同步 appendfsync everysec:每一秒写入aof文件,并完成磁盘同步 从持久化角度讲,always是最安全的。从效率上讲,no是最快的。而redis默认设置进行了折中,选择了everysec。合情合理。

appendfsync no 不保存:

命令写入aof缓冲区后,在写入系统缓冲区直接返回(系统调用write写入os cache),然后有专门线程每秒执行写入磁盘(阻塞,系统调用fsync)后返回。当设置appendfsync为no的时候,Redis不会主动调用fsync去将AOF日志内容同步到磁盘,所以这一切就完全依赖于操作系统的调试了。对大多数Linux操作系统,是每30秒进行一次fsync,将缓冲区中的数据写到磁盘上。

在这种模式下, 每次调用 flushAppendOnlyFile 函数,write(写入os cache)都会被执行, 但save(fsync)会被略过。

在这种模式下, SAVE 只会在以下任意一种情况中被执行:

  • Redis 被关闭
  • AOF 功能被关闭
  • 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)

这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。

appendfsync everysec 每一秒钟保存一次:

在这种模式中, SAVE原则上每隔一秒钟就会执行一次, 因为SAVE操作是由后台子线程调用的, 所以它不会引起服务器主进程阻塞。 注意, 在上一句的说明里面使用了词语“原则上”, 在实际运行中, Redis主进程在这种模式下对 fsync/fdatasync 的调用并不是每秒一次, 它和调用 flushAppendOnlyFile 函数时 Redis 所处的状态有关。

每当 flushAppendOnlyFile 函数被调用时, 可能会出现以下四种情况:

  • 子线程正在执行 SAVE ,并且:

1、这个 SAVE 的执行时间未超过 2 秒,那么Redis主进程就会直接返回,并不执行 WRITE 或新的 SAVE 。 2、这个 SAVE 已经执行超过 2 秒,那么程序执行 WRITE ,但不执行新的 SAVE 。注意:Redis主进程就会阻塞,因为这时 WRITE 的写入必须等待子线程先完成(旧的) SAVE ,因此这里 WRITE 会比平时阻塞更长时间。

  • 子线程没有在执行 SAVE ,并且: 1、上次成功执行 SAVE 距今不超过 1 秒,那么程序执行 WRITE ,但不执行 SAVE 。 2、上次成功执行 SAVE 距今已经超过 1 秒,那么程序执行 WRITE 和 SAVE 。

可以用流程图表示这四种情况:

根据以上说明可以知道, 在“每一秒钟保存一次”模式下, 如果在情况 1 中发生故障停机, 那么用户最多损失小于 2 秒内所产生的所有数据。

如果在情况 2 中发生故障停机, 那么用户损失的数据是可以超过 2 秒的。

Redis 官网上所说的, AOF 在“每一秒钟保存一次”时发生故障, 只丢失 1 秒钟数据的说法, 实际上并不准确。

appendfsync always 每执行一个命令保存一次:

在这种模式下,每次执行完一个命令之后, WRITE 和 SAVE 都会被执行。

另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。

6、Redis的保存模式对性能的影响

对于三种 AOF 保存模式, 它们对服务器主进程的阻塞情况如下:

  1. appendfsync no 不保存:写入和保存都由主进程执行,两个操作都会阻塞主进程。
  2. appendfsync everysec每一秒钟保存一次:写入操作由主进程执行,阻塞主进程。保存操作由子线程执行,不直接阻塞主进程,但保存操作完成的快慢会影响写入操作的阻塞时长。
  3. appendfsync always 每执行一个命令保存一次::和模式 1 一样。

因为阻塞操作会让 Redis 主进程无法持续处理请求, 所以一般说来, 阻塞操作执行得越少、完成得越快, Redis 的性能就越好。

模式 1 的保存操作只会在AOF 关闭或 Redis 关闭时执行, 或者由操作系统触发, 在一般情况下, 这种模式只需要为写入阻塞, 因此它的写入性能要比后面两种模式要高, 当然, 这种性能的提高是以降低安全性为代价的: 在这种模式下, 如果运行的中途发生停机, 那么丢失数据的数量由操作系统的缓存冲洗策略决定。

模式 2 在性能方面要优于模式 3 , 并且在通常情况下, 这种模式最多丢失不多于 2 秒的数据, 所以它的安全性要高于模式 1 , 这是一种兼顾性能和安全性的保存方案。

模式 3 的安全性是最高的, 但性能也是最差的, 因为服务器必须阻塞直到命令信息被写入并保存到磁盘之后, 才能继续处理请求。

综合起来,三种 AOF 模式的操作特性可以总结如下:

模式

WRITE 是否阻塞?

SAVE 是否阻塞?

停机时丢失的数据量

AOF_FSYNC_NO

阻塞

阻塞

操作系统最后一次对 AOF 文件触发 SAVE 操作之后的数据。

AOF_FSYNC_EVERYSEC

阻塞

不阻塞

一般情况下不超过 2 秒钟的数据。

AOF_FSYNC_ALWAYS

阻塞

阻塞

最多只丢失一个命令的数据。

4、Redis AOF日志分析

25852:M 16 Dec 19:05:10.646 * Starting automatic rewriting of AOF on 100% growth:到达触发条件,开始执行AOF 25852:M 16 Dec 19:05:10.765 * Background append only file rewriting started by pid 31619:bgrewriteaof命令,会fork一个子进程(进程号31619)来执行保存; 25852:M 16 Dec 19:08:46.489 * AOF rewrite child asks to stop sending diffs. 31619:C 16 Dec 19:08:46.489 * Parent agreed to stop sending diffs. Finalizing AOF... 31619:C 16 Dec 19:08:46.489 * Concatenating 95.85 MB of AOF diff received from parent. 31619:C 16 Dec 19:08:49.050 * SYNC append only file rewrite performed: fsync将数据落地到磁盘,最后close文件,将临时文件重命名,确保生成的aof文件完全ok,避免出现aof不完整情况,最后输出这条日志; 31619:C 16 Dec 19:08:49.142 * AOF rewrite: 3864 MB of memory used by copy-on-write:fork子进程做AOF重写,将会耗用3864MB内存作copy on write 25852:M 16 Dec 19:08:49.377 * Background AOF rewrite terminated with success 25852:M 16 Dec 19:08:49.379 * Residual parent diff successfully flushed to the rewritten AOF (1.37 MB) 25852:M 16 Dec 19:08:49.379 * Background AOF rewrite finished successfully

四、两者比较


综上所述,RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。

1.性能:Snapshot方式的性能是要明显高于AOF方式的,原因有两点: 1)采用二进制方式存储数据,数据文件比较小,加载快速. 2)存储的时候是按照配置中的save策略来存储,每次都是聚合很多数据批量存储,写入的效率很好,而AOF则一般都是工作在实时存储或者准实时模式下。相对来说存储的频率高,效率却偏低。

3) 对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

2.数据安全:AOF数据安全性高于Snapshot存储,原因: Snapshot存储是基于累计批量的思想,也就是说在允许的情况下,累计的数据越多那么写入效率也就越高,但数据的累计是靠时间的积累完成的,那么如果在长时间数据不写入RDB,但Redis又遇到了崩溃,那么没有写入的数据就无法恢复了,但是AOF方式偏偏相反,根据AOF配置的存储频率的策略可以做到最少的数据丢失和较高的数据恢复能力。

RDB优势:

1). 备份策略方便:一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。

2). 灾难恢复方便:,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。

3). 性能最大化:对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

4). 启动效率高:相比于AOF机制,如果数据集很大,RDB的启动效率会更高。

RDB缺点:

1). 数据的可靠性不高,没办法做到实时持久化:如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。

2). 影响性能:由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

3)版本兼容RDB格式问题

AOF的优点:

1). 数据安全性:该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。

2). 数据一致性由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。

3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。

4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。

AOF的缺点:

1). 恢复速度慢:对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

2). 性能低:根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。

二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。

五、Redis持久化磁盘IO方式及其带来的问题

1、RDB持久化可能导致系统内存不足导致redis 被 oom kill

发现Redis在物理内存使用比较多,但还没有超过实际物理内存总容量时就会发生不稳定甚至崩溃的问题。

redis持久化AOF重写和RDB写入都是通过fork产生子进程来操作,AOF重写和RDB写入都是通过fork产生子进程来操作,理论上需要两倍的内存来完成持久化操作,但Linux有写时复制机制(copy-on-write),父子进程会共享相同的物理内存页(只有有写入的脏页会被复制), 当父进程处理写请求时会把要修改的页创建副本,而子进程在fork操作过程中共享整个父进程内存快照。避免在大量写入时做子进程重写操作,这样将导致父进程维护大量页副本,造成内存消耗。

但是由于Redis的持久化使用了Buffer IO,所谓Buffer IO是指Redis对持久化文件的写入和读取操作都会使用物理内存的Page Cache, 而大多数数据库系统会使用Direct IO来绕过这层Page Cache并自行维护一个数据的Cache,而当Redis的持久化文件过大(尤其是快照RDB文件),并对其进行读写时,磁盘文件中的数据都会被加载到物理内存中作为操作系统对该文件的一层Cache,而这层Cache的数据与Redis内存中管理的数据实际是重复存储的,虽然内核在物理内存紧张时会做Page Cache的剔除工作,但内核很可能认为某块Page Cache更重要,而让你的进程开始Swap ,这时你的系统就会开始出现不稳定或者崩溃了。我们的经验是当你的Redis物理内存使用超过内存总容量的3/5时就会开始比较危险了。 下图是Redis在读取或者写入快照文件dump.rdb后的内存数据图:

2、持久化Fork引起redis主进程阻塞

AOF重写和RDB写入都是通过fork产生子进程来操作,子进程占用内存大小等同于父进程,理论上需要两倍的内存来完成持久化操作,但Linux有写时复制机制(copy-on-write)。父子进程会共享相同的物理内存页,当父进程处理写请求时会把要修改的页创建副本,而子进程在fork操作过程中共享整个父进程内存快照。避免在大量写入时做子进程重写操作,这样将导致父进程维护大量页副本,造成内存消耗。

测试把benchmark的11G数据写成一个1.3的RDB文件,或者等大的AOF文件rewrite,需要80秒,在redis-cli info中可查看。启动时载入一个AOF或RDB文件的速度与上面写入时相同,在log中可查看。 Fork一个使用了大量内存的进程也要时间,大约10ms per GB的样子,但Xen在EC2上是让人郁闷的239ms (KVM和VMWare貌似没有这个毛病),各种系统的对比,Info指令里的latest_fork_usec显示上次花费的时间。

3、在AOF持久化重写时,AOF重写缓冲区的指令追加新文件和改名替换造成阻塞:

在AOF持久化过程中,所有新来的写入请求依然会被写入旧的AOF文件,同时放到buffer中,当rewrite完成后,会在主线程把这部分内容合并到临时文件中之后才rename成新的AOF文件,所以rewrite过程中会不断打印”Background AOF buffer size: xx MB, Background AOF buffer size: xxMB”,这个合并的过程是阻塞的,如果你产生了280MB的buffer,在100MB/s的传统硬盘 上,Redis就要阻塞2.8秒!!!

4、持久化的系统IO阻塞redis主进程的问题

持久化很容易造成阻塞,不管是AOF rewrite,还是RDB bgsave,都有可能会出现阻塞,原因是一般情况下,如果Redis服务设置了appendfsync everysec, 主进程每秒钟便会调用fsync(), 将数据写到存储磁盘里. 但由于服务器的重写子进程正在进行大量IO操作, 导致主进程fsync()/操作被阻塞, 最终导致Redis主进程阻塞.解决方案这里就不多说了

5Redis持久化性能调整

因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。

如果启用AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。默认超过原大小100%大小时重写可以改到适当的数值,比如之前的 benchmark每个小时会产生40G大小的AOF文件,如果硬盘能撑到半夜系统闲时才用cron调度bgaofrewrite就好了。

如 果不Enable AOF ,仅靠Master-Slave Replication 实现高可用性也可以。能省掉一大笔IO也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启 动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个。新浪微博就选用了这种架构,见Tim的博客

六、 实际问题

1、Trouble Shooting —— Enable AOF可能导致整个Redis被Block住,在2.6.12版之前

现象描述:当AOF rewrite 15G大小的内存时,Redis整个死掉的样子,所有指令甚至包括slave发到master的ping,redis-cli info都不能被执行。

原因分析:

官方文档,由IO产生的Latency详细分析, 已经预言了悲剧的发生,但一开始没留意。 Redis为求简单,采用了单请求处理线程结构。 打开AOF持久化功能后, Redis处理完每个事件后会调用write(2)将变化写入kernel的buffer,如果此时write(2)被阻塞,Redis就不能处理下一个事件。 Linux规定执行write(2)时,如果对同一个文件正在执行fdatasync(2)将kernel buffer写入物理磁盘,或者有system wide sync在执行,write(2)会被block住,整个Redis被block住。 如 果系统IO繁忙,比如有别的应用在写盘,或者Redis自己在AOF rewrite或RDB snapshot(虽然此时写入的是另一个临时文件,虽然各自都在连续写,但两个文件间的切换使得磁盘磁头的寻道时间加长),就可能导致 fdatasync(2)迟迟未能完成从而block住write(2),block住整个Redis。 为了更清晰的看到fdatasync(2)的执行时长,可以使用”strace -p (pid of redis server) -T -e -f trace=fdatasync”,但会影响系统性能。 Redis 提供了一个自救的方式,当发现文件有在执行fdatasync(2)时,就先不调用write(2),只存在cache里,免得被block。但如果已经 超过两秒都还是这个样子,则会硬着头皮执行write(2),即使redis会被block住。此时那句要命的log会打印:“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.” 之后用redis-cli INFO可以看到aof_delayed_fsync的值被加1。 因此,对于fsync设为everysec时丢失数 据的可能性的最严谨说法是:如果有fdatasync在长时间的执行,此时redis意外关闭会造成文件里不多于两秒的数据丢失。如果fdatasync 运行正常,redis意外关闭没有影响,只有当操作系统crash时才会造成少于1秒的数据丢失。 解决方法: 最后发现,原来是AOF rewrite时一直埋头的调用write(2),由系统自己去触发sync。在RedHat Enterprise 6里,默认配置vm.dirty_background_ratio=10,也就是占用了10%的可用内存才会开始后台flush,而我的服务器有64G 内存。很明显一次flush太多数据会造成阻塞,所以最后果断设置了sysctl vm.dirty_bytes=33554432(32M),问题解决。

然后提了个issue,AOF rewrite时定时也执行一下fdatasync嘛, antirez三分钟后就回复了,新版中,AOF rewrite时32M就会重写主动调用fdatasync。

2、Redis log 一直在报错:background: fork: Cannot allocate memory

Redis数据回写机制

数据回写分同步和异步两种方式

  • 同步回写(SAVE), 主进程直接向磁盘回写数据. 在数据量大的情况下会导致系统假死很长时间
  • 异步回写(BGSAVE), 主进程fork后, 复制自身并通过这个新的进程回写磁盘, 回写结束后新进程自行关闭.

由于 BGSAVE 不需要主进程阻塞, 系统也不会假死, 一般会采用 BGSAVE 来实现数据回写.

故障分析

在小内存的进程上做fork, 不需要太多资源. 但当这个进程的内存空间以G为单位时, fork就成为一件很恐怖的操作. 在16G内存的足迹上fork 14G的进程, 系统肯定Cannot allocate memory. 主机的Redis 改动的越频繁 fork进程也越频繁, 所以一直在Cannot allocate memory

解决方案:

1、redis有个默认的选项: stop-writes-on-bgsave-error yes 这个选项默认情况下,如果在RDB snapshots持久化过程中出现问题,设置该参数后,Redis是不允许用户进行任何更新操作。

不彻底的解决方式是,将这个选项改为false,但是这样只是当redis写硬盘快照出错时,可以让用户继续做更新操作,但是写硬盘仍然是失败的;

2、彻底的解决方式:直接修改内核参数 vm.overcommit_memory

直接修改内核参数 vm.overcommit_memory = 1, Linux内核会根据参数vm.overcommit_memory参数的设置决定是否放行。

编辑/etc/sysctl.conf ,改vm.overcommit_memory=1

vm.overcommit_memory = 1,直接放行 vm.overcommit_memory = 0:则比较 此次请求分配的虚拟内存大小和系统当前空闲的物理内存加上swap,决定是否放行。 vm.overcommit_memory = 2:则会比较进程所有已分配的虚拟内存加上此次请求分配的虚拟内存和系统当前的空闲物理内存加上swap,决定是否放行。

执行sysctl -p使其生效;

vm.overcommit_memory 的作用

Linux对大部分申请内存的请求都回复"yes",以便能跑更多更大的程序。因为申请内存后,并不会马上使用内存,将这些不会使用的空闲内存分配给其它程序使用,以提高内存利用率,这种技术叫做Overcommit。一般情况下,当所有程序都不会用到自己申请的所有内存时,系统不会出问题,但是如果程序随着运行,需要的内存越来越大,在自己申请的大小范围内,不断占用更多内存,直到超出物理内存,当linux发现内存不足时,会发生OOM killer(OOM=out-of-memory)。它会选择杀死一些进程(用户态进程,不是内核线程,哪些占用内存越多,运行时间越短的进程越有可能被杀掉),以便释放内存。 当oom-killer发生时,linux会选择杀死哪些进程?选择进程的函数是oom_badness函数(在mm/oom_kill.c中),该函数会计算每个进程的点数(0~1000)。点数越高,这个进程越有可能被杀死。每个进程的点数跟(/proc/<pid>/oom_adj)oom_score_adj有关,而且oom_score_adj可以被设置(-1000最低,1000最高)。

当发生oom killer时,会将记录在系统日志中/var/log/messages

0 人点赞