[입 개발] Sentinel은 어떻게 노드를 찾아낼까?

점점, 내용이 없어지는 입개발입니다. 최근에는 백수가 열심히 먹고 살아볼려고 하니, 좋은 내용이 없네요. 죄송합니다. 요새는 Redis HA쪽에 관심이 있다보니, 점점 Sentinel을 자주보게 되는데요. 근원적인 질문을 하나 던집니다. Sentinel은 어떻게 감시해야할 노드를 찾아낼까?

사실 어떻게 보면 간단합니다. 우리는 sentinel.conf에 감시해야할 서버의 주소를 미리적어두니까요. 그런데 슬레이브 노드 중에 마스터로 승격을 시키게 되는데, 과연 이 슬레이브 노드들을 어떻게 찾는 것일까요?

핵심은 “INFO” 명령에 있습니다. 아무런 슬레이브가 없을 때 Redis 서버에 “INFO” 명령을 주면 다음과 같습니다.

# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

이제 하나의 슬레이브가 있을 때는 다음과 같습니다.

# Replication
role:master
connected_slaves:1
slave0:127.0.0.1,6380,online,1
master_repl_offset:1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:0

중간에 slave0: 127.0.0.1,6380,online,1 이런 정보가 보이죠. 그럼 슬레이브가 한 100개까지 추가되면 어떻게 될까요? 네 아마도 slave99 이런식으로 정보가 생길것입니다. sentinel도 마찬가지 입니다. 접속해야할 노드에 INFO명령을 통해서 위의 주소를 분석해 내게 됩니다.

얼마전에 unstable에 sentinel에서 전혀 슬레이브 노드를 찾지 못해서 sentinel이 동작하지 않는 버그가 있었는데 최신 unstable에서 해당 내용이 수정되었기 때문입니다. 최신 버전에 대해서 INFO를 날려보면 다음과 같습니다.

# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=1,lag=0
master_repl_offset:1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:0

보시면 기존에는 단순히 ‘,’ 로 분리만 되던것에 이름 정보가 붙었습니다. 현재 버전의 sentinel은 이전 형식과 최신 형식 두 가지 모두를 지원하고 있습니다. sentinel.c 의 sentinelRefreshInstanceInfo 함수를 보면 됩니다.
아주, 단순하게 구현되어 있습니다.

        if ((ri->flags & SRI_MASTER) &&
            sdslen(l) >= 7 &&
            !memcmp(l,"slave",5) && isdigit(l[5]))
        {
            char *ip, *port, *end;

            if (strstr(l,"ip=") == NULL) {
                /* Old format. */
                ip = strchr(l,':'); if (!ip) continue;
                ip++; /* Now ip points to start of ip address. */
                port = strchr(ip,','); if (!port) continue;
                *port = '\0'; /* nul term for easy access. */
                port++; /* Now port points to start of port number. */
                end = strchr(port,','); if (!end) continue;
                *end = '\0'; /* nul term for easy access. */
            } else {
                /* New format. */
                ip = strstr(l,"ip="); if (!ip) continue;
                ip += 3; /* Now ip points to start of ip address. */
                port = strstr(l,"port="); if (!port) continue;
                port += 5; /* Now port points to start of port number. */
                /* Nul term both fields for easy access. */
                end = strchr(ip,','); if (end) *end = '\0';
                end = strchr(port,','); if (end) *end = '\0';
            }

또한 여기서 행여나 sentinel.conf 에 마스터가 아닌 슬레이브 주소가 있을 때 처리하는 부분도 들어가 있습니다. 먼저 슬레이브 노드에 INFO 명령을 주면 다음과 같은 결과가 나옵니다.

# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:435
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

보시면 내용에 role: slave 이고, master_host, master_port 가 있는 것을 알 수 있습니다. 즉, 접속한 노드가 슬레이브라면, 해당 정보를 이용해서 마스터를 찾을 수 있습니다.(다만, 딱 자신의 마스터만 찾을 수 있습니다. 멀티 체인 형태로 리플리케이션을 하고 있다면, 최상위 부모는 찾을 수가 없습니다.)

그럼 이제 Sentinel에서 마스터와 슬레이브 노드를 찾아내는 것은 알아냈습니다. 그런데 sentinel을 여러대 동작시키면, sentinel 끼리도 동작하게 됩니다. 여기서는 도대체 어떻게 서로 알아내는 것일까요? 여기서는 복합적으로 정보를 주고 받게 됩니다. 앞에 내용으로 Sentinel은 INFO 명령을 통해서 마스터와 슬레이브를 구분할 수 있다고 했습니다. 그리고, Sentinel은 마스터에 pub/sub을 이용해서 SENTINEL_HELLO_CHANNEL (__sentinel__:hello) 값을 이용해서 데이터를 주고 받게 됩니다. 여기서 서로간의 heartbeat 를 이루게 됩니다. 주고 받게 되는 Hello 메시지는 다음과 같습니다. ip:port:runid 입니다. 마지막 값은 failover 할지 말지를 설정하는 값입니다.

*4
$8
pmessage
$1
*
$18
__sentinel__:hello
$58
127.0.0.1:26379:7d717f945afde99e6f82f825de052f17cab7e6f3:1

해당 코드는 sentinelReconnectInstance 에서 호출됩니다.

            int retval;

            ri->pc_conn_time = mstime();
            ri->pc->data = ri;
            redisAeAttach(server.el,ri->pc);
            redisAsyncSetConnectCallback(ri->pc,
                                            sentinelLinkEstablishedCallback);
            redisAsyncSetDisconnectCallback(ri->pc,
                                            sentinelDisconnectCallback);
            sentinelSendAuthIfNeeded(ri,ri->pc);
            /* Now we subscribe to the Sentinels "Hello" channel. */
            retval = redisAsyncCommand(ri->pc,
                sentinelReceiveHelloMessages, NULL, "SUBSCRIBE %s",
                    SENTINEL_HELLO_CHANNEL);
            if (retval != REDIS_OK) {
                /* If we can't subscribe, the Pub/Sub connection is useless
                 * and we can simply disconnect it and try again. */
                sentinelKillLink(ri,ri->pc);
                return;
            }

그리고 위의 Hello Pub/Sub을 이용해서 받은 주소가 현재 자신이 가지고 있지 않은 값이면 Sentinel로 등록하게 됩니다. sentinelReceiveHelloMessages 를 보시면 됩니다.

            port = atoi(token[1]);
            canfailover = atoi(token[3]);
            sentinel = getSentinelRedisInstanceByAddrAndRunID(
                            ri->sentinels,token[0],port,token[2]);

            if (!sentinel) {
                /* If not, remove all the sentinels that have the same runid
                 * OR the same ip/port, because it's either a restart or a
                 * network topology change. */
                removed = removeMatchingSentinelsFromMaster(ri,token[0],port,
                                token[2]);
                if (removed) {
                    sentinelEvent(REDIS_NOTICE,"-dup-sentinel",ri,
                        "%@ #duplicate of %s:%d or %s",
                        token[0],port,token[2]);
                }

                /* Add the new sentinel. */
                sentinel = createSentinelRedisInstance(NULL,SRI_SENTINEL,
                                token[0],port,ri->quorum,ri);
                if (sentinel) {
                    sentinelEvent(REDIS_NOTICE,"+sentinel",sentinel,"%@");
                    /* The runid is NULL after a new instance creation and
                     * for Sentinels we don't have a later chance to fill it,
                     * so do it now. */
                    sentinel->runid = sdsnew(token[2]);
                }
            }

사실 http://redis.io/topics/sentinel 여기를 보시면 더 잘 나와있습니다. 쿨럭…