分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记

前言

本文主要内容:

分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记_第1张图片
来源于《深入分布式缓存:从原理到实践》第八章 分布式Redis的读书笔记。

数据存储系统的挑战和Redis的应对策略

Redis作为数据存储系统,无论数据存储在内存中还是持久化到本地,作为单实例节点,在实际应用中总会面临如下挑战:

  • 数据量伸缩:单实例Redis存储的key-value对的数量受限于单机的内存和磁盘容量。长期运行的生产环境中,随着数据不断地加入,存储容量会达到瓶颈。虽然Redis提供了key的过期机制,在作为缓存使用时通过淘汰过期的数据可以达到控制容量的目的。但当Redis作为NoSQL数据库时,业务数据长期有效使得淘汰机制不再适用。
  • 访问量伸缩:单实例Redis单线程地运行,吞吐量受限于单次请求处理的平均时耗。当业务数据集面临超过单实例处理能力的高吞吐量需求时,如何提升处理能力成为难点。
  • 单点故障。Redis持久化机制一定程度上缓解了宕机/重启带来的业务数据丢失问题,但当单实例所在的物理节点发生不可恢复故障时,如何保证业务数据不丢以及如何在故障期间迅速地恢复对应业务数据的可用性也成为单点结构的挑战。

分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记_第2张图片

分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记_第3张图片

上述问题对于数据存储系统而言是通用的,基于分布式的解决方案如下:

  • 水平拆分:分布式环境下,节点分为不同的分组,每个分组处理业务数据的一个子集,分组之间的数据无交集。数据无交集的特性使得水平拆分解决了数据量瓶颈,随着分组的增加,单个分组承载的数据子集更小,即通过增加分组来伸缩数据量。同时水平拆分也也解决了访问量瓶颈,业务数据全集的请求被分摊到了不同分组,随着分组数的增加,数据全集的总吞吐量也增加,访问量的伸缩性得以实现。
  • 主备复制:同一份业务数据存在多个副本,对数据的每次访问根据一定规则分发到某一个或多个副本上执行。通过W+R>N的读写配置可以做到读取数据内容的实时性。随着N的增加,当读写访问量差不多时,业务的吞吐量相比单实例会提升到逼近2倍。但实际中,读的访问量常常远高于写的量,W=N, R=1,吞度量会随着读写比例的增加而提升。
  • 故障转移:当业务数据所在的节点故障时,这部分业务数据转移到其他节点上进行,使得故障节点在恢复期间,对应的业务数据仍然可用。显然,为了支撑故障转移,业务数据需要保持多个副本,位于不同的节点上。

水平拆分(sharding)

水平拆分(sharding)为了解决数据量和访问量增加后对单节点造成的性能压力,通常引入水平拆分机制,将数据存储和对数据的访问分散到不同节点上分别处理。水平拆分后的每个节点存储和处理的数据原则上没有交集,使得节点间相互独立;但内部的拆分和多节点通常对外部服务透明,通过数据分布和路由请求的配合,可以做到数据存放和数据访问对水平拆分的适配。

数据分布分布式环境下,有多个Redis实例I[i] (i=0, …, N),同时业务数据的key全集为{k[0], k[1], …, k[M]}。数据分布指的是一种映射关系f,每个业务数据key都能通过这种映射确定唯一的实例I,即f(k)=i,其中k对应的业务数据存放于I[i]。

如何确定这个映射关系的算法?这其实主要取决于Redis的客户端。常用的是映射有3种:

分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记_第4张图片

❑ hash映射。为了解决业务数据key的值域不确定这个问题,引入hash运算,将不可控的业务值域key映射到可控的有限值域(hash值)上,且映射做到均匀,再将有限的均匀分布的hash值枚举地映射到Redis实例上。例如,crc16(key)%16384这个hash算法,将业务key映射到了0~16383这一万多个确定的有限整数集合上,再依据一定规则将这个整数集合的不同子集不相交地划分到不同Redis实例上,以此实现数据分布。

❑ 范围映射。和hash映射不同,范围映射通常选择key本身而不是key的某个函数运算值(如hash运算)作为数据分布的条件,且每个数据节点存放的key的值域是连续的一段范围。例如,当0≤key<100时,数据存放到实例1上;当100≤key<200时,数据存放到实例2上;以此类推。key的值域是业务层决定的,业务层需要清楚每个区间的范围和Redis实例数量,才能完整地描述数据分布。这使得业务域的信息(key的值域)和系统域的信息(实例数量)耦合,数据分布无法在纯系统层面实现,从系统层面看来,业务域的key值域不确定、不可控。

❑ hash和范围结合。典型的方式就是一致性hash,首先对key进行hash运算,得到值域有限的hash值,再对hash值做范围映射,确定该key对应的业务数据存放的具体实例。这种方式的优势是节点新增或退出时,涉及的数据迁移量小——变更的节点上涉及的数据只需和相邻节点发生迁移关系;缺点是节点不是成倍变更(数量变成原有的N倍或1/N)时,会造成数据分布的不均匀。

主备复制(replication)

前述水平拆分讨论如何将数据划分到没有交集的各个数据节点上,即,不同节点间没有相同的数据。而在有的场景下,我们需要将相同的数据存放在多个不同的节点上。例如,当某个节点宕机时,其上的数据在其他节点上有副本,使得该数据对外服务可以继续进行,即,数据复制为后续所述的故障转移提供了基础。

再如,同一份数据在多个节点上存储后,写入节点可以和读取节点分离,提升性能。当一份数据落在了多个不同节点上时,如何保证节点间数据的一致性将是关键问题,在不同的存储系统架构下方案不同,有的采用客户端双写,有的采用存储层复制。Redis采用主备复制的方式保证一致性,即所有节点中,有一个节点为主节点(master)它对外提供写入服务,所有的数据变更由外界对master的写入触发,之后Redis内部异步地将数据从主节点复制到其他节点(slave)上。

主备复制流程

Redis包含master和slave两种节点:master节点对外提供读写服务;slave节点作为master的数据备份,拥有master的全量数据,对外不提供写服务。
主备复制由slave主动触发,主要流程如图所示。

分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记_第5张图片

1)首先slave向master发起SYNC命令。这一步在slave启动后触发,master被动地将新进slave节点加入自己的主备复制集群。
2)master收到SYNC后,开启BGSAVE操作。BGSAVE是Redis的一种全量模式的持久化机制。
3)BGSAVE完成后,master将快照信息发送给slave。
4)发送期间,master收到的来自用户客户端的新的写命令,除了正常地响应之外,都再存入一份到backlog队列。
5)快照信息发送完成后,master继续发送backlog队列信息。
6)backlog发送完成后,后续的写操作同时发给slave,保持实时地异步复制。

在上图的slave侧,处理逻辑如下:
❑ 发送SYNC;
❑ 开始接收master的快照信息,此时,将slave现有数据清空,并将master快照写入自身内存;
❑ 接收backlog内容并执行它,即回放;
❑ 继续接收后续来自master的命令副本并继续回放,以保持数据和master一致。

注意:在slave没有收到master快照文件之前,会根据配置决定使用现有的数据响应客户端还是直接拒绝。

如果有多个slave节点并发发送SYNC命令给master,企图建立主备关系,只要第二个slave的SYNC命令发生在master完成BGSAVE之前,第二个slave将收到和第一个slave相同的快照和后续backlog;否则,第二个slave的SYNC将触发master的第二次BGSAVE。

断点续传

每次当slave通过SYNC和master同步数据时,master都会dump全量数据并发发送。当一个已经和master完成了同步并持续保持了长时间的slave网络断开很短的时间再重新连上时,master不得不重新做一遍全量dump的传送。然而由于slave只断开了很短时间,重连之后master-slave的差异数据很少,全量dump的数据中绝大部分,slave都已经具有,再次发送这些数据会导致大量无效的开销。最好的方式是,master-slave只同步断开期间的少量数据。

Redis的PSYNC(Partial Sync)可以用于替代SYNC,做到master-slave基于断点续传的主备同步协议。master-slave两端通过维护一个offset记录当前已经同步过的命令,slave断开期间,master的客户端命令会保持在缓存中,在slave重连之后,告知master断开时的最新offset, master则将缓存中大于offset的数据发送给slave,而断开前已经同步过的数据,则不再重新同步,这样减少了数据传输开销。

故障转移(failover)

当两台以上Redis实例形成了主备关系,它们组成的集群就具备了一定的高可用性:当master故障时,slave可以成为新的master,对外提供读写服务,这种运营机制称为failover。剩下的问题在于:谁去发现master的故障做failover的决策?

一种方式是,保持一个daemon进程,监控着所有的master-slave节点,如图所示:

分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记_第6张图片

一个Redis集群里有一个master和两个slave,这个daemon进程监视着这三个节点。这种方式的问题在于:daemon作为单点,它本身的可用性无法保证。因此需要引入多daemon,如图所示:

分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记_第7张图片

在图中,为了解决一个daemon的单点问题,我们引入了两个daemon进程,同时监视着三个Redis节点。但是,多个daemon的引入虽然解决了可用性问题,但带来了一致性问题:多个daemon之间,如何就某个master是否可用达成一致?比如,daemon1和master之间的网络不通,但master和其余节点均畅通,那么daemon1和daemon2观察到的master可用状态不同,那么如何决策此时master是否需要failover?

Redis的sentinel提供了一套多daemon间的交互机制,解决故障发现、failover决策协商机制等问题,如图所示。

分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记_第8张图片

多个daemon组成了一个集群,称为sentinel集群,其中的daemon也被称为sentinel节点。这些节点相互间通信、选举、协商,在master节点的故障发现、failover决策上表现出一致性。

sentinel间的相互感知

sentinel节点间因为共同监视了同一个master节点从而相互也关联了起来,一个新加入的sentinel节点需要和有相同监视的master的其他sentinel节点相互感知,方式如下:所有需要相互感知的sentinel都向他们共同的master节点上订阅相同的channel:sentinel:hello,新加入的sentinel节点向这个channel发布一条消息,包含了自己信息,该channel的订阅者们就可以发现这个新的sentinel。随后新sentinel和已有的其他sentinel节点建立长连接。sentinel集群中所有节点两两连接,如图所示:

分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记_第9张图片

新的sentinel节点加入后,它向master节点发布自己加入这个信息,此时现有的订阅sentinel节点将会发现这条消息从而感知到了新sentinel节点的存在。

master的故障发现

sentinel节点通过定期地向master发送心跳包判断其存活状态,称为PING。一旦发现master没有正确地响应,sentinel将此master置为“主观不可用态”,所谓主观,是因为“master不可用”这个判定尚未得到其他sentinel节点的确认。如图所示:

分布式Redis:水平拆分、主备复制、断点续传、故障转移- 《深入分布式缓存:从原理到实践》读书笔记_第10张图片

随后它将“主观不可用态”发送给其他所有的sentinel节点进行确认(即图"is-master-down-by-addr"这条交互),当确认的sentinel节点数>=quorum(可配置)时,则判定为该master为“客观不可用”,随后进入failover流程。

failover决策

当一台master真正宕机后,可能多个sentinel节点同时发现了此问题并通过交互确认了相互的“主观不可用态”猜想,同时达到“客观不可用态”,同时打算发起failover。但是最终只能有一个sentinel节点作为failover的发起者,此时需要开始一个leader选举的过程,选择谁来发起failover。

Redis的sentinel机制采用类似Raft协议实现这个选举算法:

  1. sentinelState的epoch变量类似于raft协议中的term(选举回合);
  2. 每一个确认了master“客观不可用态”的sentinel节点都会向周围广自己参选的请求;
  3. 每一个接收到参选请求的sentinel节点如果还没人向它发送过参选请求,它就将本选举回合的意向置为首个参选sentinel并回复它;如果已经在本回合表过意向了,则拒绝本回合内所有其他的参选请求,并将已有意向回复给参选sentinel;
  4. 每一个发送参选请求的sentinel节点如果收到了超过一半的意向同意某个参选sentinel(也可能是自己),则确定该sentinel为leader;如果本回合持续了足够长的时间还未选出leader,则开启下一个回合。leader sentinel确定之后,从master所有的slave中依据一定的规则选取一个作为新的master,告知其他slave连接这个新的master。

你可能感兴趣的:(技术类书籍读书分享)