【Cache System】Redis Cluster

Posted by 西维蜀黍 on 2020-07-10, Last Modified on 2022-12-10

Redis Cluster

Redis Cluster 是Redis3.0之后,官方提供的Redis集群解决方案,由 Redis 官方团队来实现。

在3.0之前,为了解决容量高可用用方面的需求,基本上只能通过客户端分片 + Redis sentinel或者添加 proxy(twemproxy、codis)方案解决。

Redis Cluster最大的特性,就是对Redis集群提供了水平扩展的能力(horizential scalibility),即当整个 Redis 集群出现存储容量或者性能 bottleneck 时,使用 Redis Cluster 可以通过增加新的 master Redis node从而快速解决bottleneck。

除此之外,Redis Cluster提供了强大的高可用机制(即 failure failover)。即当集群中任何一个master Redis node无法正常工作时(比如因为底层依赖的硬件故障、网络问题),它对应的 slave Redis node就会自动代替它(当然,这里有一个前提,是我们设置了slave Redis node)。

与Codis 和 Twemproxy 不同的是:Redis Cluster并非使用Porxy模式来连接集群节点,而是使用无中心节点的模式来组建集群(即没有coordinator)。

Redis Cluster实现在多个节点之间进行数据共享,即使部分节点失效或者无法进行通讯时,Cluster仍然可以继续处理请求。若每个主节点都有一个从节点支持,在主节点下线或者无法与集群的大多数节点进行通讯的情况下, 从节点提升为主节点,并提供服务,保证Cluster正常运行。

Redis Cluster 数据分片(Data Sharding)

Redis Cluster的数据分片是通过哈希槽(hash slot)实现的,而不是使用一致性哈希(consistent hashing)。

There are 16384 hash slots in Redis Cluster, and to compute what is the hash slot of a given key, we simply take the CRC16 of the key modulo 16384.

Every node in a Redis Cluster is responsible for a subset of the hash slots, so for example you may have a cluster with 3 nodes, where:

  • Node A contains hash slots from 0 to 5500.
  • Node B contains hash slots from 5501 to 11000.
  • Node C contains hash slots from 11001 to 16383.

每个 key 存放在这 16384(0~16383) 个哈希槽中的其中一个,每个Redis node 存储一部分哈希槽。

This allows to add and remove nodes in the cluster easily. For example if I want to add a new node D, I need to move some hash slot from nodes A, B, C to D. Similarly if I want to remove node A from the cluster I can just move the hash slots served by A to B and C. When the node A will be empty I can remove it from the cluster completely.

Because moving hash slots from a node to another does not require to stop operations, adding and removing nodes, or changing the percentage of hash slots hold by nodes, does not require any downtime.

Redis Cluster supports multiple key operations as long as all the keys involved into a single command execution (or whole transaction, or Lua script execution) all belong to the same hash slot. The user can force multiple keys to be part of the same hash slot by using a concept called hash tags.

概念特点

  • 去中心、去中间件,各节点平等,保存各自数据和集群状态,节点间活跃互连。
  • 传统用一致性哈希分配数据,集群用哈希槽(hash slot)分配。 算法为CRC16。
  • 默认分配16384个slot, 用CRC16算法取模 CRC16(key)%16384 计算所属slot。
  • 每个主节点存储一部分哈希槽
  • 最少3个主节点

优点和不足

优点

  • 官方解决方案
  • 可以在线水平扩展(Twemproxy的一大弊端就是不支持在线扩容节点)
  • 客户端直连,系统瓶颈更少
  • 无中心架构
  • 支持数据分片

Redis Cluster集群现实存在的问题 尽管属于无中心化架构一类的分布式系统,但不同产品的细节实现和代码质量还是有不少差异的,就比如Redis Cluster有些地方的设计看起来就有一些“奇葩”和简陋:

  • 不能自动发现:无Auto Discovery功能。集群建立时以及运行中新增结点时,都要通过手动执行MEET命令或redis-trib.rb脚本添加到集群中
  • 不能自动Resharding:不仅不自动,连Resharding算法都没有,要自己计算从哪些结点上迁移多少Slot,然后还是得通过redis-trib.rb操作
  • 严重依赖外部redis-trib:如上所述,像集群健康状况检查、结点加入、Resharding等等功能全都抽离到一个Ruby脚本中了。还不清楚上面提到的缺失功能未来是要继续加到这个脚本里还是会集成到集群结点中?redis-trib也许要变成Codis中Dashboard的角色
  • 无监控管理UI:即便未来加了UI,像迁移进度这种信息在无中心化设计中很难得到
  • 只保证最终一致性:写Master成功后立即返回,如需强一致性,自行通过WAIT命令实现。但对于“脑裂”问题,目前Redis没提供网络恢复后的Merge功能,“脑裂”期间的更新可能丢失

注意,如果设置Redis Cluster的数据冗余是1的话,至少要3个Master和3个Slave。

Redis Cluster容错机制 - failover

failover是redis cluster的容错机制,是redis cluster最核心功能之一;它允许在某些节点失效情况下,集群还能正常提供服务。

redis cluster采用主从架构,任何时候只有主节点提供服务,从节点进行热备份,故其容错机制是主从切换机制,即主节点失效后,选取一个从节点作为新的主节点。在实现上也复用了旧版本的主从同步机制。

从纵向看,redis cluster是一层架构,节点分为主节点和从节点。从节点挂掉或失效,不需要进行failover,redis cluster能正常提供服务;主节点挂掉或失效需要进行failover。另外,redis cluster还支持manual failover,即人工进行failover,将从节点变为主节点,即使主节点还活着。

下面将介绍这两种类型的failover。

主节点失效产生的failover

1 (主)节点失效检测

一般地,集群中的节点会向其他节点发送PING数据包,同时也总是应答(accept)来自集群连接端口的连接请求,并对接收到的PING数据包进行回复。当一个节点向另一个节点发PING命令,但是目标节点未能在给定的时限(node timeout)内回复时,那么发送命令的节点会将目标节点标记为PFAIL(possible failure)。

由于节点间的交互总是伴随着信息传播的功能,此时每次当节点对其他节点发送 PING 命令的时候,就会告知目标节点此时集群中已经被标记为PFAIL或者FAIL标记的节点。相应的,当节点接收到其他节点发来的信息时, 它会记下那些被其他节点标记为失效的节点。 这称为失效报告(failure report)。

如果节点已经将某个节点标记为PFAIL,并且根据节点所收到的失效报告显式,集群中的大部分其他主节点(n/2+1)也认为那个节点进入了失效状态,那么节点会将那个PFAIL节点的状态标记为FAIL。

一旦某个节点被标记为FAIL,关于这个节点已失效的信息就会被广播到整个集群,所有接收到这条信息的节点都会将失效节点标记为FAIL。

2 选举主节点

一旦某个主节点进入 FAIL 状态, 集群变为FAIL状态,同时会触发failover。failover的目的是从从节点中选举出新的主节点,使得集群恢复正常继续提供服务。

整个主节点选举的过程可分为申请、授权、升级、同步四个阶段:

1 申请

新的主节点由原已失效的主节点属下的所有从节点中自行选举产生,从节点的选举遵循以下条件:

  1. 这个节点是已下线主节点的从节点;
  2. 已下线主节点负责处理的哈希槽数量非空;
  3. c、主从节点之间的复制连接的断线时长有限,不超过 ( (node-timeout * slave-validity-factor) + repl-ping-slave-period )。

如果一个从节点满足了以上的所有条件,那么这个从节点将向集群中的其他主节点发送授权请求,询问它们是否允许自己升级为新的主节点。

从节点发送授权请求的时机会根据各从节点与主节点的数据偏差来进行排序,让偏差小的从节点优先发起授权请求。

2 授权

其他主节点会遵信以下三点标准来进行判断:

  1. 发送授权请求的是从节点,而且它所属的主节点处于FAIL状态 ;
  2. 从节点的currentEpoch〉自身的currentEpoch,从节点的configEpoch>=自身保存的该从节点的configEpoch;
  3. 这个从节点处于正常的运行状态,没有被标记为FAIL或PFAIL状态;

如果发送授权请求的从节点满足以上标准,那么主节点将同意从节点的升级要求,向从节点返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK授权。

3 升级

一旦某个从节点在给定的时限内得到大部分主节点(n/2+1)的授权,它就会接管所有由已下线主节点负责处理的哈希槽,并主动向其他节点发送一个PONG数据包,包含以下内容:

  1. 告知其他节点自己现在是主节点了
  2. 告知其他节点自己是一个ROMOTED SLAVE,即已升级的从节点;
  3. c、告知其他节点都根据自己新的节点属性信息对配置进行相应的更新

4 同步

其他节点在接收到ROMOTED SLAVE的告知后,会根据新的主节点对配置进行相应的更新。特别地,其他从节点会将新的主节点设为自己的主节点,从而与新的主节点进行数据同步。 至此,failover结束,集群恢复正常状态。

此时,如果原主节点恢复正常,但由于其的configEpoch小于其他节点保存的configEpoch(failover了产生较大的configEpoch),故其配置会被更新为最新配置,并将自己设新主节点的从节点。

另外,在failover过程中,如果原主节点恢复正常,failover中止,不会产生新的主节点。

Manual Failover

Manual Failover是一种运维功能,允许手动设置从节点为新的主节点,即使主节点还活着。

Manual Failover与上面介绍的Failover流程大都相同,除了下面两点不同:

  • 触发机制不同,Manual Failover是通过客户端发送cluster failover触发,而且发送对象只能是从节点;
  • 申请条件不同,Manual Failover不需要主节点失效,failover有效时长固定为5秒,而且只有收到命令的从节点才会发起申请。

另外,Manual Failover分force和非force,区别在于:非force需要等从节点完全同步完主节点的数据后才进行failover,保证不丢失数据,在这过程中,原主节点停止写操作;而force不进行进行数据完整同步,直接进行failover。

集群状态检测

集群有OK和FAIL两种状态,可以通过CLUSTER INFO命令查看。当集群发生配置变化时, 集群中的每个节点都会对它所知道的节点进行扫描,只要集群中至少有一个哈希槽不可用(即负责该哈希槽的主节点失效),集群就会进入FAIL状态,停止处理任何命令。 另外,当大部分主节点都进入PFAIL状态时,集群也会进入FAIL状态。这是因为要将一个节点从PFAIL状态改变为FAIL状态,必须要有大部分主节点(n/2+1)认可,当集群中的大部分主节点都进入PFAIL时,单凭少数节点是没有办法将一个节点标记为FAIL状态的。 然而集群中的大部分主节点(n/2+1)进入了下线状态,让集群变为FAIL,是为了防止少数存着主节点继续处理用户请求,这解决了出现网络分区时,一个可能被两个主节点负责的哈希槽,同时被用户进行读写操作(通过禁掉其中少数派读写操作,证保只有一个读写操作),造成数据丢失数据问题。 说明:上面n/2+1的n是指集群里有负责哈希槽的主节点个数。

扩容&缩容

扩容

当集群出现容量限制或者其他一些原因需要扩容时,redis cluster提供了比较优雅的集群扩容方案。

首先将新节点加入到集群中,可以通过在集群中任何一个客户端执行cluster meet 新节点ip:端口,或者通过redis-trib add node添加,新添加的节点默认在集群中都是主节点。

迁移数据 迁移数据的大致流程是,首先需要确定哪些槽需要被迁移到目标节点,然后获取槽中key,将槽中的key全部迁移到目标节点,然后向集群所有主节点广播槽(数据)全部迁移到了目标节点。直接通过redis-trib工具做数据迁移很方便。 现在假设将节点A的槽10迁移到B节点,过程如下:

B:cluster setslot 10 importing A.nodeId
A:cluster setslot 10 migrating B.nodeId

循环获取槽中key,将key迁移到B节点

A:cluster getkeysinslot 10 100
A:migrate B.ip B.port "" 0 5000 keys key1[ key2....]

向集群广播槽已经迁移到B节点

cluster setslot 10 node B.nodeId

Reference