Redis源码分析之集群Meet命令和请求路由


Redis系列文章

原理篇

源码篇

问题分析


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分支

相关推荐
颜淡慕潇1 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
minihuabei1 小时前
linux centos 安装redis
linux·redis·centos
尘浮生2 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料2 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
monkey_meng3 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马3 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng3 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
hlsd#3 小时前
go 集成go-redis 缓存操作
redis·缓存·golang
奶糖趣多多5 小时前
Redis知识点
数据库·redis·缓存
CoderIsArt6 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存