Redis系列文章
原理篇
源码篇
- 【Redis源码分析之基础流程】
- 【Redis源码分析之持久化】
- 【Redis源码分析之主从复制】
- 【Redis源码分析之哨兵】
- 【Redis源码分析之集群故障转移】
- 【Redis源码分析之集群Meet命令和请求路由】
问题分析
Redis源码分析之集群Meet命令和请求路由
为了能够保存更多的数据,会采用横向扩展的方式,实现切换集群。从Redis3.0开始,官方提供了一个Redis Cluster的方案,用于实现切片集群。本文主要从源码上,Meet命令和请求路由如何处理。
1. Cluster Meet命令分析
一个新启动的节点B,想要加入到一个已有的cluster中,选择cluster 中的节点A,在A上执行meet B。就可以把B加入到集群中。
1.2 A添加新节点B
A节点在clusterCommand函数中,接收到CLUSTER MEET命令,内部主要调用clusterStartHandshake方法。
c
//cluster.c#clusterCommand
else if (!strcasecmp(c->argv[1]->ptr,"meet") && (c->argc == 4 || c->argc == 5)) {
/* CLUSTER MEET <ip> <port> [cport] */
long long port, cport;
...
if (clusterStartHandshake(c->argv[2]->ptr,port,cport) == 0 &&
errno == EINVAL)
{
addReplyErrorFormat(c,"Invalid node address specified: %s:%s",
(char*)c->argv[2]->ptr, (char*)c->argv[3]->ptr);
} else {
addReply(c,shared.ok);
}
}
在clusterStartHandshake方法中,会对ip和port合理性的检查(如端口范围)。然后会判断ip:port对应的node,是否正处于CLUSTER_NODE_HANDSHAKE状态,如果是的话,就说明重复执行了cluster meet 节点B,没必要重复操作。
校验完后,就会调用createClusterNode方法进行添加节点。
c
//cluster.c#clusterStartHandshake
int clusterStartHandshake(char *ip, int port, int cport) {
clusterNode *n;
.....
//检查节点norm_ip:port 是否正在握手
if (clusterHandshakeInProgress(norm_ip,port,cport)) {
errno = EAGAIN;
return 0;
}
//创建一个含随机名字的node,后面会在handshake过程中修复
n = createClusterNode(NULL,CLUSTER_NODE_HANDSHAKE|CLUSTER_NODE_MEET);
memcpy(n->ip,norm_ip,sizeof(n->ip));
n->port = port;
n->cport = cport;
clusterAddNode(n);
return 1;
}
在createClusterNode方法中,根据刚刚的传参,nodename为null,则会随机创建40字节字符串作为名字。然后flags信息为CLUSTER_NODE_HANDSHAKE|CLUSTER_NODE_MEET。此时A只知道B的ip和port,其他信息都不知道。添加节点信息成功后,就返回客户端了。
c
//cluster.c#createClusterNode
clusterNode *createClusterNode(char *nodename, int flags) {
clusterNode *node = zmalloc(sizeof(*node));
if (nodename)
memcpy(node->name, nodename, CLUSTER_NAMELEN);
else
getRandomHexChars(node->name, CLUSTER_NAMELEN);
node->ctime = mstime();
node->flags = flags;
listSetFreeMethod(node->fail_reports,zfree);
...
}
1.2 A发送meet节点B
在上面的过程中,仅添加了节点信息,A并没有和B建立链接。连接的建立会在周期函数cluserCron被检查到,进而触发meet msg。
在clusterCron中会遍历所有的node信息,如果发现没有建立连接,则会进行连接的建立。然后把该连接设置到node中,然后注册clusterReadHandler回调函数,用于接收节点B回复的数据。同时会向节点B发送meet消息。最后,会消除节点CLUSTER_NODE_MEET的状态。
c
//cluster.c#clusterCron
if (node->link == NULL) {
int fd;
mstime_t old_ping_sent;
clusterLink *link;
//创建连接
fd = anetTcpNonBlockBindConnect(server.neterr, node->ip,
node->cport, NET_FIRST_BIND_ADDR);
link = createClusterLink(node);
link->fd = fd;
node->link = link;
//注册事件,回调函数为clusterReadHandler
aeCreateFileEvent(server.el,link->fd,AE_READABLE,
clusterReadHandler,link);
...
//发送meet msg消息
clusterSendPing(link, node->flags & CLUSTER_NODE_MEET ?
CLUSTERMSG_TYPE_MEET : CLUSTERMSG_TYPE_PING);
...
node->flags &= ~CLUSTER_NODE_MEET;
serverLog(LL_DEBUG,"Connecting with Node %.40s at %s:%d",
node->name, node->ip, node->cport);
}
1.3 B处理A的MEET消息
在Redis实例启动时,会执行clusterInit方法,内部对监听端口。监听的端口默认为:配置的端口 + 10000。然后会注册事件,其中回调函数为clusterAcceptHandler。节点A给节点发送的消息,就走到clusterAcceptHandler方法里面,会走到clusterReadHandler方法中,最终会在clusterProcessPacket方法里执行。clusterProcessPacket方法内部处理了所有的消息的各种情况。
c
//cluster.c#clusterInit
if (listenToPort(server.port+CLUSTER_PORT_INCR,
server.cfd,&server.cfd_count) == C_ERR) {
exit(1);
} else {
int j;
for (j = 0; j < server.cfd_count; j++) {
if (aeCreateFileEvent(server.el, server.cfd[j], AE_READABLE,
clusterAcceptHandler, NULL) == AE_ERR)
serverPanic("Unrecoverable error creating Redis Cluster "
"file event.");
}
}
此时B还不认识A,因此B从本地的node信息上找不到A,所以sender是空的,所以同样会创建一个随机名字的node节点,flag为CLUSTER_NODE_HANDSHAKE,加入到本地node信息中。
最后会给A节点回复一个PONG消息。
c
//cluster.c#clusterProcessPacket
sender = clusterLookupNode(hdr->sender);
...
if (!sender && type == CLUSTERMSG_TYPE_MEET) {
clusterNode *node;
node = createClusterNode(NULL,CLUSTER_NODE_HANDSHAKE);
nodeIp2String(node->ip,link,hdr->myip);
node->port = ntohs(hdr->port);
node->cport = ntohs(hdr->cport);
clusterAddNode(node);
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG);
}
...
clusterSendPing(link,CLUSTERMSG_TYPE_PONG);
1.4 A处理B的PONG消息
A接收到B的PONG消息,仍然在clusterProcessPacket方法中处理。此时的node正处于握手阶段。这个时候会更正A本地node信息中B的名字,以及去掉CLUSTER_NODE_HANDSHAKE状态。
此时,A中的本地路由信息看到B的各种状态已经正常了。
c
//cluster.c#clusterProcessPacket
if (type == CLUSTERMSG_TYPE_PING || type == CLUSTERMSG_TYPE_PONG ||
type == CLUSTERMSG_TYPE_MEET) {
if (link->node) {
if (nodeInHandshake(link->node)) {
...
clusterRenameNode(link->node, hdr->sender);
serverLog(LL_DEBUG,"Handshake with node %.40s completed.",
link->node->name);
link->node->flags &= ~CLUSTER_NODE_HANDSHAKE;
link->node->flags |= flags&(CLUSTER_NODE_MASTER|CLUSTER_NODE_SLAVE);
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG);
}
}
}
1.5 B发送PING节点A
当B在周期性检测是,发送A的节点的连接也是没有建立的,因此也会建立连接,并发送PING msg(因为B中的A的flag为CLUSTER_NODE_HANDSHAKE),和步骤2类似。 发送什么类型消息根据flag进行判断。
c
clusterSendPing(link, node->flags & CLUSTER_NODE_MEET ?
CLUSTERMSG_TYPE_MEET : CLUSTERMSG_TYPE_PING);
1.6 A处理B的PING消息
和步骤3类似,无论是PING还是MEET,A会回复给B一个PONG消息。
1.7 B处理A回复的PONG
和步骤4类似,会给A设置名称,并且去掉节点A的flag的CLUSTER_NODE_HANDSHAKE。
但处理到这里,并没有更新sender的信息,即更新了flag,但sender依旧为null,所以后面关于slots的处理,就需要等下一次交互的时候处理了。
2. 请求路由
2.1 判断是否返回重定向信息
在执行具体命令之前,会先根据key查找对应的Node节点,如果查找到的节点不等于自身,那么会返回重定向信息给客户端。
c
//server.c#processCommand
if (server.cluster_enabled &&
!(c->flags & CLIENT_MASTER) &&
!(c->flags & CLIENT_LUA &&
server.lua_caller->flags & CLIENT_MASTER) &&
!(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0 &&
c->cmd->proc != execCommand))
{
int hashslot;
int error_code;
clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc,
&hashslot,&error_code);
if (n == NULL || n != server.cluster->myself) {
if (c->cmd->proc == execCommand) {
discardTransaction(c);
} else {
flagTransaction(c);
}
clusterRedirectClient(c,n,hashslot,error_code);
return C_OK;
}
}
2.2 获取key对应槽的节点
1)内部会对key进行crc16,得到的值进行取余。在keyHashSlot中会判断key是否被{}包裹。
c
//server.c# getNodeByQuery
int thisslot = keyHashSlot((char*)thiskey->ptr,
sdslen(thiskey->ptr));
2)得到的slot后,再获取对应的Node节点。
c
slot = thisslot;
n = server.cluster->slots[slot];
3)如果获取的Node节点为自身,且在迁移列表中,则标记为migrating状态;如果Node节点在导入列表中,则标记为importing状态。
c
if (n == myself &&
server.cluster->migrating_slots_to[slot] != NULL) {
migrating_slot = 1;
} else if (server.cluster->importing_slots_from[slot] != NULL) {
importing_slot = 1;
}
4)如果在migrating或importing状态,会查找当前数据库是否有值,如果没有获取到值,则标记missing_keys状态。
c
if ((migrating_slot || importing_slot) &&
lookupKeyRead(&server.db[0],thiskey) == NULL) {
missing_keys++;
}
5)如果正在迁移状态,且当前数据库没有值,则返回slot对应的迁移列表中的Node节点。此时会返回ASK信息。
c
if (migrating_slot && missing_keys) {
if (error_code) *error_code = CLUSTER_REDIR_ASK;
return server.cluster->migrating_slots_to[slot];
}
6)如果正在导入,且当前的命令为ASKING,那么返回自身节点。
C
if (importing_slot &&
(c->flags & CLIENT_ASKING || cmd->flags & CMD_ASKING)) {
if (multiple_keys && missing_keys) {
if (error_code) *error_code = CLUSTER_REDIR_UNSTABLE;
return NULL;
} else {
return myself;
}
}
7)如果获取到的节点,不是当前节点,则返回MOVED信息
c
if (n != myself && error_code) *error_code = CLUSTER_REDIR_MOVED;
return n;
在processCommand方法中,如果获取的节点不是当前节点,则会返回错误信息给客户端。内部会调用clusterRedirectClient方法,其中对于ASK和MOVED会返回槽,以及对应的节点的ip信息。
c
//cluster.c#clusterRedirectClient
void clusterRedirectClient(client *c, clusterNode *n, int hashslot, int error_code) {
if (error_code == CLUSTER_REDIR_CROSS_SLOT) {
addReplySds(c,sdsnew("-CROSSSLOT Keys in request don't hash to the same slot\r\n"));
} else if (error_code == CLUSTER_REDIR_UNSTABLE) {
/* The request spawns multiple keys in the same slot,
* but the slot is not "stable" currently as there is
* a migration or import in progress. */
addReplySds(c,sdsnew("-TRYAGAIN Multiple keys request during rehashing of slot\r\n"));
} else if (error_code == CLUSTER_REDIR_DOWN_STATE) {
addReplySds(c,sdsnew("-CLUSTERDOWN The cluster is down\r\n"));
} else if (error_code == CLUSTER_REDIR_DOWN_UNBOUND) {
addReplySds(c,sdsnew("-CLUSTERDOWN Hash slot not served\r\n"));
} else if (error_code == CLUSTER_REDIR_MOVED ||
error_code == CLUSTER_REDIR_ASK)
{
addReplySds(c,sdscatprintf(sdsempty(),
"-%s %d %s:%d\r\n",
(error_code == CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
hashslot,n->ip,n->port));
} else {
serverPanic("getNodeByQuery() unknown error.");
}
}
2.3 ASKING标志
在创建客户端连接时,内部会注册networking.c#readQueryFromClient方法,内部会调用processInputBufferAndReplicate方法,然后内部会调用processInputBuffer方法,然后才是会调用processCommand方法,在processCommand方法执行完后,processInputBuffer会执行resetClient方法。
c
//networking.c#processInputBuffer
if (processCommand(c) == C_OK) {
if (c->flags & CLIENT_MASTER && !(c->flags & CLIENT_MULTI)) {
/* Update the applied replication offset of our master. */
c->reploff = c->read_reploff - sdslen(c->querybuf) + c->qb_pos;
}
if (!(c->flags & CLIENT_BLOCKED) || c->btype != BLOCKED_MODULE)
resetClient(c);
}
在resetClient方法中,如果执行的命令不是askingCommand,则会把客户端的flags中的CLIENT_ASKING给清空掉。所以每一次redis返回ASK,客户端都要重新发送ASKING一次。
c
void resetClient(client *c) {
redisCommandProc *prevcmd = c->cmd ? c->cmd->proc : NULL;
freeClientArgv(c);
c->reqtype = 0;
c->multibulklen = 0;
c->bulklen = -1;
/* We clear the ASKING flag as well if we are not inside a MULTI, and
* if what we just executed is not the ASKING command itself. */
if (!(c->flags & CLIENT_MULTI) && prevcmd != askingCommand)
c->flags &= ~CLIENT_ASKING;
}
客户端发送ASKING命令时,redis会把客户端标记为CLIENT_ASKING状态。
c
//cluster.c#askingCommand
void askingCommand(client *c) {
if (server.cluster_enabled == 0) {
addReplyError(c,"This instance has cluster support disabled");
return;
}
c->flags |= CLIENT_ASKING;
addReply(c,shared.ok);
}
3. 参考资料
1)segmentfault.com/a/119000001...
2)Redis源码5.x分支