Redis Cluster 集群伸缩原理源码剖析
1. Redis 集群伸缩教程
Redis
提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以对下线节点进行缩容。
如何进行Redis Cluster
的伸缩,请参考 Redis Cluster 集群扩容与收缩 本篇教程。本文详细分别使用手动命令和redis-trib.rb
工具来执行Redis
集群的扩容和收缩操作。
本篇文章根据Redis
源码深入剖析集群伸缩的原理。Redis Cluster文件详细注释
2. 集群扩容原理剖析
集群扩容的步骤如下:
- 准备新节点
- 加入集群
- 迁移槽和数据
我们根据步骤一步一步分析。
2.1 准备新节点
本步骤就是准备新的节点和配置启动文件。具体参考 Redis Cluster 载入配置文件、节点握手、分配槽源码剖析 一文的载入配置一部分,该部分从Redis
服务器的main
函数开始分析,一直到服务器启动成功。
2.2 加入集群
将新节点加入集群,就是发送CLUSTER MEET
命令,将准备的新节点加入到已搭建好的集群,让所有的集群中的节点“认识”新的节点。
可以参考 Redis Cluster 载入配置文件、节点握手、分配槽源码剖析 一文中的节点握手部分,该部分根据源码分析了节点握手的三过程:发送MEET
消息,回复PONG
消息,发送PING
消息,最后讲解了Gossip
协议在Redis
中是如何使用的。
2.3 迁移槽和数据
集群扩容的前两步和搭建集群很像,最后一步则是将集群节点中的槽和数据迁移到新的节点中,而不是为新的节点分配槽位。因此我们重点分析这一过程。Redis Cluster文件详细注释
将源节点的槽位和数据迁移到目标节点中,迁移单个槽步骤如下:
- 对目标节点发送
CLUSTER SETSLOT <slot> importing <source_name>
,在目标节点中将<slot>
设置为导入状态(importing)。 - 对源节点发送
CLUSTER SETSLOT <slot> migrating <target_name>
,在源节点中将<slot>
设置为导出状态(migrating)。 - 对源节点发送
CLUSTER GETKEYSINSLOT <slot> <count>
命令,获取<count>
个属于<slot>
的键,这些键要发送给目标节点。 - 对于第三步获得的每个键,发送
MIGRATE host port "" dbid timeout [COPY | REPLACE] KEYS key1 key2 ... keyN
命令,将选中的键从源节点迁移到目标节点。
- 在
Redis 3.0.7
以后的版本支持了MIGRATE
命令的批量迁移操作。 - 如果不支持批量迁移,那么会发送
MIGRATE host port key dbid timeout [COPY | REPLACE]
命令将一个键从源节点迁移到目标节点。 - 当一次无法迁移完成时,会循环执行第三步和第四步,直到
<count>
个键全部迁移完成。
- 在
- 向集群中的任意节点发送
CLUSTER SETSLOT <slot> node <target_name>
,将<slot>
指派给目标节点。指派信息会通过消息发送到整个集群中,然后最终所有的节点都会知道<slot>
已经指派给了目标节点。
我们就根据这些步骤逐步分析:
2.3.1 目标节点中,将槽设置为导入状态
客户端连接上目标节点,并发送该命令给目标节点服务器,目标节点会调用clusterCommand()
函数来执行该命令,该函数是一个通用函数,能用于执行CLUSTER
开头的所有命令。该函数会判断SETSLOT
选项,但是SETSLOT
选项对应4种不同的函数分别是:
SETSLOT 10 MIGRATING <node ID> //设置10号槽处于MIGRATING状态,迁移到<node ID>指定的节点
SETSLOT 10 IMPORTING <node ID> //设置10号槽处于IMPORTING状态,将<node ID>指定的节点的槽导入到myself中
SETSLOT 10 STABLE //取消10号槽的MIGRATING/IMPORTING状态
SETSLOT 10 NODE <node ID> //将10号槽绑定到NODE节点上
在SETSLOT
选项中先会判断myself
节点是否为主节点,如果是从节点则直接返回,然后获取指定的槽号。
int slot;
clusterNode *n;
// 如果myself节点是从节点,回复错误信息
if (nodeIsSlave(myself)) {
addReplyError(c,"Please use SETSLOT only with masters.");
return;
}
// 获取槽号
if ((slot = getSlotOrReply(c,c->argv[2])) == -1) return;
本小节主要看CLUSTER SETSLOT <slot> importing <source_name>
命令,在目标节点中,将槽设置为导入状态,处理importing
状态的代码如下:
if (!strcasecmp(c->argv[3]->ptr,"importing") && c->argc == 5) {
// 如果该槽已经是myself节点负责,那么不进行导入
if (server.cluster->slots[slot] == myself) {
addReplyErrorFormat(c,"I'm already the owner of hash slot %u",slot);
return;
}
// 获取导入的目标节点
if ((n = clusterLookupNode(c->argv[4]->ptr)) == NULL) {
addReplyErrorFormat(c,"I don't know about node %s",(char*)c->argv[3]->ptr);
return;
}
// 为该槽设置导入目标
server.cluster->importing_slots_from[slot] = n;
// 更新集群状态和保存配置
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_UPDATE_STATE);
addReply(c,shared.ok);
先判断该槽位是否已经是目标节点所负责的,如果是则不需要进行导入,否则继续调用clusterLookupNode()
函数,根据<source_name>
在当前集群中查找源节点,然后将服务器集群状态的importing_slots_from
对应的槽和导入的源节点做映射。当前执行该命令的节点是目标节点,因此在目标节点视角中的集群,该槽已经处于导入状态。
如果执行成功,则在进入下个周期之前更新集群状态和保存配置。
最后返回客户端一个OK
。
2.3.2 源节点中,将槽设置为导出状态
对源节点发送CLUSTER SETSLOT <slot> migrating <target_name>
,将槽设置为导出状态,对应的代码如下:
if (!strcasecmp(c->argv[3]->ptr,"migrating") && c->argc == 5) {
// 如果该槽不是myself主节点负责,那么就不能进行迁移
if (server.cluster->slots[slot] != myself) {
addReplyErrorFormat(c,"I'm not the owner of hash slot %u",slot);
return;
}
// 获取迁移的目标节点
if ((n = clusterLookupNode(c->argv[4]->ptr)) == NULL) {
addReplyErrorFormat(c,"I don't know about node %s",(char*)c->argv[4]->ptr);
return;
}
// 为该槽设置迁移的目标
server.cluster->migrating_slots_to[slot] = n;
// 更新集群状态和保存配置
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_UPDATE_STATE);
addReply(c,shared.ok);
在源节点视角的集群中,执行该命令。先会对该槽位的所有权进行判断,如果不属于源节点,那么就无权进行迁移。然后获取要迁移到的目标节点,调用clusterLookupNode()
函数查找目标节点。最后将服务器集群状态中的migrating_slots_to
对应的槽和导出的目标节点做映射关系。
如果执行成功,则在进入下个周期之前更新集群状态和保存配置。
最后返回客户端一个OK
。
2.3.3 获取迁移槽中的键
这一步要获取迁移槽中的所有键,这些键是要从源节点发送到目标节点。因此对应的选项是getkeysinslot
,代码如下:
if (!strcasecmp(c->argv[1]->ptr,"getkeysinslot") && c->argc == 4) {
/* CLUSTER GETKEYSINSLOT <slot> <count> */
long long maxkeys, slot;
unsigned int numkeys, j;
robj **keys;
// 获取槽号
if (getLongLongFromObjectOrReply(c,c->argv[2],&slot,NULL) != C_OK)
return;
// 获取打印键的个数
if (getLongLongFromObjectOrReply(c,c->argv[3],&maxkeys,NULL)
!= C_OK)
return;
// 判断槽号和个数是否非法
if (slot < 0 || slot >= CLUSTER_SLOTS || maxkeys < 0) {
addReplyError(c,