[입 개발] Redis Cluster 에서의 Pub/Sub은 어떻게 동작할까?

저도 잘 모르고 있다가, 오늘 질문을 받고 급하게 찾아보고 아는 척을 한게 있습니다. 저야 뭐, 레디스 클러스터를 잘 안쓰고 있어서… 예전에 살짝 살펴보고 관심이 많이 떨어져 있기도 한…(비겁한 변명입니다.)

오늘의 질문은 “Redis Cluster 에서 Pub/Sub이 동작하는가?” 였습니다. 아마 다들 아시고 있으실 것 같지만… 일단 답 부터 하면 “동작합니다. 아무 노드에서나 subscribe 하고 아무 노드에서나 publish 하면 다 받을 수 있습니다.” 가 답입니다.

그럼 어떻게 구현이 되어 있을가요?

먼저 subscribe 동작 부터 살펴보면 subscribe 동작은 기존의 redis 와 동일합니다.

void subscribeCommand(client *c) {
   int j;

   for (j = 1; j < c->argc; j++)
        pubsubSubscribeChannel(c,c->argv[j]);
        c->flags |= CLIENT_PUBSUB;
   }
}

int pubsubSubscribeChannel(client *c, robj *channel) {
    dictEntry *de;
    list *clients = NULL;
    int retval = 0;

    /* Add the channel to the client->channels hash table */
    if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
        retval = 1;
        incrRefCount(channel);
        /* Add the client to the channel->list of clients hash table */
        de = dictFind(server.pubsub_channels,channel);
        if (de == NULL) {
            clients = listCreate();
            dictAdd(server.pubsub_channels,channel,clients);
            incrRefCount(channel);
        } else {
            clients = dictGetVal(de);
        }
        listAddNodeTail(clients,c);
    }
    /* Notify the client */
    addReply(c,shared.mbulkhdr[3]);
    addReply(c,shared.subscribebulk);
    addReplyBulk(c,channel);
    addReplyLongLong(c,clientSubscriptionsCount(c));
    return retval;
}

즉 클라이언트는 클러스터내의 아무(마스터중에서) Redis 서버에 접속해서 subscribe 명령을 날립니다.
그러면 해당 Redis 서버에는 server.pubsub_channels 라는 dict 안에 해당 channel 이 생성되게 됩니다.

이제 중요한 부분은 Publish 부분입니다. 그러나 간단합니다. 사실 Redis Cluster Spec 이나 github issue #1927 이런 걸 보면 Cluster Bus니 하면서 굉장히 복잡하게 보이지만… 그냥 publish는 모든 마스터에 이 채널에 이런 메시지가 왔다라고 전달하게 됩니다. 그러면 해당 서버는 다시 해당 채널에 등록된 클라이언트들에게 메시지를 보내는 아주 간단한 구조입니다.

void publishCommand(client *c) {
    int receivers = pubsubPublishMessage(c->argv[1],c->argv[2]);
    if (server.cluster_enabled)
        clusterPropagatePublish(c->argv[1],c->argv[2]);
    else
        forceCommandPropagation(c,PROPAGATE_REPL);
    addReplyLongLong(c,receivers);
}

void clusterPropagatePublish(robj *channel, robj *message) {
    clusterSendPublish(NULL, channel, message);
}

void clusterSendPublish(clusterLink *link, robj *channel, robj *message) {
    unsigned char buf[sizeof(clusterMsg)], *payload;
    clusterMsg *hdr = (clusterMsg*) buf;
    uint32_t totlen;
    uint32_t channel_len, message_len;

    channel = getDecodedObject(channel);
    message = getDecodedObject(message);
    channel_len = sdslen(channel->ptr);
    message_len = sdslen(message->ptr);

    clusterBuildMessageHdr(hdr,CLUSTERMSG_TYPE_PUBLISH);
    totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
    totlen += sizeof(clusterMsgDataPublish) - 8 + channel_len + message_len;

    hdr->data.publish.msg.channel_len = htonl(channel_len);
    hdr->data.publish.msg.message_len = htonl(message_len);
    hdr->totlen = htonl(totlen);

    /* Try to use the local buffer if possible */
    if (totlen < sizeof(buf)) {
        payload = buf;
    } else {
        payload = zmalloc(totlen);
        memcpy(payload,hdr,sizeof(*hdr));
        hdr = (clusterMsg*) payload;
    }
    memcpy(hdr->data.publish.msg.bulk_data,channel->ptr,sdslen(channel->ptr));
    memcpy(hdr->data.publish.msg.bulk_data+sdslen(channel->ptr),
        message->ptr,sdslen(message->ptr));

    if (link)
        clusterSendMessage(link,payload,totlen);
    else
        clusterBroadcastMessage(payload,totlen);

    decrRefCount(channel);
    decrRefCount(message);
    if (payload != buf) zfree(payload);
}

void clusterBroadcastMessage(void *buf, size_t len) {
    dictIterator *di;
    dictEntry *de;

    di = dictGetSafeIterator(server.cluster->nodes);
    while((de = dictNext(di)) != NULL) {
        clusterNode *node = dictGetVal(de);

        if (!node->link) continue;
        if (node->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_HANDSHAKE))
            continue;
        clusterSendMessage(node->link,buf,len);
    }
    dictReleaseIterator(di);
}

아래로 내려가면 최종적으로 clusterSendMessage를 모든 노드에 보내는 것을 알 수 있습니다. 맨 마지막 코드에서 보면 자기자신은 제외하는데, 이미 젤 위의 publishCommand 에서 자기 자신에 존재하는 채널을 찾아서 이미 보내고 있습니다.

그래서 결국 아무 Redis 서버에나 subscribe를 하고 아무 Redis 서버에서 publish를 하면 pub/sub 이 동작합니다. 그런데!!! 그런데!!! 그런데!!! 과연 이런 구조가 아무런 문제가 없을까요? 하나 더 집어드리자면… 간단하게 생각해서 A노드에만 subscribe 된 B라는 채널이 있고 마스터가 10대라면… 의미없는 9대의 서버에도 현재는 모두 메시지를 전달해야 합니다. 밴드위스 낭비와, 노드가 많아질때마다 성능상 문제가 있을 수도 있습니다. 여기에 대해서 걱정하는 글들도 있긴합니다만, 현재까지는… 좋은 방법이 구현되어 있지는 않습니다. 일단 해당 관련 내용은 다음 글들을 참고해보시기 바랍니다.

issue 2672
issue 122