8.4 Redis Cluster

Redis3.0后,节点之间通过去中心化的方式提供了完整的sharding、replication(复制机制仍复用原有机制,只是cluster具备感知主备的能力)、failover解决方案、称为Redis Cluster。

即,将proxy/sentinel的工作融合到了普通Redis节点里。

我的总结:分布式去中心化方案需要解决的问题

集群数据分布,集群节点数据交换(Gossip协议)

sharding(slot取模),sharding路由(move,ask),sharding一致性(版本号),数据迁移(importing,migrating),failover(故障感知,确认,迁移,master选举,集群状态变更)

8.4.1  拓扑结构

一个Redis Cluster由多个Redis节点组构成。不同节点组服务的数据无交集,即每一个节点组对应数据sharding的一个分片。

节点组内部分为主备两类节点,对应前章的master和slave节点,两者数据准实时一致,通过异步化的主备复制机制保证。

一个节点组有且仅有一个master节点,同时又0到多个slave节点。只有master节点对用户提供写服务,读服务可以由master或者slave提供。

如下图Redis Cluster节点结构示例图:

 上例key-value数据全集被分为5份,即5个slot(实际Redis Cluster总共16384个slot,每个节点服务一部分slot,详见后续。这里为方便讲述以5个为例)。   A、B为两个master分别负责1/2/3三个slot和4/5两个slot。

上述示例中5个节点间,两两通过Redis Cluster Bus交互,交互交换如下关键信息:

  • 数据分片(slot)和节点的对于关系。
  • 集群中每个节点可用状态
  • 集群结构(配置)发生变更时,通过一定的协议对配置信息达成一致。数据分片的迁移、故障发生时的主备切换决策、单点master的发现和其发生主备关系的变更等场景均会导致集群结构变化。
  • publish/subscribe(发布/订阅)功能在cluster版的内部实现所需要交互的信息。

Redis Cluster Bus通过单独的端口进行连接,由于bus是节点间的内部通信机制,交互的是字节序列化信息,而不是client到Redis服务器的字符序列化以提升交互效率。

Redis Cluster是一个去中心化的分布式实现方案,客户端可以和集群中的任一节点连接,通过后文所述的交互流程,逐渐地获知全集群的数据分片映射关系。

8.4.2  配置的一致性

对于一个去中心化的实现,集群的拓扑结构并不保存在单独的配置节点上,后者的引入同样会带来新的一致性问题。

各自为政的节点间如何就集群的拓扑结构达成一致,是Redis Cluster配置机制解决的问题。

Redis Cluster通过引入两个自增的epoch变量来使得集群配置在各个节点间达成最终一致。

本节首先介绍配置信息的数据结构和交互信息结构,接下来讨论集群以何种方式利用epoch变量来达成配置信息一致。

1.配置信息数据结构

Redis Cluster 中每一个节点(Node)内部都保存了集群的配置信息,这些信息存储在clusterState中,它的结构如图:

 变量语义如下:

  • clusterState:记录了从集群中某个节点的视角看来的集群配置状态。
  • currentEpoch:表示整个集群中的最大版本号,集群信息每变更一次,该版本号都会自增以保证每个信息的版本号唯一
  • nodes:是一个列表,包含了本节点所知的集群所有节点信息(clusterNode),其中也包含本节点自身
  • clusterNode:记录了每个节点的信息,其中比较关键的包括该信息的版本epoch,该版本信息的描述:该节点对应的数据分片(slot)、当该节点为master时的slave节点列表、当该节点为slave时对应的master节点。
  • 每个节点包含一个全局唯一的NodeId
  • 当集群的数据分片信息发生变更,即数据分片在节点组之间迁移的时候,Redis Cluster仍然保持对外服务,在迁移过程中,通过“分片迁移相关状态”的一组变量来管控迁移过程。
  • 当集群中某个master出现宕机时,Redis Cluster会自动发现并触发故障转移的操作,将宕机master的某个slave升级为新的master,这个过程中同样包含一组变量来控制故障转移的一系列过程。

如图可见,每个节点都保存了它的视角看来集群的结构,它描述了数据的分片方式、节点主备关系,并通过epoch作为版本号实现集群结构信息(配置)的一致性,同时也控制着数据迁移和故障转移的过程。

2.信息交互

去中心化的架构不存在统一的配置中心,各个节点对整个集群状态的认知来自于节点间的信息交互。在Redis Cluster中,这个信息交互通过Redis Cluster Bus来完成,后者端口独立。在Redis Cluster Bus上交互的信息结构如图:

 clusterMsg的type字段指明了消息的类型,配置信息的一致性达成主要依靠PING和PONG两种类型的msg,两者除了type不同之外,其余字段信息语义一致,其消息体即上图所示的Gossip数据。

每一个节点向其他所有节点较为频繁地周期性发送PING消息同时接受PONG回应。

交互消息Gossip部分,包含了发送者节点(或响应节点)所知的集群其他节点信息,接收节点根据这些Gossip信息更新自己对于集群结构的认识。对于较大规模的集群,两两频繁交互的PING/PNG包,每次包含整个集群的结构信息将造成极大的网络负担。然而集群结构稳定,即便发送全量数据,其中大部分没有实际用处(和已有信息相同)。作为优化,Redis Cluster每次PING/PONG包中,只包含全集群的部分节点信息,节点随机选取,以此控制网络流量。由于交互频繁,短时间的几次交互后,集群状态以这样的Gossip协议方式被扩散到了集群中的所有节点。

3.一致性的达成

集群结构不发生变化的时候,集群中的各个节点通过gossip协议可以在几轮交互之后得知全集群的结构信息,且达到一致状态。

然而故障转移、分片迁移等情况的发生会导致集群结构变更,由于无统一的配置服务器,变更信息只能靠各个节点自行协调,优先得知变更信息的节点利用epoch变量将自己的最新信息扩散到整个集群,达到最终一致。

配置信息的clusterNode的epoch属性描述的粒度是单个节点,即某个节点的数据分片、主备信息版本

配置信息的clusterState的currentEpoch属性的粒度是整个集群,它的存在用来辅助epoch自增地生成。由于currentEpoch信息也是维护在各个节点自身的,RedisCluster在结构变化时,通过一定时间窗口控制和更新规则保证每个节点看到的currentEpoch都是最新的。

集群信息的更新遵循以下规则:

  • 当某个节点率先知道了变更时,这个节点将currentEpoch自增使之成为集群中的最大值,再用自增后的currentEpoch作为新的epoch版本。
  • 当某个节点收到了比自己大的currentEpoch时,更新自己的currentEpoch值使之保持最新
  • 当收到的Redis Cluster Bus消息中某个节点信息的epoch值大于接收者自己内部的配置信息存储的值时,说明自己的信息太旧了,将自己的映射信息跟新为消息的内容
  • 当收到的Redis Cluster Bus消息中某个节点信息未包含在接收节点内部配置信息时,意味着接收者尚未意识到所指节点的存在,此时接收者直接将消息的信息添加到自己的内部配置信息中。

上述规则保证了更新始终是单向的,始终朝着epoch值更大的信息收敛,同时epoch也随着每次配置变更时currentEpoch的自增而单向增加,确定了各节点信息更新的方向稳定。

8.4.3 sharding

不同节点组服务于相互无交集的数据子集(分片,sharding),本节介绍数据分片方式。

因为Redis Cluster不存在单独的proxy和配置服务器,所以如何让客户端正确地路由请求也在本节得出答案。

1.数据分片(slot)

Redis Cluster将所有数据划分为16384个分片(slot),每个分片负责一部分。每一条数据(key-value)根据key值通过数据分布算法映射到16384个slot中的一个。

数据分布算法为:slotId=crc16(key)%16384

客户端根据slotId决定将请求路由到那个Redis节点。cluster不支持跨节点的单命令,例如SINTERSTORE,如果涉及的两个key对应的slot分布在不同的node上,则操作失败。

不同业务实体存在一定的关系,常常需要同一个命令中操作两条记录,两条记录有极大可能分散到不用的节点上,阻碍单条命令以原子性的方式操作两条关联性强的记录。为此,Redis引入HashTag概念,使得数据分布算法可以根据key的某一部分进行计算。,让相关的两条记录落到同一个数据分片。例如:

某条商品交易记录的key为:product_trade_{prod123}。

这个商品的详情记录的key为:product_detail_{prod123}。

Redis会根据{}之间的子字符串作为数据分布算法的输入。

2.客户端的路由

Redis Cluster的客户端相比单机Redis需要具备路由语义的识别能力,且具备一定的路由缓存能力。

当一个client访问的key不在对应Redis节点的slot中,Redis返回给client一个moved命令,告知其正确的路由信息,如图:

 从client收到moved响应,到再次向moved响应中指向的节点(假设为节点B)发送请求期间,Redis Cluster的数据分布可能有发生了变化,使得B仍然不是正确的节点,此时,B会继续响应一个moved。client根据moved响应更新其内部的路由缓存信息,以便下一次请求直接能够路由到正确的节点,降低交互次数。

当cluster处在数据重新分布(目前由人工触发)过程中时,可以通过ask命令控制客户端的路由,具体过程如图:

 上图中slot1打算迁移到新节点上,迁移过程中,如果客户端访问已经完成迁移的key,节点将响应ask告知客户端向目标节点重试。

ask命令和moved命令不同的语义在于,ask只是重新定向到新节点,后续的相同slot操作仍路由到旧节点,moved会更新client数据分布,后续路由到新节点。

迁移的过程可能持续一定时间,这段时间某个slot数据同时在新旧两个节点上各分部一部分,由于move操作会使得客户端路由缓存变更,如果新旧两个节点对于迁移中的slot上所有不在自己节点的key都响应moved信息,客户端的路由缓存信息可能会频繁变动。故引入ask类型消息,将重定向和路由缓存更新分离。

3.分片的迁移

稳定的Reids Cluster,每个slot对应的节点时确定的。但在某些情况下节点和分片的对应关系需要发生变更:

  • 新节点作为master加入
  • 某个节点分组需要下线
  • 负载不均需要调整slot分布

此时,需要进行分片的迁移。

分片迁移的触发和过程控制由外部系统完成,Redis Cluster只提供迁移过程中需要的原语供外部系统调用。这些原语主要包含两种:

  • 节点迁移状态设置:迁移前标记源/目标节点
  • key迁移的原子化命令:迁移的具体步骤

如下图,slot1从节点A迁移到节点B

1)向节点B发送状态变更命令,将B的对应slot状态置为IMPORTING

2)向节点A发送状态变更命令,将A的对应slot状态置为MIGRATING

3)针对A的slot上所有的key,分别向A发送MIGRATE命令,告知A将对应key的数据迁移到B

当A的状态置为MIGRATING后,表示对应的slot正在从A迁出,为保证该slot数据的一致性,A此时对slot内部数据提供读写服务的行为和通常状态下有所区别,对于某个迁移中的slot:

  • 如果客户端访问的key尚未迁出,则正常处理该key
  • 如果key已经迁移出或不存在该key,则回复客户端ASK信息让其跳转到B执行

当节点B的状态置为IMPORTING后,表示对应的slot正在向B迁入中,即使B仍能对外提供该slot的读写服务,但行为和通常状态也有区别:

  • 当来自客户端的正常范围不是从ASK跳转来的时,说明客户端不知道迁移正在进行,很有可能操作了一个目前尚未迁移完成的正处在A上的key,如果此时这个key在A上已被修改了,那么B和A的修改值将在未来发生冲突。
  • 所以对于该slot上的所有非ASK跳转而来的操作,B不会进行处理,而是通过MOVED命令让客户端跳转至A执行。

这样的状态控制保证了同一个key在迁移前总是在源节点执行,迁移后总是在目标节点执行,杜绝了两边同时写导致值冲突的可能性。且迁移过程中新增的key总是在目标节点执行,源节点不会再新增key,使得迁移过程时间有界,可以在确定的某个时刻结束。

剩下的问题是保证某个key在迁移过程的一致性。单个key的迁移过程被抽象为了原子化的MIGRATE命令,这个命令完成数据传输到B、等待B接收完成、在A上删除该key的动作。从前面章节得知,Redis单机对于命令的处理时单线程的,同一个key在执行MIGRATE的过程中不会处理对该key的其他操作,从而保证了迁移的原子性。

当slot的所有key从A迁移到B之后,客户端通过CLUSTER SETSLOT命令设置B的分片信息,使之包含迁移的slot。设置的过程中会自增一个新的epoch,它大于当前集群中的所有epoch值,根据后者随着前述小结的配置一致性策略,这个新的配置信息会传播到集群中的其他每个节点,完成分片节点映射关系的更新。

8.4.4 failover

同sentinel一样,Redis Cluster也具备一套完整的节点故障发现、故障状态一致性保证、主备切换机制。

1.failover的状态变迁

1)故障发现:当某个master宕机时,宕机事件如何被集群其他节点感知

2)故障确认:多个节点就某个master是否宕机如何达成一致

3)slave选举:集群确认了某个master宕机后,如何将他的slave提升为新master;多个slave选择哪个?

4)集群结构变更:选举成功的slave升级为新master后如何让集群其他节点感知并更新他们的集群结构信息。

2.故障发现

Redis Cluster两两周期性进行PING/PONG交互,当超过一定时间(NODE_TIMEOUT)未收到PONG,认为接收者故障,将其置为PFAIL状态(Possible Fail),后续通过Gossip发出的PING/PONG消息中,此节点的PFAIL状态将会传播到集群的其他节点。

无法收到PONG响应,也可能是TCP断开连接。这可能导致误报,虽然误报会因为其他节点正常连接被忽略,但是这样的误报可以避免。Redis Cluster通过预重试机制排除此类误报:当NODE_TIMEOUT/2过去了还未收到PONG消息,则重建连接重发PING消息,如果对端正常,PONG会在很短时间抵达。

3.故障确认

A收到B已经FAIL的消息会将B状态改为FAIL,并广播到其他节点

A收到足够多的PFAIL也会将B的状态从PFAIL改为FAIL,并将FAIL消息广播到其他节点。

4.slave选举

上例中,如果B是A的master,且B已经被公认为FAIL,那么A发起竞选,期望代替B成为master。

如果B有多个slaveA/E/F都认识到B已经FAIL,那么可能会同时发起竞选,当B的竞选的slave数量>=3时,可能由于票数平均而无法选出胜者,此时要再次竞选,多轮竞选延误新master选出,导致B上的slot不可用时间延长。因此slave间会在选举前协商优先级,优先级高的slave更有可能更早发起选举,提升一轮完成选举的可能性,优先级低的slave选举时间靠后,避免和高优先级slave竞争。优先级最重要的决定因素是slave最后一次同步master信息的时间,越新的表示这个slave时间越新,竞选优先级越高。

slave通过向其他msater节点发送FAILOVER_AUTH_REQUEST消息发起竞选,master收到之后回复FAILOVER_AUTH_ACK消息告知自己是否同意slave成为新的master。slave发送FAILOVER_AUTH_REQUEST前会将currentEpoch自增并将最新的epoch带入到FAILOVER_AUTH_REQUEST消息中,master收到FAILOVER_AUTH_REQUEST消息后,如果发现对于本轮(本epoch)自己尚未投过票,则回复同意,否则拒绝。

5.结构变更通知

当slave得到超过半数master同意回复时,成为新的master,此时它会以最新的epoch通过PONG消息广播自己成为master的信息,集群中其他节点尽快更新拓扑信息。

当B回复可用后,它首先仍认为自己是master,但逐渐通过Gossip协议得知A已经替代自己的事实后降级为A的slave。

8.4.5 可用性和性能

1.Redis Cluster的读写分离

对于读写分离需求的场景,应用于对于某些读的请求允许舍弃一定的数据一致性,以换取更高的读吞吐量。此时希望将读的请求交由slave处理以分担master的压力。

默认情况下,数据分片映射关系中,某个slot对应的节点一定是一个master节点,客户端通过MOVED消息得知的集群拓扑结构只会将请求路由到各个master中,即便客户端将读请求直接发送到slave上,后者也会回复MOVED到master响应。

为此,Redis CLuster引入READONLY命令。客户端向slave发送该命令后,slave对于读操作,不再MOVED回master而是直接处理,这称为slave的READONLY模式。

通过READWRITE命令,可将slave的readonly模式重置。

2.master单点保护

假设集群的结构如图:

A、B两个master分别有1、2个自己的slave,假设A1发生宕机,集群变为下图所示:

此时A成为了单点,一旦A再次宕机,将造成不可用。此时Redis Cluster将会把B的某个slave(假设是B1)进行副本迁移,让其变成A的slave,图下图:

 

这使得集群中的每个master至少有一个slave,即高可用状态。这样一来,集群只需要保持2*master+1个节点,就可以在任一节点宕机后仍自动维持高可用状态,称为单点保护。

如果不具备此功能,则需要维持3*master个节点,而其中master-1个slave节点在可用性视角来看是浪费掉了。

 

posted @ 2020-05-05 23:41  vvf  阅读(214)  评论(0编辑  收藏  举报