1 数据分区
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
1.1 分区方案
常见的分区规则有哈希分区和顺序分区两种
【哈希分区】
-
节点取余:根据节点数量N使用公式:hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。
缺点:当节点数量变化时,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。
优点:这种方式的突出优点是简单性,常用于数据库的分库分表规则,一般采用预分区的方式,提前根据数据量规划好分区数。
-
一致性哈希:一致性哈希分区(Distributed Hash Table)实现思路是为系统中每个节点分配一个token,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点
缺点:普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。
优点:这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。 -
虚拟槽:使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。槽是集群内数据管理和迁移的基本单位,方便数据拆分和集群扩展。Redis Cluster就是采用虚拟槽分区
1.2 redis数据分区
Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。
Redis虚拟槽分区的特点:
-
解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
-
节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。
-
支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。
2 集群搭建
redis-trib.rb是采用Ruby实现的Redis集群管理工具。内部通过Cluster相关命令帮我们简化集群创建、检查、槽迁移和均衡等常见运维操作,使用之前需要安装Ruby依赖环境。但这里通过手动搭建集群分享一下搭建的过程。
-
准备节点:Redis集群一般由多个节点组成,节点数量至少为6个才能保证组成完整高可用的集群。但我们启动6个节点后,每个节点彼此并不知道对方的存在。
-
节点握手:节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信,达到感知对方的过程。可通过客户端发起命令:cluster meet{ip}{port},建立节点间通信,需要执行五次完成六个节点建立通信
命令解释:cluster meet命令是一个异步命令,执行之后立刻返回,内部发起与目标节点进行握手通信。
-
分配槽:Redis集群把所有的数据映射到16384个槽中。每个key会映射为一个固定的槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过cluster addslots命令为节点分配槽。
-
从节点配置:作为一个完整的集群,每个负责处理槽的节点应该具有从节点,保证当它出现故障时可以自动进行故障转移。使用cluster replicate{nodeId}命令让一个节点成为从节点。
3 节点通信
3.1 Gossip消息
Gossip(流言)协议的主要职责就是信息交换,节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息。信息交换的载体就是节点彼此发送的Gossip消息,了解这些消息有助于我们理解集群如何完成信息交换。
-
meet消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
-
ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息发送封装了自身节点和部分其他节点的状态数据。
-
pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
-
fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
所有的消息格式划分为:消息头和消息体。
-
消息头包含发送节点自身状态数据,如节点id、槽映射、节点标识(主从角色,是否下线)等。
-
消息体clusterMsgData定义发送消息的数据,其中ping、meet、pong都采用cluster MsgDataGossip数组作为消息体数据,实际消息类型使用消息头的type属性区分。每个消息体包含该节点的多个clusterMsgDataGossip结构数据,用于信息交换,结构如下:
3.2 通信流程
在分布式存储中需要提供维护节点元数据信息的机制
-
元数据是指:节点负责哪些数据,是否出现故障等状态信息。
-
常见方式分为:集中式和P2P方式。Redis集群采用P2P的Gossip(流言)协议。
通信过程说明:
-
集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000。
-
每个节点在固定周期内通过特定规则选择几个节点发送ping消息。
-
接收到ping消息的节点用pong消息作为响应。
问题:什么时间?与哪些节点通信?消息内容是什么?
虽然Gossip协议的信息交换机制具有天然的分布式特性,但它是有成本的。由于内部需要频繁地进行节点信息交换,而ping/pong消息会携带当前节点和部分其他节点的状态数据,势必会加重带宽和计算的负担。因此节点每次选择需要通信的节点列表变得非常重要。
-
通信时机:集群内每个节点维护定时任务默认每秒执行10次
-
选择节点:
-
每秒随机选取5个节点找出最久没有通信的节点发送ping消息,用于保证Gossip信息交换的随机性。
-
每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2,则立刻发送ping消息,防止该节点信息太长时间未更新。
-
-
消息内容:每个ping消息的数据分为消息头和消息体中
-
消息头:发送节点自身状态数据,如节点id、槽映射、节点标识(主从角色,是否下线)等。最占空间的字段是myslots[CLUSTER_SLOTS/8],占用2KB。
-
消息体:消息体会携带一定数量的其他节点信息用于信息交换。
-
4 故障转移
Redis集群自身实现了高可用。高可用首先需要解决集群部分失败的场景:当集群内少量节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。
4.1 故障发现
Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线
(pfail)和客观下线(fail)。
-
主观下线:指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。
-
客观下线:指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。
当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现消息体中含有主观下线的节点状态时,会在本地找到故障节点的ClusterNode结构,保存到下线报告链表中。
-
当消息体内含有其他节点的pfail状态会判断发送节点的状态,如果发送节点是主节点则对报告的pfail状态处理,从节点则忽略。
-
找到pfail对应的节点结构,更新clusterNode内部下线报告链表。
-
根据更新后的下线报告链表尝试进行客观下线。
【尝试客观下线】
-
首先统计有效的下线报告数量,如果小于集群内持有槽的主节点总数的一半则退出。
-
当下线报告大于槽主节点数量一半时,标记对应故障节点为客观下线状态。
-
向集群广播一条fail消息,通知所有的节点将故障节点标记为客观下线,fail消息的消息体只包含故障节点的ID。
4.2 故障恢复
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程
【资格检查】
每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过cluster-node-time*cluster-slave-660validity-factor,则当前从节点不具备故障转移资格。参数cluster-slave-validity-factor用于从节点的有效因子,默认为10。
【准备选举时间】
当从节点符合故障转移资格后,通过延迟触发机制,到达故障选举的时间后才能执行后续流程。
延迟触发机制:这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。
【发起选举】
当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:
-
更新配置纪元,配置纪元是一个只增不减的整数,用于记录集群内所有主节点配置纪元的最大版本。如出现slots等关键信息不一致时,以配置纪元更大的一方为准
-
广播选举消息,在集群内广播选举消息(FAILOVER_AUTH_REQUEST),并记录已发送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举。FAILOVER_AUTH_REQUEST。
【选举投票】
只有持有槽的主节点才会处理故障选举消息(FAILOVER_AUTH_REQUEST),因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,之后相同配置纪元内其他从节点的选举消息将忽略。
-
获得N/2+1的选票,则为新主节点。N为主节点数
-
只有主节点有投票资格:主要因为从节点数必须大于等于3个才能保证凑够N/2+1个节点,将导致从节点资源浪费。
投票作废:每个配置纪元代表了一次选举周期,如果在开始投票之后的cluster-node-timeout*2时间内从节点没有获取足够数量的投票,则本次选举作废。从节点对配置纪元自增并发起下一轮投票,直到选举成功为止。
【替换主节点】
当从节点收集到足够的选票之后,触发替换主节点操作:
-
当前从节点取消复制变为主节点。
-
执行clusterDelSlot操作撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己。
-
向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息
4.3 故障转移时间
在介绍完故障发现和恢复的流程后,这时我们可以估算出故障转移时间:
-
主观下线(pfail)识别时间=cluster-node-timeout。
-
主观下线状态消息传播时间<=cluster-node-timeout/2。消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息,消息体在选择包含哪些节点时会优先选取下线状态节点,所以通常这段时间内能够收集到半数以上主节点的pfail报告从而完成故障发现。
-
从节点转移时间<=1000毫秒。由于存在延迟发起选举机制,偏移量最大的从节点会最多延迟1秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。
根据以上分析可以预估出故障转移时间:failover-time(毫秒) ≤ cluster-node-timeout + cluster-node-timeout/2 + 1000
5 请求路由
Redis集群对客户端通信协议做了比较大的修改,为了追求性能最大化,并没有采用代理的方式而是采用客户端直连节点的方式。
5.1 请求重定向
在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。
【两种客户端】
-
Dummy(傀儡)客户端:根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,它优点是代码实现简单,对客户端协议影响较小,只需要根据重定向信息再次发送请求即可。
-
Smart(智能)客户端:通过在内部维护slot→node的映射关系,本地就可实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot→node映射。
5.2 ASK重定向
Redis集群支持在线迁移槽(slot)和数据来完成水平伸缩,期间可能出现一部分数据在源节点另一部分在目标节点,当slot对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。
当出现上述情况时,客户端键命令执行流程将发生变化,如下所示:
-
客户端根据本地slots缓存发送命令到源节点,如果存在键对象则直接执行并返回结果给客户端。
-
如果键对象不存在,则可能存在于目标节点,这时源节点会回复ASK重定向异常。格式如下:(error)ASK{slot}{targetIP}:{targetPort}。
-
客户端从ASK重定向异常提取出目标节点信息,发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息。
【ASK 与 MOVED】
-
ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存。
-
MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。