Redis的设计与实现-集群模式

166 阅读13分钟

集群模式

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,对数据进行分片,也就是说每台 Redis 节点上存储不同的内容;

image.png 这里的6台redis两两之间并不是独立的,每个节点都会通过集群总线(cluster bus),与其他的节点进行通信。通讯时使用特殊的端口号,即对外服务端口号加10000。例如如果某个node的端口号是6379,那么它与其它nodes通信的端口号是16379。nodes之间的通信采用特殊的二进制协议。

对客户端来说,整个cluster被看做是一个整体,客户端可以连接任意一个node进行操作,就像操作单一Redis实例一样,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node,这有点儿像浏览器页面的302 redirect跳转。

根据官方推荐,集群部署至少要 3 台以上的master节点,最好使用 3 主 3 从六个节点的模式。测试时,也可以在一台机器上部署这六个实例,通过端口区分出来。

集群由多个节点(redis服务器)组成。多个节点之间通过握手进行连接,从而构造了集群。

CLUSTER MEET <ip> <port>

客户端向一个节点发送这条命令,可以使得接收命令的节点与命令中对应ip地址和端口的服务器节点进行握手,使得其成为集群中的一份子。

节点

启动节点

每个节点都是一个redis服务器,但除了具有redis服务器的功能与结构以外还有其他的特征,在cluster.h中分别有clusterNode,clusterLink,clusterState结构用于保存信息。

集群数据结构(clusterNode,clusterLink,clusterState)

clusterNode用于保存节点自身的信息;其中的link属性就是一个clusterLink结构,用于保存和其他及诶按进行交流所需的资源,包括连接的创建时间,套接字描述符,输入输出缓冲区,与其相连接的节点。

:clusterLink结构类似redisClient结构,但clusterLink用于连接节点,redisClient结构用于连接客户端。

在每个节点(clusterNode)中都保存一个clusterState结构用于保存当前集群的信息,包括集群状态,集群中其他节点的信息(字典),集群的配置纪元等

CLUSTER MEET命令的实现 原理类似tcp中的三次握手

  • 媒人牵线:客户端(媒人)向节点A(男主)发送命令CLUSTER MEET ,里面包含了节点B(女主)的信息。
  • 主动认识:节点A通过ip和port(女主的联系方式)给节点B发送MEET(约会)消息
  • 两情相悦:节点B收到节点A的MEET命令后,给节点A回复PONG(收到,ok)命令
  • 牵手成功:节点A收到节点B的PONG之后,了解了B的心意,最后给节点B回复PING命令。ojbk,牵手成功。

槽指派

集群用分片的方式保存数据库中的键值对,数据库被分为16384个槽,每个键都是属于一个槽,每个节点复制其中任意多个槽。

只有所有槽都有对应节点负责的时候,集群才属于上线状态。

CLUSTER ADDSLOTS 命令可以给节点分配复制的槽。

记录节点的槽指派信息

每个节点(clusterNode)结构中的slots属性,是一个大小为16384/8大小的无符号char数组,使用位图法,每位对应一个槽,记录对应的槽是不是自己负责(位为1表示这个槽由自己负责)。而numslots属性记录负责的槽的数量。

传播节点的槽指派信息

每个节点会将自己的slots数组通过发消息发送给集群中的其他节点,告诉其他节点自己负责那些槽,方便其他节点同步更新自己clusterState结构中对应到这个节点的信息。

记录集群所有槽的指派信息

clusterState结构中有一个属性是slots,是一个大小为16384大小的clusterNode指针数组,用下标对应每个槽,他们都指向对应负责这个槽的节点。

通过这个节点可以在O(1)复杂度先找到对应槽的负责节点,如果没有这个结构,靠每个节点的slots数组(位图)也能找到,但复杂度为最坏O(n),n为节点数量。

而每个节点的slots数组用于发送给其他节点信息时使用,如果发送clusterState结构的slots数组,需要遍历筛选。

CLUSTER ADDSLOTS命令的实现

  • 先检查命令中的所有槽是否都未被指派,若存在已被指派的,返回空

  • 若都为被指派,则遍历每个槽对应更新clusterNode.slots和clusterState.slots。

在集群中执行命令

  • 先检查发送的命令对应的键是不是自己负责的槽点(有一个跳跃表存在键对应的槽)

  • 如果是,直接执行命令

  • 如果不是,返回MOVED错误,并指引其到正确的节点(查clusterState.slots)

计算键属于哪个槽

CLUSTER KEYSLOT <key>

实现是利用键的校验和与整数16383进行与运算(CRC16(key)&16383)

判断槽是否由当前节点负责处理

根据clusterState.slots数组,对应这个槽指向的节点,是当前节点就直接执行命令,不是就根据指针,引导到正确的节点(换个套接字进行工作)。

:上述MOVED错误在集群状态下并不会打印到客户端,起引导到正确节点的作用,但在单机模式,服务器无法识别这个符号,会打印错误到客户端。

节点数据库的实现

用跳跃表来储存键和槽号的关系,跳跃表节点的分值为槽号,跳跃表节点的成员是键,即利用槽号进行了排序,又建立了槽号与键的关系。

重新分片

集群的重新分片操作是将任意多个已经指派给某个节点的槽改为指派到另一个节点(槽的所有键值对也会转移)。

重新分片过程中(键值对转移过程中),集群不需要下线,转移中的键和槽仍然可以被命令控制。

实现原理:转移方向:源节点—>目标节点

  • 对目标节点发送CLUSTER SETSLOT IMPORTING <source_id>命令,告诉目标节点,你将从源节点导入那个槽的键值对。

  • 给源节点发送CLUSTER SETSLOT MIGRATE <target_id>命令,告诉源节点,你要吧那个槽的键值对发到那个节点。

  • 发送CLUSTER SETSLOT 命令,获取最多属于slot槽的count个键值对的键名

  • 对应每个上一步获得的键名,都向源节点发送一个MIGRATE <target_id> <target_port> <key_name> 0 命令,将被选中的键值对签约到目标节点。

  • 重复上两步,将该槽中所有节点都转移。

  • 向集群中任意一个节点发送CLUSTER SETSLOT NODE <target_id>,把槽指派给目标节点,最后指派信息会传播到整个集群中。

ASK错误

CLUSTER SETSLOT IMPORTING命令的实现

当前节点从其他节点导入时,在本身节点对应的clusterState结构中的import_slots_from数组,属性是clusterNode的指针,大小是16384,若不为空,表示指向的这个节点正在导入对应下标的槽,为空表示,此槽未在导入过程中。

CLUSTER SETSLOT MIGRATING命令的实现

当前节点正在迁移槽到其他节点时,在本身节点对应的clusterState结构中的migrating_slots_from数组,属性是clusterNode的指针,大小是16384,若不为空,表示正在将对应下标的槽导出到指向的这个节点,为空表示,此槽未在导出过程中。

ASK错误

迁移过程中在槽没被迁移走之前,发送与数据库键有关的操作,但键被迁走了,发送ASK错误,并引导到正确的节点。在引导到目标节点后执行对应命令之前会先执行一次ASKING命令。

ASKING命令

ASKING命令的作用是打开发送命令的客户端的REDIS_ASKING标识。

在节点执行命令时,对应的键如果分配过来了,但对应的槽没分配过来的话,节点不会执行这个命令,除非对应的客户端打开了REDIS_ASKING标识。

REDIS_ASKING标识是一次性的,打开后,并在执行后,这个标识会被关闭。

ASK和错误MOVED错误的区别:同样是找不到负责的数据,然后转向正确的节点进行处理,但MOVEN错误,是正常错误,直接转向,直接执行。而ASK错误是临时性的,在没打开ASKING标识的情况下,不会执行。

复制和故障转移

设置从节点

客户端利用命令CluSTER REPLICATE <node_id>将接受命令的节点设置为对应参数的从节点。设置原理类似之前的复制。

故障检测

集群中每个主节点会不定期给其他主节点发送PING命令,在规定时间内未收到PONG命令则将这个节点标记为疑似下线。

并在自己的clusterState结构中找到这个节点的clusterNode结构将下线报告存到clusterNode.fail_reports属性中,fail_reports是一个链表。

集群中的节点会互相发消息报告各个节点的状态信息。若半数以上的主节点都将某个主节点标记为疑似下线,则这个主节点进入已下线状态,并在集群状态中发布消息告诉所有节点这个主节点已经下线。

故障转移

若主节点为已下线状态,就会从其从节点中选择一个从节点执行SLAVEOF no one成为主节点,撤销下线主节点负责的槽指派,并将这些槽全部指派给自己,然后发送一条PONG消息到广播中,表示自己成为新的主节点。

选举新的主节点

当从节点收到消息,自己复制的主节点下线后,会发消息到集群广播中给自己拉票,其他主节点具有投票权,这些主节点会投票给它收到消息的第一个从节点,每次选举都会把配置纪元加一,有一个从节点得票过半就当选,没有从节点得票过半就从新再投一次票,配置纪元又需要加一。

消息

有以下种类的消息:

  • meet消息:在客户端给节点发送CLUSTER MEET命令时,节点会给命令中提供参数所指定的节点发送meet消息。

  • ping消息:节点每隔一秒钟就会随机选择五个节点,并向其中最长没发送消息的节点发送ping信息。在节点间无ping交流达到cluster-node-timeout选项设置的时长的一半时也会发送ping消息,用于更新其他节点对本身节点的认知。

  • pong消息:用于作为meet消息和ping消息的回复。节点也会向集群广播发送消息用于刷新其他节点对自己的认知。

  • fall消息:用于向集群广播宣布某个节点已经下线

  • publish消息:向频道发布消息 每个消息分为消息头和消息正文。

消息头

消息头包裹着消息的正文,消息头记录了

消息的信息(长度,类型,消息正文的节点信息数量); 发送者的信息(配置纪元,名字,槽指派信息,集群信息,标识值,端口号,主从复制信息) 消息正文(使用联合体,不同类型的信息用同一个数据结构) 记录发送者的各种详细信息用于让接收者判断发送者的状态和角色是否发生了变化。

meet,ping,pong消息的实现

这三种消息都是用同样的消息正文,类型是使用消息正文来判断,并且从已知节点中随机选择两个保存到clusterMsgDataGossip结构中,这个结构记录了节点的信息(ip与端口号,标识符,最后一场发送ping消息和接收pong消息的时间戳),用于接收消息的节点更新对这两个节点的认识(不认识就进行握手)

fall消息的实现

散播节点下线消息,需要散播给所有节点,使用meet,ping,pong消息的实现在时间上不够高效。 散播下线消息的节点,会在集群广播中散播,散播消息的结构中只有下线节点的名字,收到的节点都会将这个名字对应的节点标记为下线

publish消息的实现

publish <channel> <message>

节点收到客户端发来这个消息的时候,不仅会向channel频道发送消息message,同时会把这条消息逐个散播到其他节点,这样其他节点也都会想channel频道发送消息messsage。

消息的结构为两个整数和一个字节数组,两个整数分别指明频道和消息这个两个参数在字节数组中的位置。

总结

  1. 节点通过握手来将其他节点添加到自己所处的集群当中

  2. 集群中的 16384 个槽可以分别指派给集群中的各个节点, 每个节点都会记录哪些槽指派给了自己, 而哪些槽又被指派给了其他节点。

  3. 节点在接到一个命令请求时, 会先检查这个命令请求要处理的键所在的槽是否由自己负责, 如果不是的话, 节点将向客户端返回一个MOVED 错误, MOVED 错误携带的信息可以指引客户端转向至正在负责相关槽的节点。

  4. 对 Redis 集群的重新分片工作是由客户端执行的, 重新分片的关键是将属于某个槽的所有键值对从一个节点转移至另一个节点。

  5. 如果节点 A 正在迁移槽 i 至节点 B , 那么当节点 A 没能在自己的数据库中找到命令指定的数据库键时, 节点 A 会向客户端返回一个 ASK 错误, 指引客户端到节点 B 继续查找指定的数据库键。

  6. MOVED 错误表示槽的负责权已经从一个节点转移到了另一个节点, 而 ASK 错误只是两个节点在迁移槽的过程中使用的一种临时措施。

  7. 集群里的从节点用于复制主节点, 并在主节点下线时, 代替主节点继续处理命令请求。

  8. 集群中的节点通过发送和接收消息来进行通讯, 常见的消息包括 MEET 、 PING 、 PONG 、 PUBLISH 、 FAIL 五种。