架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?

目录

最简单的场景:Redis单机版

数据持久化:有备无患

主从复制:多副本

哨兵:故障自动切换

分布式系统领域中的「共识算法」

分片集群:横向扩展

总结


最简单的场景:Redis单机版

首先,我们从最简单的场景开始。

假设目前你正在开发一个业务应用,希望通过引入 Redis 来提升应用的性能。

在这种情况下,你可以选择部署一个单机版的 Redis 来使用,如下图所示:

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第1张图片

这个架构非常简单:将 Redis 当做缓存来使用。

一般来讲,都是Cache Aside模式:从 MySQL 中查询数据,然后写入到 Redis 中,接着业务应用再从 Redis 中读取这些数据。

这个架构,也就是我们常用的缓存架构:由于 Redis 的数据都是存储在内存中的,因此这种数据读写速度非常快。

然而,在某一天,由于某些原因,你的 Redis 服务器突然宕机了。

这时你的所有业务流量,都会打到后端 MySQL 上,这会导致你的 MySQL 压力剧增,严重的话甚至会压垮 MySQL。

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第2张图片

面对这种情况,你会如何应对呢?

首先应对措施是迅速重启 Redis,使其能够重新提供服务。

然而,由于先前 Redis 中的数据都存储在内存里,重新启动了 Redis,之前的数据也都丢失了。

尽管重启后的 Redis 能够正常运行,但由于其中没有任何数据,业务流量仍然会被导向后端 MySQL,导致 MySQL 的压力依然巨大。

面对这种困境,你该如何是好?你陷入了沉思。 有没有可行的方案来解决这个问题呢?

既然 Redis 只将数据保存在内存中,那么是否可以考虑将这些数据也写一份到磁盘上呢?

如果采用这种方式,当 Redis 重启时,我们把磁盘中的数据快速恢复到内存中,这样它就可以继续正常提供服务了。

是的,这是一个非常好的解决方案,将内存数据写入磁盘的过程,我们称之为「数据持久化」。

数据持久化:有备无患

现在,你设想的 Redis 数据持久化是这样的:

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第3张图片

但是,数据持久化具体应该怎么做呢?

我猜你最容易想到的一个方案是,Redis 每一次执行写操作,除了写内存之外,同时也写一份到磁盘上,就像这样:

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第4张图片

没错,这是最简单直接的方案。

注意,这里用的是写后日志。

从架构的方法论上,保证数据的高可靠方式,一般有写前日志(Write Ahead Log, WAL) 和 写后日志 两种架构方案。

写前日志 VS 写后日志

写前日志(Write Ahead Log, WAL):先记录日志,再执行「写」指令请求,具体来说,在实际写数据之前,将修改的数据写到日志文件中,故障恢复得以保证。

写后日志:先执行「写」指令请求,将数据写入内存,再记录日志。

写前日志(Write Ahead Log, WAL)的案例:比如 MySQL Innodb 存储引擎 中的 redo log(重做日志)便是记录修改的数据日志,在实际修改数据前,先记录修改日志再执行修改数据。

与MySQL相反,Redis 用的是 写后日志 的架构方案。

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第5张图片

写后日志(先写内存,后写日志)的好处与风险

(1) 好处

  • 不阻塞当前写操作;Redis 是 ap型的 组件, 性能第一, 写操作不能阻塞, 日志记录可以异步进行。

  • 无需进行日志检查,往AOF写日志不需要检查命令正确性。若先写日志再执行命令,使用日志恢复时可能报错;

(2) 写后风险

  • 命令执行完成后,如果写日志前宕机,则会丢数据;

但是,仔细考虑一下,这个方案存在一个问题:客户端的每次写操作,既要写内存,又要写磁盘,而磁盘写入的时间显然要比内存写入长得多!这无疑会对 Redis 的性能产生影响。

如何规避这个问题?

我们可以这样优化:采用异步架构, Redis 写内存由主线程来做,写内存完成后就给客户端返回结果,然后 Redis 用另一个线程去写磁盘,这样就可以避免主线程写磁盘对性能的影响。

这确实是一个好方案。

除此之外,我们还可以从另一个角度思考:有哪些其他方法可以实现数据持久化呢?

我们可以减少写入磁盘的次数,减少磁盘IO的次数,来提升性能。策略是:采用定期全量写入磁盘,这种方式,也叫「数据快照」。

那么,什么是数据快照呢?

简单来说,你可以这样理解:

  1. 将 Redis 看作是一个水杯,向 Redis 写入数据,就相当于往这个杯子里倒水。

  2. 此时你用相机给这个水杯拍一张照片,拍照的瞬间,照片中记录的这个水杯中水的容量,就是这个水杯的数据快照。架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第6张图片

也就是说,Redis 的数据快照,是记录某一时刻下 Redis 中的数据,然后只需要把这个数据快照写到磁盘上即可。

它的优势在于,只有在需要持久化时,才会将数据「一次性」写入磁盘,其他时间则无需对磁盘进行操作。

基于这个方案,我们可以定时给 Redis 做数据快照,把数据持久化到磁盘上。

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第7张图片

其实,上面说的这些持久化方案,就是 Redis 的「RDB」和「AOF」:

  • RDB:只将某一时刻的数据快照持久化到磁盘上(通过创建一个子进程来实现), 这个就是 数据快照架构

  • AOF:每次写操作都会持久化到磁盘上(主线程负责写内存,根据策略可以配置由主线程还是子线程来进行数据持久化), 这个是 写后日志 架构

另外,它们还有以下特点:

  1. RDB 使用二进制 + 数据压缩的方式写入磁盘,因此文件体积较小,数据恢复速度也较快。

  2. AOF 记录每次写命令,数据最完整,但文件体积较大,数据恢复速度较慢。

如果你需要选择持久化方案,可以根据以下原则进行选择:

  1. 如果你的业务对数据丢失不敏感,可以选择 RDB 方案来持久化数据。

  2. 如果你的业务对数据完整性要求较高,可以选择 AOF 方案来持久化数据。

假设你的业务对 Redis 数据完整性要求较高,选择了 AOF 方案,那么你可能会遇到以下问题:

  1. AOF 记录每一次写操作,随着时间的推移,AOF 文件体积会逐渐增大。

  2. 这么大的 AOF 文件,在数据恢复时会变得非常缓慢。

这怎么办?

数据完整性要求变高了,恢复数据也变困难了?有没有什么方法,可以缩小文件体积?提升恢复速度呢?

我们继续来分析 AOF 的特点。

由于 AOF 文件中记录了每次写操作,但对于同一个 key 可能会发生多次修改,我们只保留最后一次修改的值是否可行呢?

是的,这就是我们常听到的「AOF rewrite」,你也可以将其理解为 AOF 的「瘦身」。

我们可以对 AOF 文件定时 rewrite,避免这个文件体积持续膨胀,这样在恢复时就可以缩短恢复时间了。

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第8张图片

深入思考一下,能否找到方法进一步减小 AOF 文件?

回顾一下我们前面讲到的,RDB 和 AOF 各自的特点:

  1. RDB 以二进制 + 数据压缩方式存储,文件体积小

  2. AOF 记录每一次写命令,数据最全

我们能否充分利用它们各自的优势呢?

当然可以,这就是 Redis 的「混合持久化」

具体来说,当 AOF rewrite 时,Redis 先以 RDB 格式在 AOF 文件中写入一个数据快照,再把在这期间产生的每一个写命令,追加到 AOF 文件中。

因为 RDB 是二进制压缩写入的,这样 AOF 文件体积就变得更小了。

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第9张图片

此时,你在使用 AOF 文件恢复数据时,恢复时间将会缩短!

需要注意的是,Redis 4.0 以上版本才支持混合持久化。

经过这样的优化,你的 Redis 再也无需担心实例宕机了,当发生宕机时,你就可以用持久化文件快速恢复 Redis 中的数据。

然而,这样就没问题了吗?

仔细想想,虽然我们已经将持久化文件优化到最小,但在恢复数据时仍需要时间,在这段时间内你的业务应用仍会受到影响,那该怎么办呢?

我们来探讨一下是否有更好的解决方案。

一个实例宕机,只能用恢复数据来解决,那我们是否可以部署多个 Redis 实例,然后让这些实例数据保持实时同步,这样当一个实例宕机时,我们可以在剩下的实例中选择一个继续提供服务。

没错,这个方案就是接下来要讲的「主从复制:多副本」。

主从复制:多副本

此时,你可以部署多个 Redis 实例,架构模型就变成了这样:

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第10张图片

我们这里把实时读写的节点叫做 master,另一个实时同步数据的节点叫做 slave。

采用多副本的方案,它的优势是:

  1. 缩短不可用时间:master 发生宕机,我们可以手动把 slave 提升为 master 继续提供服务

  2. 提升读性能:让 slave 分担一部分读请求,从而提高应用的整体性能。架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第11张图片

这个方案看起来不错,既节省了数据恢复时间,又提高了性能。

那么它有什么问题吗?

它的问题在于:当 master 宕机时,我们需要「手动」把 slave 提升为 master,这个过程也是需要花费时间的。

虽然比恢复数据要快得多,但还是需要人工介入处理。一旦需要人工介入,就必须要算上人的反应时间、操作时间,所以,在这期间你的业务应用依旧会受到影响。

那么如何解决这个问题呢?

我们是否可以把这个切换的过程,变成自动化呢?

针对这种情况,我们需要一个「故障自动切换」机制,这正是我们经常听到的「哨兵」所具备的能力。

哨兵:故障自动切换

现在,我们可以引入一个「观察者」,让这个观察者去实时监测 master 的健康状态,这个观察者就是「哨兵」。

那么,具体如何实施呢?

  1. 哨兵每间隔一段时间,询问 master 是否正常

  2. master 正常回复,表示状态正常,回复超时表示异常

  3. 哨兵发现异常,发起主从切换架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第12张图片

有了这个方案,就无需人去介入处理了,一切就变得自动化了,是不是很棒?

然而,还存在一个问题:

如果 master 状态正常,但这个哨兵在询问 master 时,它们之间的网络发生了问题,那这个哨兵可能会误判。

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第13张图片

那么,如何解决这个问题呢?

答案是,我们可以部署多个哨兵,让它们分布在不同的机器上,它们一起监测 master 的状态,流程就变成了这样:

  1. 多个哨兵每间隔一段时间,询问 master 是否正常

  2. master 若正常回复,则表示其状态良好,若回复超时,则表示存在异常

  3. 一旦有一个哨兵判定 master 异常(不管是否是网络问题),就询问其它哨兵,如果多个哨兵(设置一个阈值)都认为 master 异常了,这才判定 master 确实发生了故障

  4. 多个哨兵经过协商后,判定 master 故障,则发起主从切换

因此,我们用多个哨兵互相协商来判定 master 的状态,这样一来,就可以大大降低误判的概率。

在哨兵协商判定 master 异常后,这里还有一个问题:由哪个哨兵来发起主从切换呢?

答案是,选出一个哨兵「领导者」,由这个领导者进行主从切换。

问题又来了,这个领导者怎么选?

设想一下,在现实生活中,选举是怎么做的?是的,投票。

在选举哨兵领导者时,我们可以制定这样一个选举规则:

  1. 每个哨兵都询问其它哨兵,请求对方为自己投票

  2. 每个哨兵只投票给第一个请求投票的哨兵,且只能投票一次

  3. 首先拿到超过半数投票的哨兵,当选为领导者,发起主从切换

其实,这个选举的过程就是我们经常听到的:分布式系统领域中的「共识算法」。

分布式系统领域中的「共识算法」

什么是共识算法?

我们在多个机器部署哨兵,它们需要共同协作完成一项任务,所以它们就组成了一个「分布式系统」。

在分布式系统领域,多个节点如何就一个问题达成共识的算法,就叫共识算法。

在这个场景下,多个哨兵共同协商,选举出一个都认可的领导者,就是使用共识算法完成的。

这个算法还规定节点的数量必须是奇数个,这样可以保证系统中即使有节点发生了故障,剩余超过「半数」的节点状态正常,依旧可以提供正确的结果,也就是说,这个算法还兼容了存在故障节点的情况。

共识算法在分布式系统领域有很多,例如 Paxos、Raft,哨兵选举领导者这个场景,使用的是 Raft 共识算法,因为它足够简单,且易于实现。

现在,我们用多个哨兵共同监测 Redis 的状态,这样一来,就可以避免误判的问题了,架构模型就变成了这样:

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第14张图片

好了,到这里我们先小结一下。

你的 Redis 从最简单的单机版,经过数据持久化、主从多副本、哨兵集群这一系列优化,你的 Redis 不管是性能还是稳定性,都越来越高,即使节点出现故障,也无需担心。

你的 Redis 以这样的架构模式部署,基本上就可以稳定运行很长时间了。

然而,随着业务的发展,你的业务量开始迅速增长,此时你的架构模型,还能承受这么大的流量吗?

我们一起来分析一下:

  1. 稳定性:Redis 故障宕机,我们有哨兵 + 副本,可以自动完成主从切换

  2. 性能:读请求量增长,我们可以再部署多个 slave,实现读写分离,分担读压力

  3. 性能:写请求量增长,但我们只有一个 master 实例,这个实例达到瓶颈怎么办?

当你的写请求量越来越大时,单个 master 实例可能无法承受这么大的写流量。

要解决这个问题,此时你需要考虑使用「分片集群」。

分片集群:横向扩展

什么是「分片集群」?

简而言之,如果一个实例扛不住写压力,那我们是否可以部署多个实例,然后把这些实例按照一定规则组织起来,把它们当成一个整体,对外提供服务,这样不就可以解决集中写一个实例的瓶颈问题吗?

所以,现在的架构模型就变成了这样:

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第15张图片

现在问题又来了,那么多实例应该如何组织呢?

我们制定规则如下:

  1. 每个节点各自存储一部分数据,所有节点数据之和才是全量数据

  2. 制定一个路由规则,对于不同的 key,把它路由到固定一个实例上进行读写

而根据路由规则所在的位置,分片集群可以分为两大类:

  1. 客户端分片

  2. 服务端分片

客户端分片指的是,将 key 的路由规则放在客户端来做,就是下面这样:

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第16张图片

这个方案的缺点是,客户端需要维护这个路由规则,也就是说,你需要将路由规则写到你的业务代码中。

如何在不将路由规则与业务代码耦合的情况下实现?

你可以这样优化,将这个路由规则封装成一个模块,当需要使用时,集成这个模块就可以了。

这正是 Redis Cluster 的采用的方案。

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第17张图片

Redis Cluster 内置了哨兵逻辑,无需额外部署哨兵。

在使用 Redis Cluster 的过程中,你的业务应用需依赖相应的 Redis SDK,该 SDK 已内置路由规则,无需手动编写。

接下来,我们来看服务端分片的实现。

该方案是指将路由规则不放在客户端处理,而是在客户端与服务端之间添加一个「中间代理层」,这个代理就是我们常听说的 Proxy。

而数据的路由规则,就放在这个 Proxy 层来维护。

这样一来,你就无需关心服务端有多少个 Redis 节点了,只需要和这个 Proxy 进行交互即可。

Proxy 会根据路由规则将你的请求转发至相应的 Redis 节点。

并且,当集群实例无法承受更大的流量请求时,还可以横向扩展,通过添加新的 Redis 实例以提升性能。这一切对于你的客户端而言,都是透明且无感知的。

业界开源的 Redis 分片集群方案,例如 Twemproxy、Codis,就是采用了这种方案。

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第18张图片

在进行数据扩容时,分片集群涉及许多细节,但这不是本文的重点,因此暂时不进行详细讨论。

如今,采用分片集群后,你可以从容应对更大的流量压力了!

总结

让我们回顾一下,我们是如何演进一个稳定且高性能的 Redis 集群的。

首先,在使用最简单的单机版 Redis 时,我们遇到了 Redis 故障宕机后数据无法恢复的问题,因此我们引入了「数据持久化」,将内存中的数据保存到磁盘上,以便 Redis 重启后能快速恢复数据。

在进行数据持久化时,我们面临如何更高效地将数据保存到磁盘的问题。后来我们发现 Redis 提供了 RDB 和 AOF 两种方案,分别对应数据快照和实时命令记录。当对数据完整性要求不高时,可以选择 RDB 持久化方案;如果对数据完整性要求较高,可以选择 AOF 持久化方案。

但是我们又发现,AOF 文件体积会随着时间增长变得越来越大,此时我们想到的优化方案是,使用 AOF rewrite 的方式对其进行瘦身,减小文件体积,再后来,我们发现可以结合 RDB 和 AOF 各自的优势,在 AOF rewrite 时使用两者结合的「混合持久化」方式,又进一步减小了 AOF 文件体积。

接着,我们发现虽然可以通过数据恢复的方式还原数据,但恢复数据仍需要花费时间,这意味着业务应用仍会受到影响。我们进一步优化,采用「多副本」的方案,让多个实例保持实时同步,当一个实例故障时,可以手动把其他实例提升上来继续提供服务。

但是这样也有问题,手动提升实例上来,需要人工介入,人工介入操作也需要时间,我们开始寻找方法使这个流程自动化,因此我们引入了「哨兵」集群。哨兵集群通过互相协商的方式,发现故障节点,并可以自动完成切换,从而大幅降低对业务应用的影响。

最后,我们将关注点放在如何支持更大的写流量上,因此引入了「分片集群」来解决这个问题,让多个 Redis 实例分担写压力。面对更大的流量,我们还可以添加新的实例进行横向扩展,进一步提高集群性能。

通过这些步骤,我们的 Redis 集群能够长期稳定、高性能地为我们的业务提供服务。

在架构演进的过程中, 围绕着「架构设计」的核心思想:

  • 高性能:读写分离、分片集群

  • 高可用:数据持久化、多副本、故障自动切换

  • 易扩展:分片集群、横向扩展

  • 高可靠:写后日志、数据快照

当我们提及哨兵群体、分片群体时,还涉及到了「分布式系统」相关的知识:

  • 分布式共识:哨兵领导者选举

  • 负载均衡:分片集群数据分片、数据路由

这里用一个思维导图,方便你更好地去理解它们之间的关系,以及演化的过程。

架构设计内容分享(五十九):Redis怎么做高可用、高并发架构?_第19张图片

当然,除了 Redis 之外,对于构建任何一个数据集群,你都可以沿用这个思路去思考、去优化,看看它们到底是如何做的。

例如在使用 MySQL 时,你可以思考 MySQL 与 Redis 的差异,以及 MySQL 是如何实现高性能和高可用的。其实思路都是类似的。

如今我们随处可见分布式系统、数据群体,希望通过这篇文章,你可以理解这些软件是如何逐步演进而来的,在演化过程中,它们遇到了哪些问题,为了解决这些问题,这些软件的设计者提出了怎样的方案,做出了哪些取舍?

实际上,这个思考过程也是进行「架构设计」的思路。

在进行软件架构设计时,您面临的场景是发现问题、分析问题、解决问题,逐步优化和升级您的架构,最终在性能和可靠性方面达到平衡。尽管各种软件层出不穷,但架构设计的理念不会改变,希望您真正吸收的是这些思想,这样才能做到以不变应万变。

 

你可能感兴趣的:(架构设计,内容分享,架构)