深入理解Redis-Cluster

452 阅读14分钟

Redis-Cluster

简介

什么是Redis-Cluster

  • Redis Cluster是Redis的分布式解决方案,有效解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构达到负载均衡的目的。

Redis Cluster 优势

  • 去中心化,集群最大可增加1000个节点,性能随节点增加而线性扩展。
  • 管理方便,后续可自行增加或摘除节点,移动分槽等等。
  • 解决单机版redis 高可用问题,并发性能瓶颈问题,容量瓶颈问题
  • 官方产品

Redis Cluster设计目标

  • 高性能可线性扩展至最多1000节点

集群中没有代理,(集群节点间)使用异步复制,没有归并操作(merge operations on values)

  • 可接受的写入安全

系统尝试(采用best-effort方式)保留所有连接到master节点的client发起的写操作。通常会有一个小的时间窗,时间窗内的已确认写操作可能丢失(即,在发生failover之前的小段时间窗内的写操作可能在failover中丢失)。而在(网络)分区故障下,对少数派master的写入,发生写丢失的时间窗会很大。

  • 可用性

Redis Cluster在以下场景下集群总是可用:大部分master节点可用,并且对少部分不可用的master,每一个master至少有一个当前可用的slave。更进一步,通过使用 replicas migration 技术,当前没有slave的master会从当前拥有多个slave的master接受到一个新slave来确保可用性。

Redis Cluster核心原理

数据如何分布?

  • Redis Cluster采用哈希+虚拟槽分区(hash slot)实现数据分布式存储

hash slot

与一致性hash算法类似,但继承并增强一致性哈希的容错性,扩展性,以及平衡性。

Redis-cluster中有16384(即2的14次方)个哈希槽,每个key通过CRC16校验后对16383取模来决定放置哪个槽。Cluster中的每个节点负责一部分hash槽(hash slot)。

为什么Redis Cluster的Hash Slot 是16384?

我们知道一致性hash算法是2的16次方,为什么hash slot是2的14次方呢?作者原始回答

在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 16K),也就是说使用2k的空间创建了16k的槽数。 虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) * 1024(1k) =65K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。

例子

假如Redis-Cluster有3个节点,可以配置如下分配:

  • 节点A 负责0-5000虚拟槽
  • 节点B 负责5000-11000虚拟槽
  • 节点C 负责11000-16384虚拟槽

数据分布知识延伸

除了Redis-Cluster采用的HashSlot方式数据分布存储,还有以下方式实现数据分布

  • 节点取余

    • 原理:根据key的hash值和节点数取模的方式计算出节点ID,然后向对应的节点提交数据
    • 缺点:所有的数据几乎要全部重新hash,几乎需要重新迁移数据(而且相当麻烦),无法做到在线数据热迁移
  • 一致性Hash

    • 原理:对于任何的哈希函数,都有其取值范围。我们可以用环形结构来标识范围。通过哈希函数,每个节点都会被分配到环上的一个位置,每个键值也会被映射到环上的一个位置,然后顺时针找到相邻的节点
    • 缺点:存在数据分布不均匀问题,增减节点时需要找出受影响的区间的数据进行迁移
    • 更多一致性hash资料点击这里

如何做到高可用?

高可用及故障转移

  • 运行原理

Redis Cluster 是去中心化的,一个节点认为某个节点失联了并不代表所有的节点都 认为它失联了。所以集群还得经过一次协商的过程,只有当大多数节点都认定了某个节点失 联了,集群才认为该节点需要进行主从切换来容错。

Redis 集群节点采用 Gossip 协议来广播自己的状态以及自己对整个集群认知的改变。比 如一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可 以收到这点失联信息。如果一个节点收到了某个节点失联的数量 (PFail Count)已经达到了集群的大多数,就可以标记该节点为确定下线状态(Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换

  • 故障检测

集群中的每个节点都会定期地向集群中其它节点发送ping消息,以此来检测对方是否在线。如果在规定时间内没有收到pong回复。则认为目标节点标记为疑似下线(PFAIL)。 如果集群中的半数异常(大于等于N/2 +1)的主节点认为某个节点A疑似下线(PFAIL),那么这个节点A将被标记为已下线(FAIL)。将节点标记A为已下线的节点会向集群一条关于节点A显现的消息,所有收到这条F消息的节点都会立即将主节点A标记为已下线。

  • 故障转移

当一个从节点发现自己正在复制的主节点进入了已下线时,从节点将开始对已下线的主节点进行故障转移操作,以下是故障转移的执行步骤:

  1. 下线的主节点的所有从节点里面,会进行选举,选举出一个新的主节点。
  2. 被选中的从节点会执行 slave no one命令,成为新的主节点。
  3. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽指派给自己。
  4. 新的主节点向集群广播一条pong消息,这条pong消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点处理的槽。
  5. 新的主节点开始接受和自己负责处理的槽有关的命令请求,故障转移操作完成。
  • 主节点选举

流程如下:

  1. 当从节点发现自己复制的主节点进入已下线时,从节点(这里发出请求的从节点可能会有多个)会向集群广播一条cluster_type_failover_auth_request的消息,要求有投票权(负责处理槽)的主节点向这个节点进行投票。
  2. 收到cluster_type_failover_auth_request消息的主节点,根据自身条件(发起投票节点的current epoch不低于投票节点的current epoch)判断是否赞成该从节点成为新的主节点,若赞成则返回一条cluster_type_failover_auth_ack消息。
  3. 从节点接收到cluster_type_failover_auth_ack消息,会将选票数加1。
  4. 如果某个从节点的选票大于等于集群中主节点的一半时(大于等于N/2 + 1),这个节点就会成为新的主节点。
  5. 如果在一个配置周期内,没有一个从节点获得足够多的选票,那么集群中会进入新的配置周期,并在此进行选举,知道选出新的主节点为止。

节点之间如何通信

Redis Cluster各个节点之间使用gossip协议进行交换数据、通信。

gossip协议,延伸阅读 点击这里

Redis Cluster是如何进行扩容和缩容的呢?

扩容

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

  1. 首先将新节点加入到集群中,可以通过在集群中任何一个客户端执行cluster meet 新节点ip:端口,或者通过redis-trib add node添加,新添加的节点默认在集群中都是主节点。 迁移数据
  2. 迁移数据的大致流程是,首先需要确定哪些槽需要被迁移到目标节点,然后获取槽中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

缩容

缩容的大致过程与扩容一致,需要判断下线的节点是否是主节点,以及主节点上是否有槽,若主节点上有槽,需要将槽迁移到集群中其他主节点,槽迁移完成之后,需要向其他节点广播该节点准备下线(cluster forget nodeId)。最后需要将该下线主节点的从节点指向其他主节点,当然最好是先将从节点下线。

Redis Cluster运行细节

跳转

当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自 己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端 去连这个节点去获取数据。

客户端收到 MOVED 指令后,要立即纠正本地的槽位映射表。后续所有 key 将使用新 的槽位映射表。

迁移

Redis Cluster 提供了工具 redis-trib 可以让运维人员手动调整槽位的分配情况,它使用 Ruby 语言进行开发,通过组合各种原生的 Redis Cluster 指令来实现。这点 Codis 做的更加人性化,它不但提供了 UI 界面可以让我们方便的迁移,还提供了自动化平衡槽位工具,无需人工干预就可以均衡集群负载。

迁移过程

Redis 迁移的单位是槽,Redis 一个槽一个槽进行迁移,当一个槽正在迁移时,这个槽就 处于中间过渡状态。这个槽在原节点的状态为 migrating,在目标节点的状态为 importing,表示数据正在从源流向目标。

迁移工具 redis-trib 首先会在源和目标节点设置好中间过渡状态,然后一次性获取源节 点槽位的所有 key 列表(keysinslot 指令,可以部分获取),再挨个 key 进行迁移。每个 key的迁移过程是以原节点作为目标节点的「客户端」,原节点对当前的 key 执行 dump 指令得到序列化内容,然后通过「客户端」向目标节点发送指令 restore 携带序列化的内容作为参数,目标节点再进行反序列化就可以将内容恢复到目标节点的内存中,然后返回「客户端」OK,原节点「客户端」收到后再把当前节点的 key 删除掉就完成了单个 key 迁移的整个过程。

从源节点 获取内容 => 存到目标节点 => 从源节点删除内容。

注意这里的迁移过程是同步的,在目标节点执行 restore 指令到原节点删除 key 之间,原 节点的主线程会处于阻塞状态,直到 key 被成功删除。如果迁移过程中突然出现网络故障,整个 slot 的迁移只进行了一半。这时两个节点依旧处于中间过渡状态。待下次迁移工具重新连上时,会提示用户继续进行迁移。在迁移过程中,如果每个 key 的内容都很小,migrate 指令执行会很快,它就并不会影响客户端的正常访问。如果 key 的内容很大,因为 migrate 指令是阻塞指令会同时导致原节点和目标节点卡顿,影响集群的稳定型。所以在集群环境下业务逻辑要尽可能避免大 key 的产生。

在迁移过程中,客户端访问的流程会有很大的变化。

首先新旧两个节点对应的槽位都存在部分 key 数据。客户端先尝试访问旧节点,如果对 应的数据还在旧节点里面,那么旧节点正常处理。如果对应的数据不在旧节点里面,那么有 两种可能,要么该数据在新节点里,要么根本就不存在。旧节点不知道是哪种情况,所以它 会向客户端返回一个-ASK targetNodeAddr的重定向指令。客户端收到这个重定向指令后,先去目标节点执行一个不带任何参数的 asking 指令,然后在目标节点再重新执行原先的操作指 令。

为什么需要执行一个不带参数的 asking 指令呢?

因为在迁移没有完成之前,按理说这个槽位还是不归新节点管理的,如果这个时候向目标节点发送该槽位的指令,节点是不认的,它会向客户端返回一个-MOVED 重定向指令告诉它去源节点去执行。如此就会形成 重定向循环。asking 指令的目标就是打开目标节点的选项,告诉它下一条指令不能不理,而要当成自己的槽位来处理。

从以上过程可以看出,迁移是会影响服务效率的,同样的指令在正常情况下一个 ttl 就 能完成,而在迁移中得 3 个 ttl 才能搞定。

集群变更感知

当服务器节点变更时,客户端应该即时得到通知以实时刷新自己的节点关系表。那客户 端是如何得到通知的呢?这里要分 2 种情况:

  1. 目标节点挂掉了,客户端会抛出一个 ConnectionError,紧接着会随机挑一个节点来重试,这时被重试的节点会通过 moved error 告知目标槽位被分配到的新的节点地址。

  2. 运维手动修改了集群信息,将 master 切换到其它节点,并将旧的 master 移除集群。这时打在旧节点上的指令会收到一个 ClusterDown 的错误,告知当前节点所在集群不可用 (当前节点已经被孤立了,它不再属于之前的集群)。这时客户端就会关闭所有的连接,清空槽位映射关系表,然后向上层抛错。待下一条指令过来时,就会重新尝试初始化节点信息。

Redis Cluster的限制

  1. 只支持一个数据库
  2. key批量操作支持有限:例如mget,mset必须在一个slot
  3. key事务和Lua支持有限:操作的key必须在一个节点
  4. key是数据分区的最小粒度:不支持bigkey分区

参考资料

  • Redis深度历险
  • Redis设计与实现