从MOVED、ASK看集群节点如何处理命令

97 阅读8分钟

集群节点处理命令的基本流程

Redis server 处理一条命令的过程可以分成四个阶段,分别是命令读取、命令解析、命令执行和结果返回。

对于像 Redis Cluster 这样,没有使用中心化的第三方系统来维护数据分布的分布式系统来说,当集群由于负载均衡或是节点故障而导致数据迁移时,请求重定向是不可避免的。

在命令执行阶段中,针对集群节点增加的处理流程,这是在 processCommand 函数(在 server.c 文件)中实现的。

processCommand 函数在执行过程中,会判断当前节点是否处于集群模式,这是通过全局变量 server 的 cluster_enable 标记来判断的。如果当前节点处于集群模式,processCommand 函数会判断是否需要执行重定向。

调用了 getNodeByQuery 函数(在cluster.c文件中),来查询当前收到的命令能在哪个集群节点上进行处理。如果 getNodeByQuery 函数返回的结果是空,或者查询到的集群节点不是当前节点,那么,processCommand 函数就会调用 clusterRedirectClient 函数(在 cluster.c 文件中),来实际执行请求重定向。

int processCommand(client *c) {
   …
   //当前Redis server启用了Redis Cluster模式;收到的命令不是来自于当前key的主节点;收到的命令包含了key参数,或者命令是EXEC
   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))
  {
     …
     clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc, &hashslot,&error_code); //查询当前命令可以被哪个集群节点处理
     if (n == NULL || n != server.cluster->myself) {
        …
        clusterRedirectClient(c,n,hashslot,error_code); //实际执行请求重定向
              return C_OK;
          }
      }

如何查询能运行命令的集群节点?

getNodeByQuery 函数的原型,如下所示:

clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, int argc, int *hashslot, int *error_code)

它的函数参数包括了节点收到的命令及参数。同时,它的参数中还包括了两个指针:hashslot 和 error_code,这两个指针分别表示命令访问的 key 所在的 slot(哈希槽),以及函数执行后的错误代码。此外,getNodeByQuery 函数的返回值是 clusterNode 类型,表示的是能处理命令的集群节点。

getNodeByQuery 函数的具体执行过程,这个过程基本可以分成三个步骤来完成。

第一步,使用 multiState 结构体封装收到的命令

因为集群节点可能收到 MULTI 命令,而 MULTI 命令表示紧接着它的多条命令是需要作为一个事务来执行的。

针对单节点:

执行函数 processCommand ,它在处理命令时,会判断客户端变量 client 中是否有 CLIENT_MULTI 标记。如果有的话,processCommand 会调用 queueMultiCommand 函数,把后续收到的命令缓存在 client 结构体的 mstate 成员变量中。mstate 成员变量的类型是 multiState 结构体,它记录了 MULTI 命令后的其他命令以及命令个数。

int processCommand(client *c) {
…
//客户端有CLIENT_MULTI标记,同时当前命令不是EXEC,DISCARD, MULTI和WATCH
if (c->flags & CLIENT_MULTI &&
  c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
  c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
  {
        queueMultiCommand(c); //缓存命令
        …
  }

针对集群:

为了使用同样的数据结构,来处理 MULTI 命令的后续命令和常规的单条命令,getNodeByQuery 函数就使用了 multiState 结构体,来封装当前要查询的命令.

multiState *ms, _ms; //使用multiState结构体封装要查询的命令
…
if (cmd->proc == execCommand) { //如果收到EXEC命令,那么就要检查MULTI后续命令访问的key情况,所以从客户端变量c中获取mstate
   …
   ms = &c->mstate;
} else {
   ms = &_ms;  //如果是其他命令,那么也使用multiState结构体封装命令
   _ms.commands = &mc;
   _ms.count = 1;  //封装的命令个数为1
   mc.argv = argv; //命令的参数
   mc.argc = argc; //命令的参数个数
   mc.cmd = cmd; //命令本身
}

MULTI 命令后缓存的其他命令并不会立即执行,而是需要等到 EXEC 命令执行时才会执行。所以,在刚才的代码中,getNodeByQuery 函数也是在收到 EXEC 命令时,才会从客户端变量 c 中获取缓存的命令 mstate。

第二步,针对收到的每个命令,逐一检查这些命令访问的 key 所在的 slots

getNodeByQuery 函数会根据 multiState 结构中记录的命令条数,执行一个循环,逐一检查每条命令访问的 key。具体来说,它会调用 getKeysFromCommand 函数(在db.c文件中)获取命令中的 key 位置和 key 个数。

然后,它会针对每个 key,调用 keyHashSlot 函数(在 cluster.c 文件中)查询这个 key 所在的 slot,并在全局变量 server 的 cluster 成员变量中,查找这个 slot 所属的集群节点,如下所示:

for (i = 0; i < ms->count; i++) {
   …
   //获取命令中的key位置和key个数
   keyindex = getKeysFromCommand(mcmd,margv,margc,&numkeys);
   //针对每个key执行
   for (j = 0; j < numkeys; j++) {
  …
  int thisslot = keyHashSlot((char*)thiskey->ptr, //获取key所属的slot                                       sdslen(thiskey->ptr));
  if (firstkey == NULL) {
     …
     slot = thisslot;
     n = server.cluster->slots[slot]; //查找key所属的slot对应的集群节点
  }
  …
    }
}

紧接着,getNodeByQuery 函数会根据查找的集群节点结果进行判断,主要有以下三种情况。

  • 情况一:查找的集群节点为空,此时它会报错,将 error_code 设置为 CLUSTER_REDIR_DOWN_UNBOUND。
if (n == NULL) {
   …
    if (error_code)
       *error_code = CLUSTER_REDIR_DOWN_UNBOUND;
    return NULL;
}
  • 情况二:查找的集群节点就是当前节点,而 key 所属的 slot 正在做数据迁出操作,此时,getNodeByQuery 函数会设置变量 migrating_slot 为 1,表示正在做数据迁出。
  • 情况三:key 所属的 slot 正在做数据迁入操作,此时,getNodeByQuery 函数会设置变量 importing_slot 为 1,表示正在做数据迁入。
//如果key所属的slot正在迁出,则设置migrating_slot为1
if (n == myself && server.cluster->migrating_slots_to[slot] != NULL)
{
   migrating_slot = 1;
} //如果key所属的slot正在迁入,则设置importing_slot为1
else if (server.cluster->importing_slots_from[slot] != NULL) {
   importing_slot = 1;
}

如果命令包含的 key 不止 1 个,而且这些 keys 不在同一个 slot,那么 getNodeByQuery 函数也会报错,并把 error_code 设置为 CLUSTER_REDIR_CROSS_SLOT。

如果节点正在做数据迁出或迁入,那么,getNodeByQuery 函数就会调用 lookupKeyRead 函数(在 db.c 文件中),检查命令访问的 key 是否在当前节点的数据库中。如果没有的话,它会用一个变量 missing_keys,记录缺失的 key 数量,如下所示:

//如果key所属slot正在迁出或迁入,并且当前访问的key不在本地数据库,那么增加missing_keys的大小
if ((migrating_slot || importing_slot) && lookupKeyRead(&server.db[0],thiskey) == NULL)
{
    missing_keys++;
}

第三步,根据 slot 的检查结果返回 hashslot、error_code 和相应的集群节点

在 getNodeByQuery 函数的返回结果中,我们可以重点关注以下四种情况。

  • 情况一:命令访问 key 所属的 slot 没有对应的集群节点,此时,getNodeByQuery 函数会返回当前节点。在这种情况下,有可能是集群有故障导致无法查找到 slot 所对应的节点,而 error_code 中会有相应的报错信息。
if (n == NULL) return myself;
  • 情况二:命令访问 key 所属的 slot 正在做数据迁出或迁入,而且当前命令就是用来执行数据迁移的 MIGRATE 命令,那么,getNodeByQuery 函数会返回当前节点,如下所示:
if ((migrating_slot || importing_slot) && cmd->proc == migrateCommand)
   return myself;
  • 情况三:命令访问 key 所属的 slot 正在做数据迁出,并且命令访问的 key 在当前节点数据库中缺失了,也就是刚才介绍的 missing_keys 大于 0。此时,getNodeByQuery 函数会把 error_code 设置为 CLUSTER_REDIR_ASK,并返回数据迁出的目标节点。
if (migrating_slot && missing_keys) {
        if (error_code) *error_code = CLUSTER_REDIR_ASK;
        return server.cluster->migrating_slots_to[slot];
  }
  • 情况四:命令访问 key 所属的 slot 对应的节点不是当前节点,而是其他节点,此时,getNodeByQuery 函数会把 error_code 设置为 CLUSTER_REDIR_MOVED,并返回 key 所属 slot 对应的实际节点。
if (n != myself && error_code) *error_code = CLUSTER_REDIR_MOVED;
    return n;

请求重定向函数 clusterRedirectClient 的执行

当 getNodeByQuery 函数查到的集群节点为空或者不是当前节点时,clusterRedirectClient 函数就会被调用。

clusterRedirectClient 函数的逻辑比较简单,它就是根据 getNodeByQuery 函数返回的 error_code 的不同值,执行相应的代码分支,主要是把 key 所属 slot 对应集群节点的情况返回给客户端,从而让客户端根据返回的信息作出相应处理。比如:

  • 当 error_code 被设置成 CLUSTER_REDIR_CROSS_SLOT 时,clusterRedirectClient 函数就返回给客户端“key 不在同一个 slot 中”的报错信息;
  • 当 error_code 被设置成 CLUSTER_REDIR_MOVED 时,clusterRedirectClient 函数会返回 MOVED 命令,并把 key 所属的 slot、slot 实际所属的节点 IP 和端口号,返回给客户端;
  • 当 error_code 被设置成 CLUSTER_REDIR_ASK 时,clusterRedirectClient 函数会返回 ASK 命令,并把 key 所属的 slot、slot 正在迁往的目标节点 IP 和端口号,返回给客户端。
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_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));
  }
  
}

Redis Cluster 的客户端和针对单个 Redis server 的客户端,在实现上是有差别的。Redis Cluster 客户端需要能处理节点返回的报错信息,比如说,如果集群节点返回 MOVED 命令,客户端就需要根据这个命令,以及其中包含的实际节点 IP 和端口号,来访问实际有数据的节点。


此文章为10月Day27学习笔记,内容来源于极客时间《Redis 源码剖析与实战》