Redis设计与实现3.3:集群
集群
这是《Redis设计与实现》系列的文章,系列导航:Redis设计与实现笔记
集群中的节点
创建集群
通过 CLUSTER NODE
命令可以查看当前集群中的节点。刚启动时,默认每一台节点都是一个集群。
participant r0 as redis.cn 7000
participant r1 as redis.cn 7001
participant r2 as redis.cn 7002
note over r1: CLUSTER MEET redis.cn 7000
r1 ->>+ r0: 握手
r0 ->>- r1: 响应握手
note over r1: CLUSTER MEET redis.cn 7002
r1 ->>+ r2: 握手
r2 ->>- r1: 响应握手
如上图所示,登录 7001,然后输出相应的指令,请求和7000、7002搭建一个集群。
subgraph 集群
7001
7002
7003
end
集群的数据结构
节点会继续使用所有在单机模式中使用的服务器组件,而那些只有在集群模式下才能用到的数据,节点将他们保存到了 cluster.h/clusterNode
、cluster.h/clusterLink
、cluster.h/clusterState
结构中,如下图所示。
前面两个结构主要记录的是其他节点的信息,而clusterState则是集群的信息。
这里有两个纪元:
- clusterNode 中有一个,称为节点纪元
- clusterState 中也有一个,称为集群纪元
这两个纪元分别用在哪里,有什么不同?
MEET命令的实现
上面的那张 MEET 的时序图非常的概括,比如:
- 怎么握手,怎么响应握手
- 接收方怎么开始与其他节点进行 MEET
这里在该图的基础上补充一些细节
participant r0 as redis.cn 7000
participant r1 as redis.cn 7001
note over r1: CLUSTER MEET redis.cn 7000
r1 ->> r1: 创建7000的clusterNode结构
r1 ->>+ r0: MEET
r0 ->> r0: 创建7001的clusterNode结构
r0 ->>- r1: PONG
note over r1: 我知道了你已经收到了
r1 ->> r0: PING
note over r0: 我知道你已经收到了
这里互相确认有一点三次握手的感觉
节点数据库的实现
集群节点保存键值对以及过期时间的方式与单机 Redis 服务器的方式完全相同。
但是一个区别是,节点只能使用0号数据库。
槽指派
利用集群可以实现分区的功能,从而减少单台服务器的业务量。那么集群的首要任务就是如何保证一致性。Redis 采用了槽指派的模式进行分区,类似于一致性哈希的做法。
Redis 集群将整个数据库分为 16384 个槽,数据库中每一个键都属于者16384个槽中的一个,集群中的每个节点可以处理0到16384个槽。
操作
只有所有槽都有节点在处理时,集群才处于上线状态,否则,处于下线状态。
可以用 CLUSTER INFO
查看集群状态(我们之前的集群就没有分配槽,所以是下线状态)。
可以用 CLUSTER ADDSLOTS xxx
命令进行槽的分配。
数据结构
clusterNode结构中:
- numslots:表示一个节点负责的槽数量
- slots:位数组,每一位记录了这个节点归不归我管,比如下面的表格,表示这个节点只负责 1 ~ 8 号槽
clusterState结构中:
clusterNode* slots[16384]
结构中记录了所有槽的指派信息:
- NULL 表示未被指派
- 否则指向该节点的结构
ADDSLOTS命令实现
先遍历一遍,看有没有已经分配过了的,如果有则直接失败。
否则,设置更新上述的两个结构。
伪码便于理解:
槽指派信息传播
一个节点除了会记录自己的上述信息外,还会将这个数组通过消息发送给集群中的其他节点。
其他节点收到后,进行保存或更新。
命令执行
执行流程
集群模式的一个重要的不同是,数据被分配在了不同的节点上,所以接收到请求的服务器未必能对该请求进行处理,因此多了一个寻找有能力处理的服务器的过程:
participant a as NodeA
participant c as Client
participant b as NodeB
c ->>+ a: GET name
a ->> a: 计算 CRC16(name) % 16384
a ->> a: clusterNode.slots 中该值是否是我负责
alt slots[i] = 0 是我的工作
a ->> c : name="张三"
else slots[i] = 0 不是我的工作
a ->> c : MOVE NodeB
c ->> b : GET name
note over b: 省略若干判断操作
b ->> c : name="张三"
end
MOVE指令
指令格式为:
MOVE <slot> <ip>:<port>
和 HTTP 请求的重定向有些类似
重新分片
数据结构
保存槽的分配情况:
clusterState.slots_to_keys
是一个跳表,用来保存槽和键之间的关系。跳表的分值是一个槽号,而节点的成员都是数据库键。每当节点往数据库中插入一个新的键值对时,节点就会将这个键以及键的槽号关联到这个跳表中。
这么做的目的是,方便我们找到某一个槽值对应的键,例如命令 CLUSTER GETKEYSINSLOT
。(话说如果要重新分配槽的话,不就有这个需求了!)
执行流程
由Redis的集群管理软件 redis-trib 负责执行,流程如下:
autonumber
loop 对于迁移的每个槽
participant s as source node
participant rt as redis-trib
participant t as target node
note over rt: 先通知你们俩都作好准备啊
rt ->> t: 你从<source_id>获得<slot>号槽
rt ->> s: 你把<slot>号槽给<source_id>
note over rt: 我要开始迁移了!
loop 只要还有值没有迁移
rt ->> s: 返回最多<count>个<slot>槽的键
rt ->> t: 这是键的信息,你保存一下
end
note over rt: 迁移完了,通知一下
end
图注:
CLUSTER SETSLOT <slot> IMPORTING <source_id>
CLUSTER SETSLOT <slot> MIGRATING <target_id>
CLUSTER GETKEYSINSLOT <slot> <count>
MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>
最后的通知步骤,书上说是:向集群中任意一个节点发送
CLUSTER SETSLOT <slot> NODE <target_id>
命令。我理解之为 Gossip 协议的方式。
IMPORTING 命令的实现
clusterState
结构的 importing_slots_from
数组记录了当前节点正在从其他节点导入的槽。如果 importing_slots_from[i]
的值不为 NULL,而是指向一个 clusterNode结构,则表明当前节点正在从 clusterNode 所代表的节点导入槽i。
MIGRATEING 命令的实现
类似地,clusterState
结构的 migrating_slots_to
数组记录了当前节点正在迁移至其他节点的槽。其情况和上述命令一样。
ASK错误
在迁移的过程中,难免会出现一种情况:某个槽值的键只迁移了一部分,有一部分还保存在原来的节点,而另一部分已经保存在目标节点了。
为了处理这种情况,我们需要一些机制来进行处理:
participant c as Client
participant s1 as Node1
participant s2 as Node2
note over s1,s2: Node1正在给Node2迁移槽
c ->>+ s1: GET name
s1 ->> s1: 计算对应槽值,发现该槽是我负责的
s1 ->> s1: 查找槽值,发现没找到,发现这个槽正在转移
s1 ->>- c: ASK Node2
c ->>+ c: 打开我的ASING标识
c ->>+ s2: ASKING
c ->> s2: GET name
s2 ->> s2: 这个槽不归我管,给它回个MOVE吧
s2 ->> s2: 不对,你是ASKING,我再找找
s2 ->>- c: 找到了, name="张三"
c ->>- c: 好滴,关闭我的ASKING标识
- 收到 ASK 后,会打开自己的 ASKING 标识
- 在发送请求前先发送一次 ASKING
- ASK 使用后就会关闭
复制与故障转移
可以给集群中的节点设置从节点,从而提高系统的容错性和高可用。
节点复制的方法
- 节点收到
CLUSTER REPLICATE <node_id>
,开始进行复制 - 修改
clusterState.myself.slaveof
指针,指向主节点 - 修改
clusterState.myself.flag
,关闭REDIS_NODE_MASTER
标识,打开REDIS_NODE_SLAVE
标识 - 进行复制,相当于执行
SLAVEOF <master_ip> <master_port>
- 将这一情况通过消息发送给集群中的所有节点
故障检测
这里的故障检测和 Sentinel 的故障检测是很相似的,如下:
- 集群的节点互相发送 PING,并接收 PONG
- 如果一定时间没有收到恢复,就标记为疑似下线状态(PFAIL)
- 集群中的节点会相互发消息交换状态信息
- 如果一个集群里半数以上负责处理槽的主节点都标记为下线,则其被标记为下线(FAIL)
- 广播下线消息
故障转移
当一个从节点发现自己的老大挂了,就要选举一个新的老大,并给老大安排后事。
- 选举一个节点
- 这个新的节点将执行
SLAVEOF no one
,成为新的老大 - 新的老大会把原来老大的槽全部指派给自己
- 新的老大会向别人(集群)广播一条 PONG 消息,宣告自己的地位
- 新的老大开始接收和处理命令请求,故障转移完成
那么谁来当新的老大呢,如何选举?基于Raft算法的领头选举
和之前 Sentinel 部分的处理情况非常的类似,这里我就不再次描述了,贴上官方文档,供大家学习:Redis cluster specification | Redis
消息
消息介绍
有五种消息:
- MEET:接收到客户端发送的
CLUSTER MEET
指令时发送,请求接收者加入当前的集群中 - PING:集群中的每个节点默认每隔一秒就会从已知节点列表中随机选出5个节点,然后对这五个节点中最长时间没有发过 PING 消息的节点发送 PING 消息,以此检测被选中的节点是否在线。除此之外,对超过
cluster-node-timeout
时间没有发送过节点的也会发送。 - PONG:一是对 MEET 或 PING 命令的响应;二是通过向集群广播 PONG 来刷新其他节点对自己的认识,例如故障转移后的主节点。
- FAIL:当一个主节点 A 判断另一个节点 B 已经进入 FAIL 状态时,会广播一条关于 B 的 FAIL 消息,所有收到这条消息的节点会将 B 标记为下线(前文提到过)
- PUBLISH:当一个节点接收到一个 PUBLISH 命令时,节点会执行这个命令,并向集群广播一条 PUBLISH 命令,所有接收到这条信息的节点会同样进行这个过程。
一条消息由消息头和消息正文组成。
消息头
每个消息头由一个 cluster.h/clusterMsg
结构表示:
typedef struct {
char sig[4]; /* Siganture "RCmb" (Redis Cluster message bus). */
// 消息的长度(包括这个消息头的长度和消息正文的长度)
uint32_t totlen; /* Total length of this message */
uint16_t ver; /* Protocol version, currently set to 0. */
uint16_t notused0; /* 2 bytes not used. */
// 消息的类型
uint16_t type;
// 消息正文包含的节点信息数量
// 只在发送 MEET 、 PING 和 PONG 这三种 Gossip 协议消息时使用
uint16_t count;
// 消息发送者的配置纪元
uint64_t currentEpoch;
// 如果消息发送者是一个主节点,那么这里记录的是消息发送者的配置纪元
// 如果消息发送者是一个从节点,那么这里记录的是消息发送者正在复制的主节点的配置纪元
uint64_t configEpoch;
// 节点的复制偏移量
uint64_t offset;
// 消息发送者的名字(ID)
char sender[REDIS_CLUSTER_NAMELEN];
// 消息发送者目前的槽指派信息
unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
// 如果消息发送者是一个从节点,那么这里记录的是消息发送者正在复制的主节点的名字
// 如果消息发送者是一个主节点,那么这里记录的是 REDIS_NODE_NULL_NAME
// (一个 40 字节长,值全为 0 的字节数组)
char slaveof[REDIS_CLUSTER_NAMELEN];
char notused1[32];
// 消息发送者的端口号
uint16_t port;
// 消息发送者的标识值
uint16_t flags;
// 消息发送者所处集群的状态
unsigned char state;
// 消息标志
unsigned char mflags[3];
// 消息的正文(或者说,内容)
union clusterMsgData data;
} clusterMsg;
这些属性记录了发送者自身的节点信息,接收者会根据这些信息,在 clusterState.nodes 字典中找到发送者对应的 clusterNode 结构,并对结构进行更新。
消息体
上文的最后一个属性 union clusterMsgData data
指向联合结构,这个结构就是消息的正文:
MEET、PING、PONG
Redis 集群中各个节点通过 Gossip 协议来交换各自关于不同节点的状态信息,其中 Gossip 协议由 MEET、PING、PONG三种消息实现,这三种消息的正文都由两个 cluster.h/clusterMsgDataGossip
结构组成:
注意到MEET、PING、PONG 都使用相同的消息正文,所以节点通过消息头的 type
属性来判断一条消息是 MEET 消息、PING 消息还是 PONG 消息。
每次发送 MEET、PING、PONG 三种消息时,发送者都从自己的已知节点列表中随机选择出两个节点(可以是主或从),并将这两个被选中的节点信息分别保存到两个 clusterMsgDataGossip
结构中:
typedef struct {
// 节点的名字
// 在刚开始的时候,节点的名字会是随机的
// 当 MEET 信息发送并得到回复之后,集群就会为节点设置正式的名字
char nodename[REDIS_CLUSTER_NAMELEN];
// 最后一次向该节点发送 PING 消息的时间戳
uint32_t ping_sent;
// 最后一次从该节点接收到 PONG 消息的时间戳
uint32_t pong_received;
// 节点的 IP 地址
char ip[REDIS_IP_STR_LEN]; /* IP address last time it was seen */
// 节点的端口号
uint16_t port; /* port last time it was seen */
// 节点的标识值
uint16_t flags;
// 对齐字节,不使用
uint32_t notused; /* for 64 bit alignment */
} clusterMsgDataGossip;
过程分为认识和不认识:
participant s as A
participant r as B
participant r3 as D
s ->> s: RandomGetTwoNode
s ->> r: PING(C, D)
r ->> r: 我认识C, 所以我更新他的信息
r ->> r3: 不认识D,进行握手
r ->> r: RandomGetTwoNode
r ->> s: PONG(E, F)
有一个疑惑:源码中 clusterMsgDataGossip 大小明明为 1,怎么保存两个节点的信息的。
备注:前面提到的 PING 每秒选五个节点进行发送,这里提到的是每次发送这三种信息时附带随机的两个节点的信息。
FAIL
FAIL 消息用来宣告某一个节点的失效,由于这个消息属于“八百里加急”,需要让所有节点立即知道。而当节点数量比较大的时候延迟较大,所以不适合使用 Gossip 协议。
cluster.h/clusterMsgDataFail
的结构比较简单,仅用名称标识进行唯一标识:
typedef struct {
// 下线节点的名字
char nodename[REDIS_CLUSTER_NAMELEN];
} clusterMsgDataFail;
PUBLISH
当客户端向集群中某个节点发送命令:
PUBLISH <channel> <message>
接收到 PUBLIHS 命令的节点不仅会向 channel 频道发送消息 message,还会向集群广播一条 PUBLISH 消息。而其他接收到消息的节点的也都会向 channel 频道发送 message 消息。
原书:为什么不直接向节点广播 PUBLISH 命令?
要让集群所有节点都执行相同的 PUBLISH 命令,最简单的方法就是向所有节点广播相同的 PUBLISH 命令,这也是 Redis 在复制 PUBLISH 命令时使用的方式,不过这种做法并不符合 Redis 集群的 “各个节点通过发送和接收消息来进行通信”这一规则,所以节点没有采取广播 PUBLISH 命令的方法。
消息的结构:
typedef struct {
// 频道名长度
uint32_t channel_len;
// 消息长度
uint32_t message_len;
// 消息内容,格式为 频道名+消息
// bulk_data[0:channel_len-1] 为频道名
// bulk_data[channel_len:channel_len+message_len-1] 为消息
unsigned char bulk_data[8]; /* defined as 8 just for alignment concerns. */
} clusterMsgDataPublish;
Redis设计与实现3.3:集群的更多相关文章
- 【笔记】《Redis设计与实现》chapter17 集群
17.1 节点 启动节点 Redis服务器启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式 节点会继续使用redisServer结构来保存服务器的状态,使用 ...
- Redis详解(七)——集群
Redis详解(七)--集群 Redis3.0版本之前,可以通过Redis Sentinel(哨兵)来实现高可用 ( HA ),从3.0版本之后,官方推出了Redis Cluster,它的主要用途是 ...
- Linux下redis 的部署、主从与集群
老男孩Python全栈6期——redis--------------------------Linux 操作系统 默认的内存管理机制RSS:page cache:anno page:Linux操作系统 ...
- redis 5.0.3 讲解、集群搭建
REDIS 一 .redis 介绍 不管你是从事Python.Java.Go.PHP.Ruby等等... Redis都应该是一个比较熟悉的中间件.而大部分经常写业务代码的程序员,实际工作中或许只用到了 ...
- 实现Redis Cluster并实现Python链接集群
目录 一.Redis Cluster简单介绍 二.背景 三.环境准备 3.1 主机环境 3.2 主机规划 四.部署Redis 4.1 安装Redis软件 4.2 编辑Redis配置文件 4.3 启动R ...
- Redis安装(单机及各类集群,阿里云)
Redis安装(单机及各类集群,阿里云) 前言 上周,我朋友突然悄悄咪咪地指着手机上的一篇博客说,这是你的博客吧.我看了一眼,是之前发布的<Rabbit安装(单机及集群,阿里云>.我朋友很 ...
- Redis学习笔记(九)——集群
一.概述 Redis Cluster与Redis3.0.0同时发布,以此结束了Redis无官方集群方案的时代. Redis Cluster是去中心化,去中间件,也就是说,集群中的每个节点都是平等的关 ...
- Redis——(主从复制、哨兵模式、集群)的部署及搭建
Redis--(主从复制.哨兵模式.集群)的部署及搭建 重点: 主从复制:主从复制是高可用redis的基础,主从复制主要实现了数据的多机备份,以及对于读操作的负载均衡和简单的故障恢复. 哨兵和集群都是 ...
- Redis系列5:深入分析Cluster 集群模式
Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) 1 背景 前面我们 ...
- Redis架构之哨兵机制与集群
Redis架构之哨兵机制与集群 哨兵机制 1.介绍: Sentinel(哨兵)是redis高可用性解决方案:由一个或多个由一个或多个Sentinel 实例 组成的Sentinel 系统可以监视任意多个 ...
随机推荐
- springboot-mail发邮件,不需要邮件服务器
很简单 步骤走起-> 1.需要一个邮箱账号,我以163邮箱为例,先开启第三方服务后获得密码,后面用来邮箱登录 2.加入mail 依赖 3.properties配置账号和第三方服务密码(不是邮箱密 ...
- 【动态规划】洛谷P1802 5 倍经验日(01背包问题)
一个洛谷普及-的题目,也是我刚刚入门学习动态规划的练习题. 下面发一下我的思路和代码题解: 我的思路及伪代码: 我的AC图: 接下来上代码: 1 //动态规划 洛谷P1802 五倍经验日 2 #inc ...
- scanf()函数的原理
最近使用scanf发现了自己对scanf函数还是不太了解,主要出现在无意中出现的一个错误: scanf正确的写法是,scanf中以什么格式输入变量,则变量的类型就应该是什么格式,如下面scanf输入到 ...
- 4.7 ROS分布式通信
4.7 ROS分布式通信 ROS是一个分布式计算环境.一个运行中的ROS系统可以包含分布在多台计算机上多个节点.根据系统的配置方式,任何节点可能随时需要与任何其他节点进行通信. 因此,ROS对网络配置 ...
- chrome插件one-read开发1:准备
阅读我的博客文章:chrome插件one-read开发:准备 前言 为啥要做这个,因为我原本想用chrome做一个书签管理的东西,但是很久没有碰过chrome的插件开发了.所以先做一个简单的,来熟悉下 ...
- github 上有趣又实用的前端项目(持续更新,欢迎补充)
github 上有趣又实用的前端项目(持续更新,欢迎补充) 1. reveal.js: 幻灯片展示框架 一个专门用来做 HTML 幻灯片的框架,支持 HTML 和 Markdown 语法. githu ...
- 理解Promise函数中的resolve和reject
看了promise的用法,一直不明白里面的resolve和reject的用法: 运行了这两段代码之后彻底理解了promise的用法: var p = new Promise(function (res ...
- 面试 | Java 算法的 ACM 模式
(Java 算法的 ACM 模式) 前言 经常在 LeetCode 上用核心代码模式刷题的小伙伴突然用 ACM 模式可能会适应不过来,把时间花在输入输出上很浪费时间,因此本篇笔记对 Java 算法的 ...
- 帝国CMS内容页模板过滤清理简介smalltext前后空格的方法!
在内容模板你需要调用的地方使用如下代码输出简介即可过滤简介smalltext前后的空格了: <? $qian=array(" "," ","\t ...
- ORM中聚合函数、分组查询、Django开启事务、ORM中常用字段及参数、数据库查询优化
聚合函数 名称 作用 Max() 最大值 Min() 最小值 Sum() 求和 Count() 计数 Avg() 平均值 关键字: aggregate 聚合查询通常都是配合分组一起使用的 关于数据库的 ...