Redis Cluster 是Redis的分布式解决方案,在3.0版本正式推出,有效的解决了Redis分布式方面的需求。
Redis Cluster由多个分片组成, 每个分片包含一个master和多个slave来实现高可用, 每个master和slave都是单线程。Redis每个请求和数据都对应一个key, Redis Cluster将请求的所有key通过crc16哈希到16384(2的14次方)个slot中, 并将这16384个slot分配到集群中的所有分片上. 这样每个分片就只负责一部分请求和数据。Redis Cluster的扩缩容就是在分片间迁移slot和增加剔除分片节点.
在此之间,Redis 分布式方案有两种:
1、客户端分区
优点是分区逻辑可控,缺点是需要自己处理数据路由、高可用、故障转移等问题,比如在redis2.8之前通常的做法是获取某个key的hashcode,然后取余分布到不同节点,不过这种做法无法很好的支持动态伸缩性需求,一旦节点的增或者删操作,都会导致key无法在redis中命中。
2、代理方案
优点是简化客户端分布式逻辑和升级维护便利,缺点是加重架构部署复杂度和性能损耗,比如twemproxy、Codis
Redis 集群相对单机,在功能上存在一些限制,需要我们尽早了解,以规避在使用时触发这些限制:
1)key 批量操作支持有限。 如 mset、mget,目前只支持具有相同 slot 值的 key 执行批量 操作。对于映射为不同 slot 值的 key 由于执行 mget、 mget 等操作可能出现在多个节点上因此不被支持。
2)key 事务操作支持有限。只支持多 key 在同一节点上的事务操作,当多个key 分布在不同 的节点上时无法使用事务功能。
3)key 作为数据分区的最小粒度,因此不能将一个大的键值对象如 hash、list 等映射到不同 的节点。
4)不支持多数据库空间。单机下的Redis 可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0。
5)复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
参见Redis集群规范:https://redis.io/topics/cluster-spec
Redis Cluster是Redis的分布式实现,具有以下目标:
Redis Cluster实现Redis的非分布式版本中可用的所有单个键命令。 执行复杂的多键操作(如Set类型联合或交叉)的命令也可以实现,只要这些键都属于同一个节点。
Redis Cluster实现了一个称为哈希标记(hash tags)的概念,可用于强制某些密钥存储在同一节点中。 但是,在手动重新分片期间,多键操作可能会在一段时间内不可用,而单键操作始终可用。
Redis Cluster 不支持多个数据库,只有数据库0,不允许使用SELECT命令。
在Redis集群中,节点负责保存数据并获取群集的状态,包括将键映射到正确的节点。 集群节点还能够自动发现其他节点,检测非工作节点,并在需要时切换从节点为主节点,以便在发生故障时继续运行。
集群节点都使用TCP总线和二进制协议连接,称为Redis集群总线(Redis Cluster Bus)。 每个节点都使用集群总线连接到集群中的每个其他节点。 节点使用 gossip 协议传播有关群集的信息,以便发现新节点,发送ping数据包以确保所有其他节点正常工作。 集群总线还用于在群集中传播发布/订阅消息,并在用户请求时协调手动故障转移。
由于集群节点无法代理请求,因此可以使用重定向错误 -MOVED 和 -ASK 将客户端重定向到其他节点。 理论上,客户端可以自由地向集群中的所有节点发送请求,并在需要时重定向,因此客户端不需要保持集群的状态。 但是,能够在键和节点之间缓存映射的客户端可以以合理的方式提高性能。
在Master-slaves之间使用异步replication机制,在failover之后,新的Master将会最终替代其他的replicas(即slave)。在出现网络分区时(network partition),总会有一个窗口期(node timeout)可能会导致数据丢失。
write提交到master,master执行完毕后向Client反馈“OK”,不过此时可能数据还没有传播给slaves(异步replication);如果此时master不可达的时间超过阀值(参见配置参数),那么将触发slave被选举为新的Master,这意味着那些没有replication到slaves的writes将永远丢失。
Redis Cluster在分区的少数端不可用。在分区的大多数端必须持有大多数Masters,同时每个不可达的Master至少有一个slave,当NODE_TIMEOUT时,触发failover,此后集群仍是可用的。
这意味着Redis Cluster旨在经受群集中几个节点的故障,但对于需要在大型网络分裂时需要可用性的应用程序而言,它不是合适的解决方案。
在由N个主节点组成的集群示例中,每个节点都有一个从节点,只要单个节点被分区,集群的大多数端将保持可用,并且将保持可用的概率为1-(1 /(N * 2-1))当两个节点被分区时(在第一个节点发生故障之后,我们总共剩下N * 2-1个节点,而没有副本的唯一主节点失败的概率是1 /(N *2-1))。
由于Redis Cluster提供了复制副本迁移(replicas migration)机制,在实际应用方面,可以有效的提高集群的可用性,当每次failover发生后,集群都会重新配置、平衡slaves的分布,以更好的抵御下一次故障的发生。
Redis Cluster并没有提供Proxy层,而是告知客户端将key的请求转发给合适的nodes。Client保存集群中nodes与keys的映射关系(slots),并保持此数据的更新,所以通常Client总能够将请求直接发送到正确的nodes上。
因为采用异步replication,所以master不会等待slaves也保存成功后才向客户端反馈结果,除非显式的指定了WAIT指令。
multi-key指令仅限于单个节点内,除了resharding操作外,节点的数据不会在节点间迁移。每个操作只会在特定的一个节点上执行,所以集群的性能为master节点的线性扩展。
客户端与每个nodes保持连接,所以请求的延迟等同于单个节点,即请求的延迟并不会因为Cluster的规模增大而受到影响。高性能和扩展性,同时保持合理的数据安全性,是Redis Cluster的设计目标。
Redis Cluster没有Proxy层,Client请求的数据也无法在nodes间merge;因为Redis核心就是K-V数据存储,没有scan类型(sort,limit,group by)的操作,因此merge操作并不被Redis Cluster所接受,而且这种特性会极大增加了Cluster的设计复杂度。
Redis Cluser 采用虚拟槽分区,所有的键根据哈希函数映射到 0~ 16383 整数槽内,计算公式:slot= CRC16(key)& 16383。 每一个节点负责维护一部分槽以及槽所 映射的键值数据
Redis 虚拟槽分区的特点:
在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障转移等状态信息。常见的元数据维护方式分为:集中式和p2p方式。Redis 集群采用P2P的Gossip协议,Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。
通信流程如下:
1)集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加 10000。
2)每个节点在固定周期内通过特定规则选择几个节点发送 ping 消息。
3)接收到 ping 消息的节点用 pong 消息作为响应。
集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出故障、新节点加入、主从切换、槽信息变更等事件发生时,通过不断的 ping/pong 消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的。
Gossip 协议的主要职责就是信息交换,信息交换的内容就是节点之间发送的Gossip消息。
常用的Gossip消息分为:ping 消息、pong消息、meet消息、fail消息等,它们的通信模式如下:
1)meet 消息:用于通知新节点加入。 消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点 会加入到集群中并进行周期性的ping、pong消息交换。
2)集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送 ping 消息,用于检测节点是否在线和交换彼此状态信息。ping 消息发送封装了自身节点和部分其他节点的状态数据。
3)pong 消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong 消息内部封装了自身状态数据。节点也可以向集群内广播自身的 pong 消息来通知整个集群对自身状态进行更新。
4)fail 消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 fail 消息,其他节点接收到 fail 消息 之后把对应节点更新为下线状态。
虽然 Gossip 协议的信息交换机制具有天然的分布式特性,但它是有成本的。
由于内部需要频繁地进行节点信息交换,而 ping/pong 消息会携带当前节点和部分其他节点的状态数据,必然会加重 带宽和计算的负担。
Redis 集群内节点通信采用固定频率(定时任务每秒执行10次)。因此节点每次选择需要通信的节点列表变得非常重要。
通信节点选择过多虽然可以做到信息及时交换但成本过高。
节点选择过少会降低集群内所有节点彼此信息交换频率,从而影响故障判定、新节点发现等需求的速度。
因此Redis 集群的 Gossip 协议需要兼顾信息交换实时性和成本开销,通信节点选择的规则如下:
根据通信节点选择的流程可以看出消息交换的成本主要体现在单位时间选择发送消息的节点数量和每个消息携带的数据量。
集群内每个节点维护定时任务默认每秒执行10次
每秒会随机选取 5 个节点找出最久没有通信的节点发送 ping消息,用于保证 Gossip 信息交换的随机性。
每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受 pong 消息的时间大于 cluster_ node_ timeout/ 2, 则立刻发送ping消息,防止该节点信息太长时间未更新。
根据以上规则得出每个节点每秒需要发送 ping 消息的数量= 1+ 10* num(node.pong_ received > cluster_node_timeout/2),因此 cluster_node_timeout 参数对消息发送的节点数量影响非常大。
当带宽资源紧张时,可以适当调大这个参数,如从默认 15 秒改为30 秒来降低带宽占用率。过度调 大cluster_node_ timeout 会影响消息交换的频率从而影响故障转移、槽信息更新、新节点发现的速度。
因此需要根据业务容忍度和资源消耗进行平衡。同时整个集群消息总交换量也跟节点数成正比。
每个ping消息的数据量体现在消息头和消息体中,其中消息头占用 2KB,这块空间占用相对固定。消息体会携带一定 数量的其他节点信息用于信息交换。消息体携带数据量跟集群的节点数息息相关,更大的集群每次消息通信的成本也就更高。
Redis 集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群扩容和缩容。
Redis 集群可以实现对节点的灵活上下线控制。其中原理可抽象为槽和对应数据在不同节点之间灵活移动。
槽和数据与节点之间的关系:
三个主节点分别维护自己负责的槽和对应的数据,当加入1个节点实现集群扩容时,需要把一部分槽和数据迁移到新节点,如下:
在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复 MOVED 重定向错误,通知客户端请求正确的节点,这个过程称为 MOVED 重定向。
大多数开发语言的 Redis 客户端都采用 Smart 客户端支持集群协议,客户端如何选择见:http://redis.io/clients。 Smart客户端通过在内部维护 slot → node 的映射关系,本地就可实现键到节点的查找,从而保证IO 效率的最大化,而 MOVED 重定向 负责协助Smart客户端更新 slot → node 映射。
Redis 集群支持在线迁移槽(slot)和数据来完成水平伸缩,当 slot 对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。例如当一个 slot 数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点, 而另一部分在目标节点。
ASK 重定向流程如下:
1)客户端根据本地 slots 缓存发送命令到源节点,如果存在键对象则直接执行并返回结果给客户端。
2)如果键对象不存在,则可能存在于目标节点,这时源节点会回复 ASK 重定向异常。
3)客户端从 ASK 重定向异常提取出目标节点信息,发送 asking 命令到目标节点打开客户端连接标识,再 执行 键 命令。如果存在则执行,不存在则返回不存在信息。
ASK 与 MOVED 虽然都是对客户端的重定向控制,但是有着本质区别。ASK 重定向说明集群正在进行 slot 数据迁移,客户端无法知道什么时候迁移完成,因此只能临时重定向,客户端不会更新slots缓存。但是MOVED重定向说明键对应的槽已经指向了新的节点,因此需要更新slots缓存。
Redis 集群自身实现了高可用。当集群内的节点出现故障时,通过自动故障转移保证集群可以正常的提供服务。
Redis 集群内节点通过ping/pong消息来实现节点通信,该消息不但可以传送节点槽信息,还可以传送其他状态信息,比如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,故障发现主要包含以下两个环节:主观下线(pfail)和客观下线(fail)
某个节点认为另一个节点不可用,即下线状态,这个状态不是最终的判定,只能代表一个节点的判断,会存在误判的情况。
集群中每个节点都会定期向其他节点发送 ping 消息,收到的节点回复 pong 消息作为响应。如果在 cluster-node-timeout 时间内通信一直失败,则发送节点会认为接收节点存在故障,就会将接收节点标记为主观下线状态(pfail)
1)节点 a 发送 ping 消息给节点 b,如果通信正常将接收到 pong 消息,节点 a 更新最 一次与节点 b 的通信时间。
2)如果节点 a 与节点 b 通信出现问题则断开连接,下次会进行重连。如果一直通信失败,则节点 a 记录的与节点 b 最后通信时间将无法更新。
3)节点 a 内的定时任务检测到与节点 b 最后通信时间超过 cluster-node-timeout 时,更新本地记录的节点 b 为主观下 线(pfail)状态。
集群中多个节点都认为某个节点不可用,从而达成共识的结果,就叫做客观下线。如果持有槽的主节点故障,需要为该节点进行故障转移。
当某个节点判断另一个节点主观下线后,节点状态信息会随着节点通信在集群内传播。ping/pong消息体会携带其他节点的状态信息,当接收节点发现消息体中有主观下线节点的信息时,会将下线节点信息维护在本地的下线报告链表中。
通过 Gossip 消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下 线时,就会触发客观下线流程。
在此有两个问题我们要弄明白,如下:
1)为什么必须是负责槽的主节点参与故障发现决策?
因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行主节点数据和状态信息的复制。
2)为什么必须是半数以上处理槽的主节点?
必须半数以上是为了应对网络分区等原因造成的集群分割情况,被分割的小集群因为无法完成从主观下线到客观下线这一关键过程,从而防止小集群完成故障转移之后继续对外提供服务。
1、下线报告链表维护
每个节点 ClusterNode 结构中都会维护一个下线链表结构,保存了其他主节点针对当前节点的下线报告,下线报告中保存了故障的节点结构和最近收到下线报告的时间,当接收到 fail 状态时,会更新对应节点的下线信息。
每个下线报告都存在有效期,每次在尝试客观下线时,都会检测下线报告是否过期,过期的下线报告将被删除。如果在 cluster-node-time * 2 的时间内下线报告没有更新则过期并删除。
2、尝试客观下线
集群中的节点每次收到其他节点的pfail状态,都会舱室触发客观下线,流程如下:
1)首先统计有效的下线报告数量,如果小于集群内持有槽的主节点总数的一半则退出。
2)当下线报告大于槽主节点数量一半时,标记对应故障节点为客观下线状态。
3)向集群广播一条 fail 消息,通知所有的节点将故障节点标记为客观下线,fail 消息的消息体只包含故障节点的 ID。
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。 下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。
每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过 cluster-node-time * cluster-slave-validity-factor,则当前从节点不具备故障转移资格。 参数 cluster-slave-validity-factor 用于判断从节点是否有资格的有效因子,默认为 10。
当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。
这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明 从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。
当从节点定时任务检测到故障选举时间(failover_auth_time)到达后,发起选举流程如下:
1)更新配置纪元
配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元( clusterNode.configEpoch) 表示当前主节点的版本,所有主节点的配置纪元都不相等,从节点会复制主节点的配置纪元。整个集群又维护一个全局的配置纪元(clusterState.currentEpoch),用于记录集群内所有主节点配置纪元的最大版本。
配置纪元的应用场景有:
2)广播选举消息
在集群内广播选举消息(FAILOVER_AUTH_REQUEST),并记录已发送过消息的状态,保证该从节点一个配置纪元内只能发起一次选举。消息内容如同 ping 消息只是将 type 类型变为 FAILOVER_AUTH_REQUEST。
只有持有槽的主节点才会处理故障选举消息(FAILOVER_AUTH_REQUEST),因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复 FAILOVER_AUTH_ACK 消息作为投票,只有相同配置纪元内其他从节点的选举消息将被忽略。
当从节点收集到足够的选票之后,将触发替换主节点的操作:
1)当前从节点取消复制变为主节点
2)执行 clusterDelSlot 操作撤销故障主节点负责的槽,并执行 clusterAddSlot 把这些槽委派给自己
3)向集群广播自己的 pong 消息,通知 集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。
参考资料:
Redis 设计与实现
Redis 开发与运维
Redis集群:https://redis.io/topics/cluster-tutorial
Redis集群规范:https://redis.io/topics/cluster-spec