无论是单机还是主从结构,都无法解决数据量过大导致内存/虚拟内存无法村下所有数据的问题。redis集群通过分片根据key计算出数据所属的槽(slot),通过指派使每个集群节点负责一部分槽内数据的处理,从而成为一种分布式数据库。
redis集群一共有16384个槽,集群内每个节点可以负责处理其中一部分槽。下图表示集群内有3个节点,分别在端口7000,7001,7002。其中0-5000号槽被指派给7000端口所在的节点,...

1.节点
1.1 构建集群
redis集群的节点在初始时都是互相独立的,要组建集群只需在某台主机上使用CLUSTER MEET <ip> <port>
命令,当前主机就会与目的主机进行连接,连接成功后就会将对方添加到自己所在的集群内。
为了使7000-7002端口所在的3个服务主机构成一个[图片上传中...(image.png-891ba7-1632229388314-0)]
集群,只需要在7000端口的客户端分别输入
CLUSTER MEET <127.0.0.1> <7001>
CLUSTER MEET <127.0.0.1> <7002>
即可。

1.2 集群对应的数据结构
集群内每一个主机都对应一个clusterNode类型的数据。
typedef struct clusterNode {
mstime_t ctime; /* Node object creation time. */
char name[CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
int flags; /* CLUSTER_NODE_... */
...
unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
int numslots; /* Number of slots handled by this node */
int numslaves; /* Number of slave nodes, if this is a master */
struct clusterNode **slaves; /* pointers to slave nodes */
struct clusterNode *slaveof; /* pointer to the master node. Note that it
may be NULL even if the node is a slave
if we don't have the master node in our
tables. */
...
char ip[NET_IP_STR_LEN]; /* Latest known IP address of this node */
int port; /* Latest known clients port of this node */
int cport; /* Latest known cluster port of this node. */
clusterLink *link; /* TCP/IP link with this node */
...
} clusterNode;
clusterNode中比较关键的几个属性分别是: slots数组,长度为16384/8字节(16384 = 2^^14位,计算key应位于哪个slot时取余数方便),以一个bitmap的形式表示了当前节点处理的slot; 一个指向slave数组的指针(如果当前主机为master);一个指向master主机的指针(如果当前为slave);另外在clusterLink中包含了当前节点的tcp/ip连接信息,如文件描述符,输入、输出缓冲区等。
整个集群的信息由clusterState表示。 在每个节点中都会保存一个clusterState结构,表示在当前主机的视角下集群所处的状态。
typedef struct clusterState {
// 指向表示当前节点自身的clusterNode
clusterNode *myself; /* This node */
uint64_t currentEpoch;
// 集群当前状态:在线 || 下线
int state;
// 集群中有多少节点至少处理一个slot
int size;
// 集群中所有节点的字典,以name : clusterNode形式存储
dict *nodes; /* Hash table of name -> clusterNode structures */
...
clusterNode *slots[CLUSTER_SLOTS];
...
} clusterState;
在上述3个节点创建完集群后的初始状态下,每个节点都没有被分配任何负责处理的槽,在7000节点视角下状态如下图

1.3 槽指派
通过在某个节点的客户端发送CLUSTER ADDSLOTS <slot> [slot ...]
命令,可以将命令中的slot分派给当前节点负责处理。分派主要需要完成这样几件事情:
- 记录节点的槽指派信息。 即将节点对应的clusterNode内的slots数组相应位置为1, 同时使clusterState中的slots数组的对应位存放的指针指向当前clusterNode。
- 传播节点的槽指派信息。当前节点不仅要自己记录一份指派信息,还需要将指派信息发送给其他节点。其他节点在收到后,修改自己的clusterState,将slots数组的对应位指向被分派的节点,同时修改该节点的slots数组。
在所有节点指派完成之后,每个节点内clusterState的slots数组状态大致如下图。同时,各个clusterNode内,slots数组中被该节点负责的槽被置位为1.

此时,想要知道某个slot被哪个节点负责处理,只需要通过clusterState的slots数组对应位的指针,即可访问到该节点的信息。而如果想要知道当前节点所负责的slot信息(例如发送当前节点负责处理的slot信息给其他节点),只需要取出myself指向的clusterNode,拿到它的slots数组,取出被置位1的位就行了。
2. 执行命令
clusterState.slots可以快速定位到slot被哪个节点负责处理,因此在集群中执行命令也就相对容易了:客户端连接集群中任一节点,向其发送对key的处理命令。收到命令的节点现根据key计算出该key应该配分配到哪个slot上,比如slot = CRC16(key)%16834
根据clusterState.slots[slot]就能快速定位到对应的节点N。然后将节点N与myself指向的节点对比,如果是同一个,那么就可以直接处理。如果不是,那么说明应当由其他节点处理。此时向客户端返回MOVED错误,根据节点N的信息,指引客户端去请求对应的主机。
其流程如下图:

3.重新分片
重新分片可以将已经指派给某节点的任意数量的槽,重新分派给另外一个节点。重新分派后,槽和槽所属的键值对会被从源节点移动到目标节点。重新分片可以在线进行,并且再重新分片过程中,源节点和目标节点都可以继续处理命令请求。
重新分片由redis-trib负责执行,通过向源节点和目标节点发送命令来完成该操作。
对每个槽:
- 向目标节点发送
CLUSTER SETSLOT <slot> IMPORTING <source_id>
命令,让其准备好从源节点导入属于slot的键值对数据 - 向源节点发送
CLUSTER SETSLOT <slot> MIGRATING <target_id>
命令,让其准备好将slot内的键值对迁移到target - 向源节点发送
CLUSTER GETKEYSINSLOT <slot> <count>
命令,获得最多count个key name - 对于每个key_name, redis-trib向源节点发送
MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>
,将key原子地迁移至目标节点 - 重复3和4,直到slot内所有的key都被迁移完成。
-
向集群内任一节点发送``CLUSTER SETSLOT <slot> NODE <target_id>```,将slot指派给target,然后通过消息发送到整个集群,每个节点更新自己的slotState。
迁移slot内的键
4.复制与故障转移
1.集群设置从节点
向某个服务器发送 CLUSTER REPLICATE <node_id>
命令,使之复制node_id对应的节点,成为其从节点。收到命令的服务器需要做以下几件事:
- 在自己的clusterState.nodes中找到node_id对应的clusterNode,将自己的slaveof指向该node。
- 修改自身flag, 由master 变为slave
3.调用复制代码,根据master的ip:port发起复制请求。(这一过程和之前讲的主从复制相同,类似于SLAVEOF <master_ip> <master_port>
) - 通知集群内其他节点,这些节点收到通知后修改自己的clusterState
2.故障检测
- 集群内每个节点定期向集群内其他节点发送PING消息,如果某节点回复超时,那么发送的节点将其标记为疑似下线。(修改clusterState.nodes中对于clusterNode的标志)
- 各节点间通过消息交换集群中各节点的状态信息。
- 当某节点A得知B认为C疑似下线时,向C对应的clusterNode.fail_reports添加该报告。
如果集群内半数以上负责处理槽的主节点都向节点A报告C为疑似下线,那么X就被A标记为已下线。A将C已下线的消息广播到集群内,所有收到消息的节点都将该节点标记为已下线。
3.故障转移
当已下线节点C的从节点C1收到C已下线时,开始进行故障转移。
- 选举新的主节点。 (基于raft领导选举算法)
1.1 某个从节点开始故障转移时,先将自己内部clusterState.epoch加1,然后发起投票
1.2 负责处理槽的主节点具有投票权,每个投票者根据发过来的epoch和自己当前的epoch对比,如果当前epoch小于请求的epoch,则投票给他,并更新自己的epoch。
1.3 胜出规则: 得票数不小于 N/2 +1 则当选为新的主节点。
1.4 如果没人胜出,则进行下一轮投票 - 将已下线主节点负责处理的槽指派给自己。
- 将自己成为主节点的消息广播给集群内的其他节点。
- 转移完成,C1开始处理落在原来由C负责的槽内的请求。
网友评论