Redis核心技术与实战【学习笔记】 - 2.Redis数据高可靠原理

总所周知,Redis 绝大部分事业场景是当做缓存使用,但是有个问题是绝对不能忽略的:一旦服务器宕机,内存中的数据将全部丢失。虽然从后端数据库可以恢复这些数据,但是这种方式存在两个问题:一是需要频繁访问数据库,给数据库带来巨大压力;二是这些数据是从慢速数据库中读取出来的,性能肯定比不上从 Redis 中读取,导致使用这些数据的应用程序响应变慢。所以,对于 Redis 来说,实现数据持久化,避免从后端数据库中进行恢复,是至关重要的。

目前,Redis 的持久化主要有两大机制,及 AOF 日志和 RDB 快照。

1.AOF 日志(Redis 宕机了如何避免数据丢失?)

我们熟悉的是数据库的写前日志(Write Ahead Log,WAL),也就是在实际写数据前,先把修改的数据记录到日志文件中,以便故障时进行恢复。而 AOF 正好相反,它是写后日志,也就是先执行命令,把数据写入内存,然后才记录日志。
Redis核心技术与实战【学习笔记】 - 2.Redis数据高可靠原理_第1张图片
为什么 AOF 要先执行命令再写日志呢?
我们以 Redis 收到 “set testkey testvalue” 命令后记录的日志为例,先看看 AOF 文件内容。

  • *3 表示当前命令有三个部分,每个部分都是由 $数字 开头,后面紧跟着具体的命令、键、值。
  • $数字 中的数字表示这部分中的命令、键、值一共有多少字节。
  • 例如 $3set,表示这部分有3个字节,也就是 set 命令。
    Redis核心技术与实战【学习笔记】 - 2.Redis数据高可靠原理_第2张图片
    Redis 为了避免额外的检查开销,在向 AOF 里记录日志的时候,并不会先去对这些命令进行语法检查。所以,先记录日志再执行的话,日志中就有可能记录了错误的命令。而后写这种方式则是先让系统执行命令,只有成功的命令才会被记录到日志中,否则,系统就会直接向客户端报错。
    除此之外,AOF 后写还有一个好处,就是在命令执行完后才记录日志,不会阻塞当前的写操作

不过,AOF 也有两个潜在风险:

  • 如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复;但是,Redis 如果是用作数据库的话,此时,就无法用日志进行恢复了。
  • AOF 虽然避免了对当前命令的阻塞,但是可能会给下一个操作带来阻塞风险

仔细分析,你会发现,这两个风险都是和 AOF 写回磁盘的时机相关。

1.1 三种写回策略

AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendsync 的三个可选值。

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

针对避免主线程阻塞和减少数据丢失问题,这三种写回策略都无法做到两全其美,我们分析下:

  • 同步写回可以基本做到数据不丢失,但它在没一个写命令后面都有一个慢速的落盘操作,不可避免地影响主线程性能。
  • 虽然“操作系统控制写回”在写完缓冲区后,就可以继续执行命令,但是落盘的时机已经不在Redis中了,只要 AOF 记录没有写回磁盘,一旦宕机,数据就丢失了。
  • “每秒写回”采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果宕机,则可能会丢失上一秒未落盘的命令。所以,这只能算是在避免主线程性能和避免丢失数据两者间去了个折中。
配置项 写回时机 优点 缺点
Always 同步写回 可靠性高,数据基本不缺失 每个命令都要落盘,性能影响较大
Everysec 每秒写回 性能适中 宕机时丢失1秒内的数据
No 操作系统控制的写回 性能好 宕机时丢失数据较多

但是,并不是按照性能需求和数据可靠性需求选定了 AOF 的写回策略,就“高枕无忧”了。随着接收的命令越来越多,AOF 文件会越来越大。我们要小心 AOF 文件过大带来的性能问题:一是,文件系统本身对文件大小有限制;二是,如果文件太大,之后再往里追加命令记录的话,效率也变低;三是,如果宕机,AOF 中记录的命令要一个个的被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程会非常慢,这就会影响到 Redis 的正常使用。

1.2 日志文件太大了怎么办? AOF 重写

AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件。读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。比如说,当读取了键值对“testkey”: “testvalue”之后,重写机制会记录 set testkey testvalue 这条命令。这样,当需要恢复时,可以重新执行该命令,实现“testkey”: “testvalue”的写入。

为什么重写机制可以把日志文件变小?实际上,重写机制具有“多变一”功能。也就是将就日志文件中的多条命令,在重写后的新日志文件中变成一条命令。
AOF 文件是以追加方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。但是,在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。

Redis核心技术与实战【学习笔记】 - 2.Redis数据高可靠原理_第3张图片
不过,虽然 AOF 重写后,日志文件会缩小,但是,要把整个数据库最新数据的操作日志都写回磁盘,仍是一个非常耗时的过程。这时,我们要关注重写会不会阻塞主线程。

1.3 AOF 重写会阻塞吗?

和 AOF 日志由主线程写回不同,重写过程是由后台子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程。

重写的过程可总结为“一处拷贝,两处日志”。

“一个拷贝”是指每次重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里包含了最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。

“两处日志”是什么?

  • 第一处日志就是指正在使用的 AOF 日志。因为主线程未阻塞,仍可以处理新来的操作。此时,若有写操作,Redis 会把这个操作写到第一处日志。这样一来即使宕机了,这个 AOF 日志仍然是完整的,可以用于回复。
  • 第二处日志就是指新的重写日志。这个操作也会被写道重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。待重写完成后,缓冲区记录的这些新操作也会写入新 AOF 文件,以保证最新状态的记录。此时,就可以用新的 AOF 文件代替旧文件了。
    Redis核心技术与实战【学习笔记】 - 2.Redis数据高可靠原理_第4张图片

2.RDB 内存快照 (宕机后,Redis如何实现快速恢复?)

AOF 方法进行故障恢复的时候,需要逐一把操作日志都执行一遍。如果操作日志非常多,Redis 就会恢复的很缓慢,影响到正常功能。那么有没有既可以保证可靠,还能再宕机时实现快速恢复的其他方法呢?

当然有了,这就是 Redis 的另一种持久化方法: 内存快照。所谓内存快照,就是指内存中的数据在某一个时刻的状态记录。

对于 Redis 来说,它实现类似照片记录效果的方式,即把某一时刻的状态以文件的形式写到磁盘上。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件称为 RDB 文件。

与 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以在做数据恢复时直接把 RDB 文件读入内存,很快地完成恢复。

听起来这好像不错,但内存快照也并不是最优选项,我们还要考虑两个关键问题:

  • 对哪些数据做快照?这关系到快照的执行效率问题;
  • 做快照时,数据还能被增删改吗?这关系到 Redis 是否被阻塞。

2.1 给哪些数据做快照

Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中。给内存的权力数据做快照,把他们全部写入磁盘也会花费很多时间。而且,全量数据越多,RDB 文件越大,往磁盘上写数据的时间开销越大。

对 Redis 来说,单线程模型就决定了,我们要尽量避免会阻塞主线程的操作,所以针对任何操作,我们都会提出一个灵魂拷问:“它会阻塞主线程吗?”。同理,RDB 文件的的生成是否会阻塞主线程,会关系到是否会降低 Redis 的性能。

Redis 提供了两个命令来生产 RDB 文件,分别是 save 和 bgsave。

  • save 在主线程中执行,会阻塞主线程。
  • bgsave 创建一个子进程,专门用于写入 RDB,避免了主线程的阻塞,这也是 RDB 文件生成的默认配置。

接下里,要关注的问题是在对内存数据做快照时,这些数据还能“动”吗?也就是说这些数据还能被修改吗?

2.2 快照时数据能修改吗?

Redis 借助操作系统提供的写时复制技术(Copy-On-Write,COW),在执行快照的同时,正常处理些操作。

简单来说, bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。

此时,如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么主线程和 bgsave 子进程相互不影响。但是,若主线程要修改一块数据(例如图中的 C),那么,这块数据就会被赋值一份,生成该数据的副本(键值对 C’)。然后主线程在这个数据副本上进行修改。同时 bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。
Redis核心技术与实战【学习笔记】 - 2.Redis数据高可靠原理_第5张图片
这样既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

到这里,我们就解决了对“哪些数据做快照”以及“做快照时数据能否修改”这两大问题:Redis 会使用 bgsave 对当前内存中的所有数据做快照,这个操作是子进程完成的,这就允许主线程同时可以修改数据。

再来看另一个问题:多久做一次快照?我们在拍照的时候,还有项技术叫“连拍”,可以记录人或物连续多个瞬间的状态。那么,快照也适合“连拍”吗?

2.3 可以每秒做一次快照吗?

对于快照来说,所谓“连拍”就是指连续地做快照。这样一来,快照的间隔时间变得很短,及时某一刻发生宕机了,因为上一时刻快照刚刚执行完,丢失的数据也不会太多。但是这其中的快照间隔时间就很关键了。

如下所示,我们在 T0 时刻做了一次快照,然后又在 T0 + t 时刻做了一次快照,在这期间,数据库 5 和 9 被修改了。 如果在 t 这段时间内,机器宕机了,那么,只能按照 T0 时刻的快照进行恢复。此时,数据 5 和 9 的修改值就无法恢复了。
Redis核心技术与实战【学习笔记】 - 2.Redis数据高可靠原理_第6张图片
所以,要想尽可能恢复数据, t 值就要尽可能小,t 越小,就越像“连拍”。那么 t 值可以小到什么程度呢,是不是每秒可以做一次快照?

这种想法其实是错误的。虽然 bgsave 执行时不阻塞主线程,但是,如果频繁的执行全量快照,也会带来两方面的开销

  • 一方面,频繁的将全量数据写入磁盘,会给磁盘带来压力,多个快照竞争有限的磁盘带宽,前一个快照还没有昨晚,后一个又开始做了,容易造成恶性循环。
  • 另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会在阻塞主线程,但是 fork 这个过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。

有没有什么好的办法?

我们可以做增量快照,即做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样就可以避免每次全量快照的开销。

在昨晚全量快照后, T1 和 T2 时刻如果再做快照,我们只需将被修改的数据接入快照文件就行。但是,这么做的前提是要记住哪些数据被修改了。“记住”这个功能,需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。如下图所示:
Redis核心技术与实战【学习笔记】 - 2.Redis数据高可靠原理_第7张图片

想象一下,我们对每个键值对的修改都做记录,那么若有1万个被修改的键值对,我们就需要有1万条额外的记录。
并且有的时候键值对非常小,比如只有 32 字节,而记录它被修改的元数据信息,可能需要 8 字节。这样的话,为了“记住”修改这个功能,引入的额外空间开销比较大。这对于内存资源宝贵的 redis 来说,得不偿失。

讲到这,我们发现,虽然根 AOF 相比,快照的恢复速度快,但是快照的频率不好把我,如果太低,两次快照间一旦宕机,就可能会丢失较多的数据。如果频率太高,又产生额外开销。有没有什么办法既能利用 RDB 的快速恢复,又能已较小的开销做到尽量少丢数据呢?

Redis 4.0 中提出了混合使用 AOF 日志和内存快照的方法。即,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有操作。

这样一来,快照不需要很频繁的执行,避免了 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,不需要记录所有的操作,就不会出现文件过大的问题,也可以避开重写开销。

如下图所示, T1 和 T1 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了。

Redis核心技术与实战【学习笔记】 - 2.Redis数据高可靠原理_第8张图片

这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势,颇有点“鱼和熊掌可以兼得”的感觉。

最后,关于 AOF 和 RDB 的选择问题,我想再给你提三点建议:

  1. 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择
  2. 如果允许分钟级别的数据丢失,可以只使用 RDB
  3. 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡

你可能感兴趣的:(Redis核心技术学习,redis,数据高可靠)