redis集群
Redis Cluster是Redis的分布式解决方案, 在3.0版本正式推出, 有效地解决了Redis分布式方面的需求。 当遇到单机内存、 并发、 流量等瓶颈时, 可以采用Cluster架构方案达到负载均衡的目的。
Redis Cluster, 它非常优雅地解决了Redis集群方面的问题, 因此理解应用好Redis Cluster将极大地解放我们使用分布式Redis的工作量, 同时它也是学习分布式存储的绝佳案例。
数据分布
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题, 即把数据集划分到多个节点上, 每个节点负责整体数据的一个子集。
1.节点取余分区
使用特定的数据, 如Redis的键或用户ID, 再根据节点数量N使用公式:hash(key) %N计算出哈希值, 用来决定数据映射到哪一个节点上。 这种方案存在一个问题: 当节点数量变化时, 如扩容或收缩节点, 数据节点映射关系需要重新计算, 会导致数据的重新迁移。
2.一致性哈希分区
一致性哈希分区(Distributed Hash Table) 实现思路是为系统中每个节点分配一个token, 范围一般在0~232, 这些token构成一个哈希环。 数据读写执行节点查找操作时, 先根据key计算hash值, 然后顺时针找到第一个大于等于该哈希值的token节点。
这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点, 对其他节点无影响。 但一致性哈希分区存在几个问题:
·当使用少量节点时, 节点变化将大范围影响哈希环中数据映射, 因此这种方式不适合少量数据节点的分布式方案。
·普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。
3.虚拟槽分区
虚拟槽分区巧妙地使用了哈希空间, 使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中, 整数定义为槽(slot) 。 这个范围一般远远大于节点数, 比如Redis Cluster槽范围是0~16383。 槽是集群内数据管理和迁移的基本单位。 采用大范围槽的主要目的是为了方便数据拆分和集群扩展。
如图0~16383的槽平均分配给5个节点
Redis虚拟槽分区的特点:
·解耦数据和节点之间的关系, 简化了节点扩容和收缩难度。
·节点自身维护槽的映射关系, 不需要客户端或者代理服务维护槽分区元数据。
·支持节点、 槽、 键之间的映射查询, 用于数据路由、 在线伸缩等场景。
集群功能限制
由于节点并不能存到同一个节点,所以大量的批量操作不能再集群上面使用,pipline,mget,事务等。
1) key批量操作支持有限。 如mset、 mget, 目前只支持具有相同slot值的key执行批量操作。 对于映射为不同slot值的key由于执行mget、 mget等操作可能存在于多个节点上因此不被支持。
2) key事务操作支持有限。 同理只支持多key在同一节点上的事务操作, 当多个key分布在不同的节点上时无法使用事务功能。
3) key作为数据分区的最小粒度, 因此不能将一个大的键值对象如hash、 list等映射到不同的节点。
4) 不支持多数据库空间。 单机下的Redis可以支持16个数据库, 集群模式下只能使用一个数据库空间, 即db0。
5) 复制结构只支持一层, 从节点只能复制主节点, 不支持嵌套树状复制结构。
搭建集群
搭建集群工作需要以下三个步骤:
1) 准备节点。
2) 节点握手。
3) 分配槽。
1 准备节点
Redis集群一般由多个节点组成, 节点数量至少为6个才能保证组成完整高可用的集群。 每个节点需要开启配置cluster-enabled yes, 让Redis运行在集群模式下。 建议为集群内所有节点统一目录, 一般划分三个目录: conf、data、 log, 分别存放配置、 数据和日志相关文件。 把6个节点配置统一放在conf目录下, 集群相关配置如下:
#节点端口
port 6379
#开启集群模式
cluster-enabled yes
#节点超时时间, 单位毫秒
cluster-node-timeout 15000
#集群内部配置文件
cluster-config-file “nodes-6379.conf”
其他配置和单机模式一致即可, 配置文件命名规则redis-{port}.conf, 准备好配置后启动所有节点。
检查节点日志是否正确
cat log/redis-6379.log
*No cluster configuration found, I’m cfb28ef1deee4e0fa78da86abe5d24566744411e
#Server started, Redis version 3.0.7
*The server is now ready to accept connections on port 6379
第一次启动时如果没有集群配置文件, 它会自动创建一份, 文件名称采用cluster-config-file参数项控制, 建议采用node-{port}.conf格式定义, 通过使用端口号区分不同节点, 防止同一机器下多个节点彼此覆盖, 造成集群信息异常。 如果启动时存在集群配置文件, 节点会使用配置文件内容初始化集群信息。
集群模式的Redis除了原有的配置文件之外又加了一份集群配置文件。当集群内节点信息发生变化, 如添加节点、 节点下线、 故障转移等。 节点会自动保存集群状态到配置文件中。 需要注意的是, Redis自动维护集群配置文件, 不要手动修改, 防止节点重启时产生集群信息错乱。
如节点6379首次启动后生成集群配置如下:
#cat data/nodes-6379.conf
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
文件内容记录了集群初始状态, 这里最重要的是节点ID, 它是一个40位16进制字符串, 用于唯一标识集群内一个节点, 之后很多集群操作都要借助于节点ID来完成。 需要注意是, 节点ID不同于运行ID。 节点ID在集群初始化时只创建一次, 节点重启时会加载集群配置文件进行重用, 而Redis的运行ID每次重启都会变化。
2 节点握手
节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信,达到感知对方的过程。 节点握手是集群彼此通信的第一步, 由客户端发起命令: cluster meet{ip}{port}。
对节点6379和6380分别执行cluster nodes命令, 可以看到它们彼此已经感知到对方的存在。
127.0.0.1:6379> cluster nodes
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0
0 connected
8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380 master - 0 1468073534265
1 connected
127.0.0.1:6380> cluster nodes
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 master - 0 1468073571641
0 connected
8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380 myself,master - 0 0
1 connected
我们只需要在集群内任意节点上执行cluster meet命令加入新节点, 握手状态会通过消息在集群内传播, 这样其他节点会自动发现新节点并发起握手流程。
节点建立握手之后集群还不能正常工作, 这时集群处于下线状态, 所有的数据读写都被禁止。由于目前所有的槽没有分配到节点, 因此集群无法完成槽到节点的映射。 只有当16384个槽全部分配给节点后, 集群才进入在线状态。
3 分配槽
Redis集群把所有的数据映射到16384个槽中。 每个key会映射为一个固定的槽, 只有当节点分配了槽, 才能响应和这些槽关联的键命令。 通过cluster addslots命令为节点分配槽。 这里利用bash特性批量设置槽(slots) ,命令如下:
redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0…5461}
redis-cli -h 127.0.0.1 -p 6380 cluster addslots {5462…10922}
redis-cli -h 127.0.0.1 -p 6381 cluster addslots {10923…16383}
把16384个slot平均分配给6379、 6380、 6381三个节点。 执行cluster info查看集群状态, 如下所示:
127.0.0.1:6379> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:5
cluster_my_epoch:0
cluster_stats_messages_sent:4874
cluster_stats_messages_received:4726
目前还有三个节点没有使用, 作为一个完整的集群, 每个负责处理槽的节点应该具有从节点, 保证当它出现故障时可以自动进行故障转移。 集群模式下, Reids节点角色分为主节点和从节点。 首次启动的节点和被分配槽的节点都是主节点, 从节点负责复制主节点槽信息和相关的数据。 使用cluster replicate{nodeId}命令让一个节点成为从节点。 其中命令执行必须在对应的从节点上执行, nodeId是要复制主节点的节点ID, 命令如下:
127.0.0.1:6382>cluster replicate cfb28ef1deee4e0fa78da86abe5d24566744411e OK
127.0.0.1:6383>cluster replicate 8e41673d59c9568aa9d29fb174ce733345b3e8f1 OK
127.0.0.1:6384>cluster replicate 40b8d09d44294d2e23c7c768efc8fcd153446746 OK
Redis集群模式下的主从复制使用了之前介绍的Redis复制流程, 依然支持全量和部分复制。
目前为止, 我们依照Redis协议手动建立一个集群。 它由6个节点构成,3个主节点负责处理槽和相关数据, 3个从节点负责故障转移。 手动搭建集群便于理解集群建立的流程和细节, 不过读者也从中发现集群搭建需要很多步骤, 当集群节点众多时, 必然会加大搭建集群的复杂度和运维成本。 因此Redis官方提供了redis-trib.rb工具方便我们快速搭建集群。这里就不介绍具体的工具了。
节点通信
1 通信流程
在分布式存储中需要提供维护节点元数据信息的机制, 所谓元数据是指: 节点负责哪些数据, 是否出现故障等状态信息。 常见的元数据维护方式分为: 集中式和P2P方式。 Redis集群采用P2P的Gossip(流言) 协议,Gossip协议工作原理就是节点彼此不断通信交换信息, 一段时间后所有的节
点都会知道集群完整的信息, 这种方式类似流言传播。
通信过程说明:
1) 集群中的每个节点都会单独开辟一个TCP通道, 用于节点之间彼此通信, 通信端口号在基础端口上加10000。
2) 每个节点在固定周期内通过特定规则选择几个节点发送ping消息。
3) 接收到ping消息的节点用pong消息作为响应。
集群中每个节点通过一定规则挑选要通信的节点, 每个节点可能知道全部节点, 也可能仅知道部分节点, 只要这些节点彼此可以正常通信, 最终它们会达到一致的状态。 当节点出故障、 新节点加入、 主从角色变化、 槽信息变更等事件发生时, 通过不断的ping/pong消息通信, 经过一段时间后所有的节点都会知道整个集群全部节点的最新状态, 从而达到集群状态同步的目的。
2 Gossip消息
常用的Gossip消息可分为: ping消息、 pong消息、 meet消息、 fail消息等
·meet消息: 用于通知新节点加入。 消息发送者通知接收者加入到当前集群, meet消息通信正常完成后, 接收节点会加入到集群中并进行周期性的ping、 pong消息交换。
·ping消息: 集群内交换最频繁的消息, 集群内每个节点每秒向多个其他节点发送ping消息, 用于检测节点是否在线和交换彼此状态信息。 ping消息发送封装了自身节点和部分其他节点的状态数据。
·pong消息: 当接收到ping、 meet消息时, 作为响应消息回复给发送方确认消息正常通信。 pong消息内部封装了自身状态数据。 节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
·fail消息: 当节点判定集群内另一个节点下线时, 会向集群内广播一个fail消息, 其他节点接收到fail消息之后把对应节点更新为下线状态。
接收节点收到ping/meet消息时, 执行解析消息头和消息体流程:
·解析消息头过程: 消息头包含了发送节点的信息, 如果发送节点是新节点且消息是meet类型, 则加入到本地节点列表; 如果是已知节点, 则尝试更新发送节点的状态, 如槽映射关系、 主从角色等状态。
·解析消息体过程: 如果消息体的clusterMsgDataGossip数组包含的节点是新节点, 则尝试发起与新节点的meet握手流程; 如果是已知节点, 则根据cluster MsgDataGossip中的flags字段判断该节点是否下线, 用于故障转移。
消息处理完后回复pong消息, 内容同样包含消息头和消息体, 发送节点接收到回复的pong消息后, 采用类似的流程解析处理消息并更新与接收节点最后通信时间, 完成一次消息通信。
3 节点选择
由于内部需要频繁地进行节点信息交换,而ping/pong消息会携带当前节点和部分其他节点的状态数据, 势必会加重带宽和计算的负担。通讯节点选择过多则增加网络负担,过少则影响故障判断,新节点加入的速度。因此Redis集群的Gossip协议需要兼顾信息交换实时性和成本开销。
消息交换的成本主要体现在单位时间选择发送消息的节点数量和每个消息携带的数据量。
·选择发送消息的节点数量
集群内每个节点维护定时任务默认每秒执行10次, 每秒会随机选取5个节点找出最久没有通信的节点发送ping消息, 用于保证Gossip信息交换的随机性。 每100毫秒都会扫描本地节点列表, 如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2, 则立刻发送ping消息, 防止该节点信息太长时间未更新。
·消息数据量
每个ping消息的数据量体现在消息头和消息体中, 其中消息头主要占用空间的字段是myslots[CLUSTER_SLOTS/8], 占用2KB, 这块空间占用相对固定。 消息体会携带一定数量的其他节点信息用于信息交换。消息体携带数据量跟集群的节点数息息相关, 更大的集群每次消息通信的成本也就更高, 因此对于Redis集群来说并不是大而全的集群更好。
集群伸缩
Redis集群提供了灵活的节点扩容和收缩方案。 在不影响集群对外服务的情况下, 可以为集群添加节点进行扩容也可以下线部分节点进行缩容。
从图看出, Redis集群可以实现对节点的灵活上下线控制。 其中原理可抽象为槽和对应数据在不同节点之间灵活移动。
如图是之前集群的槽分配情况。如果要新增一个节点的话则只需要每个节点把一部分槽和数据迁移到新的节点。集群的水平伸缩的上层原理: 集群伸缩=槽和数据在节点之间的移动。
2 扩容集群
Redis集群扩容操作可分为如下步骤:
1) 准备新节点。
需要提前准备好新节点并运行在集群模式下, 新节点建议跟集群内的节点配置保持一致, 便于管理统一。启动后的新节点作为孤儿节点运行, 并没有其他节点与之通信。
2) 加入集群。
新节点依然采用cluster meet命令加入到现有集群中。 在集群内任意节点执行cluster meet命令让6385和6386节点加入进来
127.0.0.1:6380>cluster ndoes
1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 127.0.0.1:6385 master - 0 1469347800759
7 connected
475528b1bcf8e74d227104a6cf1bf70f00c24aae 127.0.0.1:6386 master - 0 1469347798743
8 connected
新节点刚开始都是主节点状态, 但是由于没有负责的槽, 所以不能接受任何读写操作。 对于新加的节点一般为他迁移槽或者是做其他主节点的从节点。
3) 迁移槽和数据。
加入集群后需要为新节点迁移槽和相关数据, 槽在迁移过程中集群可以正常提供读写服务, 迁移过程是集群扩容最核心的环节。
(1) 槽迁移计划
槽是Redis集群管理数据的基本单位, 首先需要为新节点制定槽的迁移计划, 确定原有节点的哪些槽需要迁移到新节点。 迁移计划需要确保每个节点负责相似数量的槽, 从而保证各节点的数据均匀。
1) 对目标节点发送cluster setslot{slot}importing{sourceNodeId}命令, 让目标节点准备导入槽的数据。
2) 对源节点发送cluster setslot{slot}migrating{targetNodeId}命令, 让源节点准备迁出槽的数据。
3) 源节点循环执行cluster getkeysinslot{slot}{count}命令, 获取count个属于槽{slot}的键。
4) 在源节点上执行migrate{targetIp}{targetPort}""0{timeout}keys{keys…}命令, 把获取的键通过流水线(pipeline) 机制批量迁移到目标节点, 批量迁移版本的migrate命令在Redis3.0.6以上版本提供, 之前的migrate命令只能单个键迁移。 对于大量key的场景, 批量键迁移将极大降低节点之间网络IO次数。
5) 重复执行步骤3) 和步骤4) 直到槽下所有的键值数据迁移到目标节点。
6) 向集群内所有主节点发送cluster setslot{slot}node{targetNodeId}命令, 通知槽分配给目标节点。 为了保证槽节点映射变更及时传播, 需要遍历发送给所有主节点更新被迁移的槽指向新节点。
3 收缩集群
收缩集群意味着缩减规模, 需要从现有集群中安全下线部分节点。
1) 首先需要确定下线节点是否有负责的槽, 如果是, 需要把槽迁移到其他节点, 保证节点下线后整个集群槽节点映射的完整性。
2) 当下线节点不再负责槽或者本身是从节点时, 就可以通知集群内其他节点忘记下线节点, 当所有的节点忘记该节点后可以正常关闭。
下线迁移槽
下线节点需要把自己负责的槽迁移到其他节点, 原理与之前节点扩容的迁移槽过程一致。
忘记节点
由于集群内的节点不停地通过Gossip消息彼此交换节点状态, 因此需要通过一种健壮的机制让集群内所有节点忘记下线的节点。 也就是说让其他节点不再与要下线节点进行Gossip消息交换。 Redis提供了cluster forget{downNodeId}命令实现该功能。
当节点接收到cluster forget{down NodeId}命令后, 会把nodeId指定的节点加入到禁用列表中, 在禁用列表内的节点不再发送Gossip消息。 禁用列表有效期是60秒, 超过60秒节点会再次参与消息交换。 也就是说当第一次forget命令发出后, 我们有60秒的时间让集群内的所有节点忘记下线节点。
线上操作不建议直接使用cluster forget命令下线节点, 需要跟大量节点命令交互, 实际操作起来过于繁琐并且容易遗漏forget节点。 建议使用redistrib.rb del-node{host: port}{downNodeId}命令。
请求路由
请求重定向
在集群模式下, Redis接收任何键相关命令时首先计算键对应的槽, 再根据槽找出所对应的节点, 如果节点是自身, 则处理键命令; 否则回复MOVED重定向错误, 通知客户端请求正确的节点。 这个过程称为MOVED重定向。
可以加入-c参数来自动完成重定向 redis-cli自动帮我们连接到正确的节点执行命令, 这个过程是在redis-cli内部维护, 实质上是client端接到MOVED信息之后再次发起请求, 并不在Redis节点中完成请求转发。
redis主节点接受到客户端命令时会计算操作的key的slot是否是本节点处理的,是则执行对应操作,不是则返回moved错误,并指定key的位置。所以redis主节点必须维护一个集群的slot数据结构。
由此可见,如果客户端可以随机连接一个节点以获取对应的key的真正位置。这样优点就是客户端实现简单,只需重定向发送即可。但是它的弊端很明显, 每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点, 额外增加了IO开销,这不是Redis集群高效的使用方式。
Smart客户端通过在内部维护slot→node的映射关系, 本地就可实现键到节点的查找, 从而保证IO效率的最大化, 而MOVED重定向负责协助Smart客户端更新slot→node映射。 我们以Java的Jedis为例, 说明Smart客户端操作集群的流程。
1) 首先在JedisCluster初始化时会选择一个运行节点, 初始化槽和节点映射关系, 使用cluster slots命令完成。
2) JedisCluster解析cluster slots结果缓存在本地, 并为每个节点创建唯一的JedisPool连接池。 映射关系在JedisClusterInfoCache类
3) JedisCluster执行键命令的过程有些复杂, 但是理解这个过程对于开发人员分析定位问题非常有帮助
键命令执行流程:
1) 计算slot并根据slots缓存获取目标节点连接, 发送命令。
2) 如果出现连接错误, 使用随机连接重新执行键命令, 每次命令重试对redi-rections参数减1。
3) 捕获到MOVED重定向错误, 当重试次数到最后1次或者出现MovedDataException时才更新slots操作, 降低了cluster slots命令调用次数 。
4) 重复执行1) ~3) 步, 直到命令执行成功, 或者当redirections<=0时抛出Jedis ClusterMaxRedirectionsException异常。
ASK重定向
Redis集群支持在线迁移槽(slot) 和数据来完成水平伸缩, 当slot对应的数据从源节点到目标节点迁移过程中, 客户端需要做到智能识别, 保证键命令可正常执行。 例如当一个slot数据从源节点迁移到目标节点时, 期间可能出现一部分数据在源节点, 而另一部分在目标节点。
当出现上述情况时, 客户端键命令执行流程将发生变化, 如下所示:
1) 客户端根据本地slots缓存发送命令到源节点, 如果存在键对象则直接执行并返回结果给客户端。
2) 如果键对象不存在, 则可能存在于目标节点, 这时源节点会回复ASK重定向异常。 格式如下: (error) ASK{slot}{targetIP}: {targetPort}。
3) 客户端从ASK重定向异常提取出目标节点信息, 发送asking命令到目标节点打开客户端连接标识, 再执行键命令。 如果存在则执行, 不存在则返回不存在信息。
ASK与MOVED虽然都是对客户端的重定向控制, 但是有着本质区别。ASK重定向说明集群正在进行slot数据迁移, 客户端无法知道什么时候迁移完成, 因此只能是临时性的重定向, 客户端不会更新slots缓存。 但是MOVED重定向说明键对应的槽已经明确指定到新的节点, 因此需要更新slots缓存。
为了支持ASK重定向, 源节点和目标节点在内部的clusterState结构中维护当前正在迁移的槽信息, 用于识别槽迁移情况。
typedef struct clusterState {
clusterNode myself; / 自身节点 /
clusterNode slots[CLUSTER_SLOTS]; / 槽和节点映射数组 */
clusterNode migrating_slots_to[CLUSTER_SLOTS];/ 正在迁出的槽节点数组 /
clusterNode importing_slots_from[CLUSTER_SLOTS];/ 正在迁入的槽节点数组/
…
} clusterState;
节点每次接收到键命令时, 都会根据clusterState内的迁移属性进行命令处理, 如下所示:
·如果键所在的槽由当前节点负责, 但键不存在则查找migrating_slots_to数组查看槽是否正在迁出, 如果是返回ASK重定向。
·如果客户端发送asking命令打开了CLIENT_ASKING标识, 则该客户端下次发送键命令时查找importing_slots_from数组获取clusterNode, 如果指向自身则执行命令。
·需要注意的是, asking命令是一次性命令, 每次执行完后客户端标识都会修改回原状态, 因此每次客户端接收到ASK重定向后都需要发送asking命令。
·批量操作。 ASK重定向对单键命令支持得很完善, 但是, 在开发中我们经常使用批量操作, 如mget或pipeline。 当槽处于迁移状态时, 批量操作会受到影响。
使用jediscluster.mget等批量操作命令的话,如果key对应的slot不属于当前节点,则抛出异常。
[value:68253, value:79212]
redis.clients.jedis.exceptions.JedisDataException: TRYAGAIN Multiple keys request during rehashing of slot
at redis.clients.jedis.Protocol.processError(Protocol.java:127)
对于需要批量使用的场景的话建议使用pipline,pipline对于不属于当前节点的key值直接返回异常对象,我们可以分析这个异常对象再去访问对应的key节点。
故障转移
Redis集群自身实现了高可用。 高可用首先需要解决集群部分失败的场景: 当集群内少量节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。
1 故障发现
当集群内某个节点出现问题时, 需要通过一种健壮的方式保证识别出节点是否发生了故障。 Redis集群内节点通过ping/pong消息实现节点通信, 消息不但可以传播节点槽信息, 还可以传播其他状态如: 主从状态、 节点故障等。 因此故障发现也是通过消息传播机制实现的, 主要环节包括: 主观下线(pfail) 和客观下线(fail) 。
1.主观下线
集群中每个节点都会定期向其他节点发送ping消息, 接收节点回复pong消息作为响应。 如果在cluster-node-timeout时间内通信一直失败, 则发送节点会认为接收节点存在故障, 把接收节点标记为主观下线(pfail) 状态。主观下线简单来讲就是, 当cluster-note-timeout时间内某节点无法与另一个节点顺利完成ping消息通信时, 则将该节点标记为主观下线状态。 每个节点内的cluster State结构都需要保存其他节点信息, 用于从自身视角判断其他节点的状态。
2.客观下线
当某个节点判断另一个节点主观下线后, 相应的节点状态会跟随消息在集群内传播。 ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现消息体中含有主观下线的节点状态时, 会在本地找到故障节点的ClusterNode结构, 保存到下线报告链表中。
每个节点都会维护一个故障表,当接收到主节点发送的主观下线时,更新本地的故障表,下线报告的有效期限是server.cluster_node_timeout*2,过期之后自动删除,尝试客观下线。客观下线首先会去计算有效期内报告的数量,如果数量大于集群中持有槽节点的一半则向集群节点广播下线节点的fail消息。
2 故障恢复
故障节点变为客观下线后, 如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它, 从而保证集群的高可用。 下线主节点的所有从节点承担故障恢复的义务, 当从节点通过内部定时任务发现自身复制的主节点进入客观下线时, 将会触发故障恢复流程。
1).资格检查
每个从节点都要检查最后与主节点断线时间, 判断是否有资格替换故障的主节点。 如果从节点与主节点断线时间超过cluster-node-time*cluster-slave-validity-factor, 则当前从节点不具备故障转移资格。 参数cluster-slavevalidity-factor用于从节点的有效因子, 默认为10。
2).准备选举时间
当从节点符合故障转移资格后, 更新触发故障选举的时间, 只有到达该时间后才能执行后续流程。
这里之所以采用延迟触发机制, 主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。 复制偏移量越大说明从节点延迟越低, 那么它应该具有更高的优先级来替换故障主节点.所有的从节点中复制偏移量最大的将提前触发故障选举流程 复制偏移量越大,数据越完整。
3.发起选举
当从节点定时任务检测到达故障选举时间(failover_auth_time) 到达后, 发起选举。
(1) 更新配置纪元
配置纪元是一个只增不减的整数, 每个主节点自身维护一个配置纪元(clusterNode.configEpoch) 标示当前主节点的版本, 所有主节点的配置纪元都不相等, 从节点会复制主节点的配置纪元。 整个集群又维护一个全局的配置纪元(clusterState.current Epoch) , 用于记录集群内所有主节点配置纪元的最大版本。
配置纪元的主要作用:
·标示集群内每个主节点的不同版本和当前集群最大的版本。
·每次集群发生重要事件时, 这里的重要事件指出现新的主节点(新加入的或者由从节点转换而来) , 从节点竞争选举。 都会递增集群全局的配置纪元并赋值给相关主节点, 用于记录这一关键事件。
·主节点具有更大的配置纪元代表了更新的集群状态, 因此当节点间进行ping/pong消息交换时, 如出现slots等关键信息不一致时, 以配置纪元更大的一方为准, 防止过时的消息状态污染集群。
(2) 广播选举消息
在集群内广播选举消息(FAILOVER_AUTH_REQUEST) , 并记录已发送过消息的状态, 保证该从节点在一个配置纪元内只能发起一次选举。 消息内容如同ping消息只是将type类型变为FAILOVER_AUTH_REQUEST。
4.选举投票
只有持有槽的主节点才会处理故障选举消息(FAILOVER_AUTH_REQUEST) , 因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票, 当接到第一个请求投票的从节点消息时回复
FAILOVER_AUTH_ACK消息作为投票, **之后相同配置纪元内其他从节点的选举消息将忽略。**也就是说数据最完整的从节点将会优先获得投票。
投票过程其实是一个领导者选举的过程, 如集群内有N个持有槽的主节点代表有N张选票。 由于在每个配置纪元内持有槽的主节点只能投票给一个从节点, 因此只能有一个从节点获得N/2+1的选票, 保证能够找出唯一的从节点。
5.替换主节点
当从节点收集到足够的选票之后, 触发替换主节点操作:
1) 当前从节点取消复制变为主节点。
2) 执行clusterDelSlot操作撤销故障主节点负责的槽, 并执行clusterAddSlot把这些槽委派给自己。
3) 向集群广播自己的pong消息, 通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。
最后重启故障的主节点,该节点获取其他节点的ping信息得知该节点已经成为了从节点,则自动变成从节点。
集群读写分离
集群模式下从节点不接受任何读写请求, 发送过来的键命令会重定向到负责槽的主节点上(其中包括它的主节点) 。 当需要使用从节点分担主节点读压力时, 可以使用readonly命令打开客户端连接只读状态。 之前的复制配置slave-read-only在集群模式下无效。 当开启只读状态时, 从节点接收读命令处理流程变为: 如果对应的槽属于自己正在复制的主节点则直接执行读命令, 否则返回重定向信息。