Quantcast
Channel: Charsyam's Blog
Viewing all articles
Browse latest Browse all 122

[입개발] Redis Command 구조체에 대해서…

$
0
0

오늘 올라온 Redis 버그 중에 클러스터에서 geo command가 redirect를 먹지 않는다 라는 버그가 있었습니다. 그런데 antirez가 아주 쉽게 버그를 수정하면서 패치가 되었다고 답변을 답니다.(이 버그는 cluster 모드일때만 발생합니다.) 그리고 해당 글은 꼭 끝까지 보셔야 합니다!!!

https://github.com/antirez/redis/issues/2671

그리고 그 패치라는 것도 다음과 같습니다.
https://github.com/antirez/redis/commit/5c4fcaf3fe448c5575a9911edbcd421c6dbebb54

     {"geoadd",geoaddCommand,-5,"wm",0,NULL,1,1,1,0,0},
     {"georadius",georadiusCommand,-6,"r",0,NULL,1,1,1,0,0},
     {"georadiusbymember",georadiusByMemberCommand,-5,"r",0,NULL,1,1,1,0,0},
-    {"geohash",geohashCommand,-2,"r",0,NULL,0,0,0,0,0},
-    {"geopos",geoposCommand,-2,"r",0,NULL,0,0,0,0,0},
-    {"geodist",geodistCommand,-4,"r",0,NULL,0,0,0,0,0},
+    {"geohash",geohashCommand,-2,"r",0,NULL,1,1,1,0,0},
+    {"geopos",geoposCommand,-2,"r",0,NULL,1,1,1,0,0},
+    {"geodist",geodistCommand,-4,"r",0,NULL,1,1,1,0,0},
     {"pfselftest",pfselftestCommand,1,"r",0,NULL,0,0,0,0,0},
     {"pfadd",pfaddCommand,-2,"wmF",0,NULL,1,1,1,0,0},
     {"pfcount",pfcountCommand,-2,"r",0,NULL,1,1,1,0,0},

0, 0, 0 이 1, 1, 1 로만 바뀐거죠. 그럼 도대체 저 값들은 뭘 의미하는 걸까요? 그걸 위해서 이제 Redis Command 구조체를 알아보도록 하겠습니다.

먼저 Redis Command 구조체는 다음과 같습니다.

struct redisCommand {
    char *name;
    redisCommandProc *proc;
    int arity;
    char *sflags; /* Flags as string representation, one char per flag. */
    int flags;    /* The actual flags, obtained from the 'sflags' field. */
    /* Use a function to determine keys arguments in a command line.
     * Used for Redis Cluster redirect. */
    redisGetKeysProc *getkeys_proc;
    /* What keys should be loaded in background when calling this command? */
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey;  /* The last argument that's a key */
    int keystep;  /* The step between first and last key */
    long long microseconds, calls;
};

먼저 위의 샘플 커맨드와 하나를 비교해보면

     {"pfadd",pfaddCommand,-2,"wmF",0,NULL,1,1,1,0,0},

name 과 proc는 각각 명확합니다. name은 사용자로 부터 입력받는 명령어이고, proc는 해당 명령어에 연결된 함수포인터입니다. 즉 name으로 사용자가 입력하면 proc가 실행되는 것이죠.

이제 arity 부터가 영어를 알면 쉽게 알 수 있습니다. 그렇습니다. 영어를 모르면 어렵지만…(전 방금 사전을…)
다음과 같은 뜻이 있습니다.

(logic, mathematics, computer science) The number of arguments or operands a function or operation takes. For a relation, the number of domains in the corresponding Cartesian product.

즉 파라메터의 개수입니다. 다음 코드를 보시죠. arity가 양수일 때는 파라매터 개수랑 동일해야 하고(고정적), 음수이면, 절대값으로 이값보다는 많거나 같아야 하는거죠(동적). 즉 -2 면 파라매터가 2개 이상이어야 하는거고, 2면 딱 2개여야 하는겁니다.

    } else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
               (c->argc < -c->cmd->arity)) {
        flagTransaction(c);
        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
            c->cmd->name);
        return REDIS_OK;
    }

sflags는 명령어의 속성을 문자열로 나타냅니다. 각 속성은 populateCommandTable 함수에 잘 나타나 있습니다.

            case 'w': c->flags |= REDIS_CMD_WRITE; break;
            case 'r': c->flags |= REDIS_CMD_READONLY; break;
            case 'm': c->flags |= REDIS_CMD_DENYOOM; break;
            case 'a': c->flags |= REDIS_CMD_ADMIN; break;
            case 'p': c->flags |= REDIS_CMD_PUBSUB; break;
            case 's': c->flags |= REDIS_CMD_NOSCRIPT; break;
            case 'R': c->flags |= REDIS_CMD_RANDOM; break;
            case 'S': c->flags |= REDIS_CMD_SORT_FOR_SCRIPT; break;
            case 'l': c->flags |= REDIS_CMD_LOADING; break;
            case 't': c->flags |= REDIS_CMD_STALE; break;
            case 'M': c->flags |= REDIS_CMD_SKIP_MONITOR; break;
            case 'k': c->flags |= REDIS_CMD_ASKING; break;
            case 'F': c->flags |= REDIS_CMD_FAST; break;

flags는 값이 모두 0입니다. 이 값은 로딩시에 sflags 로 부터 계산해서 만들어집니다. 위의 populateCommandTable 함수를 보시면 sflags 속성에 따라서 c->flags를 만드는 코드가 바로 보입니다. 왜 이렇게 구현했을까요? 아마 숫자값으로 설정되어 있는것 보다, 문자열이 보기 쉬워서가 아닐까 합니다.

getkeys_proc 는 파라매터만 가지고는 key를 찾기가 힘들때, 보조적으로 사용하는 함수의 함수포인터입니다. 위의 예에서는 전부 NULL 이지만, 다음과 같은 커맨들들이 있습니다.

    {"zunionstore",zunionstoreCommand,-4,"wm",0,zunionInterGetKeys,0,0,0,0,0},
    {"zinterstore",zinterstoreCommand,-4,"wm",0,zunionInterGetKeys,0,0,0,0,0},

zunionInterGetKeys 의 형태를 살펴보면 다음과 같습니다.

/* Helper function to extract keys from following commands:
 * ZUNIONSTORE <destkey> <num-keys> <key> <key> ... <key> <options>
 * ZINTERSTORE <destkey> <num-keys> <key> <key> ... <key> <options> */
int *zunionInterGetKeys(struct redisCommand *cmd, robj **argv, int argc, int *numkeys) {
    int i, num, *keys;
    REDIS_NOTUSED(cmd);

    num = atoi(argv[2]->ptr);
    /* Sanity check. Don't return any key if the command is going to
     * reply with syntax error. */
    if (num > (argc-3)) {
        *numkeys = 0;
        return NULL;
    }

    /* Keys in z{union,inter}store come from two places:
     * argv[1] = storage key,
     * argv[3...n] = keys to intersect */
    keys = zmalloc(sizeof(int)*(num+1));

    /* Add all key positions for argv[3...n] to keys[] */
    for (i = 0; i < num; i++) keys[i] = 3+i;

    /* Finally add the argv[1] key position (the storage key target). */
    keys[num] = 1;
    *numkeys = num+1;  /* Total keys = {union,inter} keys + storage key */
    return keys;
}

그리고 getkeys_proc 를 쓰는 곳은 db.c 에서 getKeysFromCommand 에서 다음과 같이 사용하고 있습니다.

int *getKeysFromCommand(struct redisCommand *cmd, robj **argv, int argc, int *numkeys) {
    if (cmd->getkeys_proc) {
        return cmd->getkeys_proc(cmd,argv,argc,numkeys);
    } else {
        return getKeysUsingCommandTable(cmd,argv,argc,numkeys);
    }
}

그런데 재미있는 것은 getkeys_proc 는 실제로 코드는 예전부터 존재했으나, 거의 쓰이지 않던 파라매터라는 것입니다. 실제로 현재 버전에서도 commandCommand 와 cluster 정보를 얻기위해서만 쓰고 있습니다.

firstkey, lastkey, keystep 은 각각, 첫번째 파라매터가 key 된다. 마지막 파라매터가 key 가 된다. keystep은 1이면 [key, key, key] 형태고, 2이면 [key, val, key, val, key, val] 형태가 되는 것을 의미합니다. 그래서 이 값이, 0, 0, 0 이면 geohash가 다음과 같은 결과를 냅니다.

127.0.0.1:6379> GEOHASH Sicily Palermo Catania
(empty list or set)

위의 값이 1로 셋팅이 되면… 다음과 같이 redirect 결과가 나옵니다.

127.0.0.1:6381> GEOHASH Sicily Palermo Catania
(error) MOVED 10713 127.0.0.1:6380

물론 둘 다 해당 서버인 6380에서 실행하면 정상적인 결과가 나옵니다.

127.0.0.1:6380> GEOHASH Sicily Palermo Catania
1) "sqc8b49rny0"
2) "sqdtr74hyu0"

그런데 왜 이게 이런 차이가 날까요? 아까 살짝 언급한게 “getkeys_proc 는 commandCommand 와 cluster 정보를 얻기위해서만 쓰고 있습니다.” 라고 말했습니다. 저 firstkey, lastkey, keystep 도 상단의 getKeysUsingCommandTable 즉 getKeysFromCommand 에서만 사용되고 있습니다.

다시말하면, 결국 cluster 에서 뭔가 정보를 얻을때, firstkey, lastkey, keystep을 쓴다는 것입니다. 그냥 커맨드가 독립적으로 실행될 때는 안쓴다는 T.T

코드를 찾아보면 cluster.c 의 getNodeByQuery 에서 위의 getKeysFromCommand 를 쓰고 있습니다. 어떤용도로 쓰고 있을까요?

geohash 명령을 cluster 모드에서 받았다고 할때… 실제로 getKeysFromCommand 는 getKeysUsingCommandTable를 호출하게 되는데 cmd->firstkey 는 0이므로 NULL이 리턴됩니다. numkey도 0이 셋팅됩니다.

int *getKeysUsingCommandTable(struct redisCommand *cmd,robj **argv, int argc, int *numkey>
    int j, i = 0, last, *keys;
    REDIS_NOTUSED(argv);

    if (cmd->firstkey == 0) {
        *numkeys = 0;
        return NULL;
    }
    last = cmd->lastkey;
    if (last < 0) last = argc+last;
    keys = zmalloc(sizeof(int)*((last - cmd->firstkey)+1));
    for (j = cmd->firstkey; j <= last; j += cmd->keystep) {
        redisAssert(j < argc);
        keys[i++] = j;
    }
    *numkeys = i;
    return keys;
}

그래서 getNodeByQuery에서는 다음과 같은 코드가 실행됩니다.

clusterNode *n = NULL
keyindex = getKeysFromCommand(mcmd,margv,margc,&numkeys);

//......

if (n == NULL) return myself;

즉 numkey가 0이라 노드를 찾는 작업을 하지 않고 n == NULL 이라서 자기자신에 속한다고 리턴해버립니다. 그래서 자기가 붙은 노드에서 검색하고 결과가 없어서 빈 값이 넘어가게 되는겁니다.

실제로 processCommand 소스를 보시면… 아래에 getNodeByQuery 를 호출하고, myself 가 아닐때에는 hashslot을 가진 서버로 clusterRedirectClient를 호출해서 보내게 되는데… 아까 위의 결과에서 myself가 리턴되기 때문에… 해당 서버에서 실행되게 되어서 결과가 없는 것입니다.

    if (server.cluster_enabled &&
        !(c->flags & REDIS_MASTER) &&
        !(c->flags & REDIS_LUA_CLIENT &&
          server.lua_caller->flags & REDIS_MASTER) &&
        !(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0))
    {
        int hashslot;

        if (server.cluster->state != REDIS_CLUSTER_OK) {
            flagTransaction(c);
            clusterRedirectClient(c,NULL,0,REDIS_CLUSTER_REDIR_DOWN_STATE);
            return REDIS_OK;
        } else {
            int error_code;
            clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc,&hashslot,&error_cod>
            if (n == NULL || n != server.cluster->myself) {
                flagTransaction(c);
                clusterRedirectClient(c,n,hashslot,error_code);
                return REDIS_OK;
            }
        }
    }

딱, 이렇습니다라고 말해야 하지만… 사실 여기는 훼이크가 있습니다. 위의 processCommand의 코드를 자세히 살펴보면… 아래와 같은 코드가 있습니다.

    !(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0)

네 그렇습니다. getkeys_proc 가 없고, firstkey가 0이 아니어야 아래의 코드를 타는 것입니다. 미리 조건을 걸러낸것입니다. 그래서 실제로… 위의 코드를 타지 않고… 아예 바로 해당 서버에서 실행됩니다. 즉 myself를 리턴한것과 동일한 효과가 나는 것이지요. firstkey가 0이면 파라매터가 없는 명령으로 인식되어서 그냥 해당 서버에서 실행이 되는 것입니다.

살짝 속으셨다고 분노하시겠지만, 결국은 해당 코드도 그렇게 동작하게 됩니다. :) 호호호호… 그러면 즐거운 하루되시길…



Viewing all articles
Browse latest Browse all 122

Trending Articles