[REDIS] SLAVEOF NO ONE 과 Redis Initial Sync 과정

Redis 는 Chained Replication을 지원합니다. 다만 Master/Slave 간의 Replication만 지원하고 Master/Master는 지원을 하지 않습니다.( Replication 받은 것에 대해서 M/M이면 다시 전달하는 과정이 있으면 안되는데, 이런 부분에 대해서 처리하는 부분이 없습니다. )

그리고 Redis의 명령중에 SLAVEOF 라는 명령을 사용해서 새로운 Master를 설정하거나, M/S 간의 관계를 끊을 수 가 있습니다. 그리고 이 명령어에 대해서 잘 알고 있어야만 차후 실수로 인한 재앙을 막을 수 있습니다. 왜 그런지에 대해서 Redis의 초기 initial Sync 과정을 통해서 알아보도록 하겠습니다.

redis 가 실행되면 당연히 main 에서 먼저 실행이 되게 됩니다.( 뭐 OS를 복잡하게 생각하면 이 앞단계에 여러가지가 있지만, 이건 소스에서 확인할 수 있는 부분은 아니니 패스합니다. )

일단 소스는 2.4.8 버전을 대상으로 합니다. redis.c:1749 에 main 함수가 있습니다. 여기서 redis.c:901 의 initServer 를 호출하게 됩니다.

    if (server.daemonize) daemonize();
    initServer();
    if (server.daemonize) createPidFile();
    redisLog(REDIS_NOTICE,"Server started, Redis version " REDIS_VERSION);

다시 initServer 에서는 network 설정을 하고 timer 이벤트를 통해서 redis.c:523의 serverCron을 호출하게 됩니다. 주기적으로
serverCron이 호출되면서 종료처리나 에러처리에 대한 처리를 하게 됩니다.

    server.stat_fork_time = 0;
    server.unixtime = time(NULL);
    aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
    if (server.ipfd > 0 && aeCreateFileEvent(server.el,server.ipfd,AE_READABLE,
        acceptTcpHandler,NULL) == AE_ERR) oom("creating file event");
    if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
        acceptUnixHandler,NULL) == AE_ERR) oom("creating file event");

그리고 여기서 replication.c:521의 replicationCron을 호출합니다. replicationCron에서는 실제로 master와의 Connection 관계를 확인해서 복원하는 등의 일을 하게 됩니다.

    /* Replication cron function -- used to reconnect to master and
     * to detect transfer failures. */
    if (!(loops % 10)) replicationCron();

    server.cronloops++;
    return 100;

그리고 Replication 설정이 되어있으면 replication.c:584 의 connectWithMaster라는 함수를 호출하게 됩니다. 이 함수는 실제 마스터와 연결을 시도하고 연결이 되면 syncWithMaster 라는 함수를 호출하게 됩니다.

int connectWithMaster(void) {
    int fd;

    fd = anetTcpNonBlockConnect(NULL,server.masterhost,server.masterport);
    if (fd == -1) {
        redisLog(REDIS_WARNING,"Unable to connect to MASTER: %s",
            strerror(errno));
        return REDIS_ERR;
    }

    if (aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL) ==
            AE_ERR)
    {
        close(fd);
        redisLog(REDIS_WARNING,"Can't create readable event for SYNC");
        return REDIS_ERR;
    }

    server.repl_transfer_lastio = time(NULL);
    server.repl_transfer_s = fd;
    server.replstate = REDIS_REPL_CONNECTING;
    return REDIS_OK;
}

replication.c:371 의 syncWithMaster 라는 함수에서는 Auth 설정이 있으면 Auth를 하고, Master에거 Sync명령을 전달합니다. 그리고 내부적으로 임시 RDB파일을 생성합니다. 이 임시 RDB파일에 실제로 Master의 내용을 저장해두고 나중에 로드하게 됩니다.

    /* Prepare a suitable temp file for bulk transfer */
    while(maxtries--) {
        snprintf(tmpfile,256,
            "temp-%d.%ld.rdb",(int)time(NULL),(long int)getpid());
        dfd = open(tmpfile,O_CREAT|O_WRONLY|O_EXCL,0644);
        if (dfd != -1) break;
        sleep(1);
    }   
    if (dfd == -1) {
        redisLog(REDIS_WARNING,"Opening the temp file needed for MASTER <-> SLAVE synchronization: %s",strerror(errno));
        goto error;
    }   

    /* Setup the non blocking download of the bulk file. */
    if (aeCreateFileEvent(server.el,fd, AE_READABLE,readSyncBulkPayload,NULL)
            == AE_ERR)
    {           
        redisLog(REDIS_WARNING,"Can't create readable event for SYNC");
        goto error;
    }       

    server.replstate = REDIS_REPL_TRANSFER;
    server.repl_transfer_left = -1;
    server.repl_transfer_fd = dfd;
    server.repl_transfer_lastio = time(NULL);
    server.repl_transfer_tmpfile = zstrdup(tmpfile);

그리고 Master로 부터 데이터가 올때마다 replication.c:278의 readSyncBulkPayload 를 호출하게 됩니다. 이것은 데이터가 전달될때 마다 호출하게 되는데, SYNC 가 완료되면 일단 db.c:158의 emptyDb 라는 함수를 호출해서 메모리를 모두 날려버립니다. 그리고 아까 만들었던 임시 RDB파일을 로드하게 됩니다.

/* Empty the whole database */
long long emptyDb() {
    int j;
    long long removed = 0;

    for (j = 0; j < server.dbnum; j++) {
        removed += dictSize(server.db[j].dict);
        dictEmpty(server.db[j].dict);
        dictEmpty(server.db[j].expires);
    }   
    return removed;
}

그런데 이 과정에 어떤 문제가 있느냐? 라는 의문이 생길껍니다. Master가 장애가 났다가 복구가 되면 어떻게 될까요? 다시 같은 현상이 벌어지게 됩니다. 그런데 이 상황에서 Master에 내용이 없다면 Slave는 아무 데이터도 가져오지 못하게 되고 emptyDb가 호출되어서 메모리르 모두 날리게 되는 상황이 벌어지는 겁니다.

일반적으로 Redis의 경우 Master는 성능 향상을 위해서 Disk I/O를 없애고 Slave 에서 Snapshot을 저장하는 구조를 이용하기 때문에, 경우에 따라서 문제가 발생할 수 있습니다.

SLAVEOF NO ONE은 Master 설정을 제거해주는 간단한 명령입니다. 이 경우 Master설정이 없으므로 위와 같은 감시가 일어나지 않으므로, 데이터가 유실된 가능성이 줄어듭니다. 즉 Master/Slave 구조에서 Master에 장애가 나면 Slave에게 SLAVEOF NO ONE을 던지고 해당 장비를 Master로 이용하는 것이 괜찮은 방법일 듯 합니다.