1. 数据分区
redis cluser采用了虚拟槽分区,所有的键根据哈希函数映射到0 ~ 16383整数槽内。 公式:slot = CRC16(key)&16383 。每一个节点负责维护一部分槽以及槽所映射的键值数据。
特点:
- 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
- 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据
- 支持节点、槽、键之间的映射查询。
功能限制:
- key批量操作支持有限。 如mset, mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同的slot值的key由于执行mset,mget等操作可能存在多个节点上因此不被支持。
- key事务支持有限。同理只支持多key在同一节点下的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
- key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点。
- 不支持多数据库空间。 单机下的redis可以支持16个数据库,集群模式下只支持一个数据库, db 0。
- 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
2. 节点通信
在分布式存储中需要提供维护节点元数据信息的机制。元数据是指:节点复制哪些数据,是否出现故障等状态信息。
redis集群采用p2p的Gossip(流言)协议,原理是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息。
通信过程:
- 集群中的每个 节点都会独立开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000。
- 每个节点再固定周期内通过特定规则选择几个节点发送ping消息。
- 接收到ping信息的节点用pong消息作为响应。
3. 节点选择
Gossip协议需要频繁进行节点信息交换,而ping/pong消息会携带当前节点和部分其他节点的状态数据,加重了带宽和计算的负担。redis集群启用定时任务,每秒10次执行。 因此通信的节点选择就非常重要,节点选择多可以做到信息及时交换但成本高,节点选择少会影响故障判定,新节点发现等。节点选择需要兼顾实时性和成本开销。
选择规则: 每秒随机选取5个节点找出最久没有通信的节点发送ping消息。每100毫米都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout / 2,则立即发送ping消息,防止该节点信息太长时间没有更新。
4. 迁移槽和数据
加入集群后需要为新节点迁移槽和相关数据,槽在迁移过程中集群可以正常提供读写服务。数据迁移过程是逐个槽进行的,流程如下:
- 对目标节点发送cluster setslot {slot} importing {sourceNodeId} 命令,让目标节点准备导入槽的数据。
- 对源节点发送cluster setslot {slot} migrating {targetNodeId} 命令, 让源节点准备导入槽的数据。
- 源节点循环执行cluster getkeysinslot {slot} {count}命令,获取数据槽{slot}的键
- 在源节点上执行migrate {targetIp} {targetPort} “” 0 {timeout} keys {keys…}命令,把获取得键通过流水线机制批量迁移到目标节。
- 重复执行步骤3和4直到槽下所有的键值数据迁移到目标节点。
- 向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点。
收缩集群意味着缩减规模,需要从现有集群中安全下线部分节点。
- 首先需要确定下线节点是否有负责的槽,如果是,需要把槽迁移到其他节点,保证节点下线后整个集群槽点映射的完整性。
- 当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其他节点忘记下线节点。 当所有节点忘记该节点后可以正常关闭。
5. 请求路由
为了性能最大化,redis集群采用客户端直连节点的方式。
redis接收到任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理命令。否则回复MOVED重定向错误,通知客户端请求正确的节点。
6. 故障转移
6.1 故障发现
当集群某个节点出现问题时,需要通过一种健壮的方式保证识别出节点是否发生了故障。
主观下线:指某个节点认为另一个节点不可用,即下线状态,这个状态不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong信息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线状态。
客观下线:指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。
当某个节点判断另一个节点主观下线后,相应的节点状态会随消息在集群内传播。在ping/pong信息的信息体会携带集群1/10的其他节点状态数据。其他节点收到pfail信息后,会更新clusterNode内部下线报告链表(从节点忽略)
集群的节点每次接收到其他节点的pfail状态,都会尝试触发客观下线
- 首先统计有效的下线报告数量,如果小于集群内持有槽的主节点总数的一半则退出,
- 当下线报告大于槽主节点数量一半时,标记对应故障节点为客观下线状态。
- 向集群广播一条fail信息,通知所有的节点将故障节点标记为客观下线。
6.2 故障恢复
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它。下线的主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。
6.2.1. 资格检查
每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过cluster-node-time * cluster-slave-validity-factor,则当前从节点不具备故障转移资格。
6.2.2 准备选举时间
Raft协议选举过程中,所有参与选举的节点首先随机休眠一段时间,每个节点一旦唤醒就立刻向所有的投票节点发起拉票请求。对于投票节点来说,每一轮选举中只能投出一票,投票的规则就是先到先得。所以一般情况下,都是休眠时间最短的节点容易获得大部分投票。
在redis中,复制偏移量越大说明节点延时越低,那么它应该具有更高的优先级来替换故障主节点。因此各个从节点会根据复制偏移量进行排名。并根据下面公式计算出节点的准备选举时间:
DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds + SLAVE_RANK * 1000 milliseconds.
- 一部分为固定的500ms时间,这500ms主要是为了等待集群状态同步。
- 另一部分主要是一个随机的时间加上由该Slave节点的排名决定的附加时间。
6.2.3 发起选举
当从节点故障选举时间到达后,将发起选举
1. 更新配置纪元。 配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元标识当前主节点的版本,并且所有主节点的配置纪元都不想等,从节点会复制主节点的配置纪元。整个集群又维护一个全局的配置纪元,用于记录集群内所有主节点配置纪元的最大版本。
配置纪元的主要作用:
- 标示集群内每个主节点的不同版本和当前集群最大的版本。
- 每次集群发生重要事件时,如出现新的主节点,从节点竞争选举。都会递增集群全局的配置纪元并赋值给相关的主节点。
- 主节点具有更大的配置纪元代表了更新的集群状态,因此当节点间进行ping/pong信息交换时,如出现slots等关键信息不一致时,以配置纪元更大的一方为准,防止过时信息状态污染集群。
配置纪元的应用场景:
- 新节点加入
- 槽节点映射冲突检测。
- 从节点投票选举冲突检查。
2. 广播选举信息
在集群内广播选举信息,并记录已发送过信息的状态,保证该从节点再一个配置纪元内只能发起一次选举。
6.2.4 选举投票
只有持有槽的主节点才会处理故障选举信息,因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点信息时回复FAILOVER_AUTH_ACK信息作为投票,之后相同配置纪元内其他从节点的选举信息将忽略。
当从节点收到的票大于等于 N/2 + 1时,将成为主节点。
redis集群没有使用从节点进行领导者选举,是应为从节点必须大于等于3才能凑够 N/2 + 1,会导致资源浪费。
投票作废:每个配置纪元代表一次选举周期,如果在开始投票之后的cluster-node-timeout * 2时间内从节点没有获取足够数量的投票,则本次选举将作废。 从节点对配置纪元进行自增并发起下一轮投票,直到选举成功。
6.2.5 替换主节点
- 当前从节点取消复制变成主节点
- 执行clusterDelSlot操作撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己。
- 向集群广播自己的pong信息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。