본문 바로가기

Data Base/Redis

Redis - Master / Replica(Slave) 의 구성 및 복제

Redis는 기본적으로 모든 명령을 처리하는 Master , Master의 복제본인 Replica(Slave/clone), Master와 Slave의 구조를 확인하고 잘동작하고 있는 지 확인하는 Sentinel 이렇게 3개의 노드들이 존재한다. 

 

기본적인 Redis의 구조는 Master - Replica(Slave) - Replica(Slave) 구조로 아래와 같은 구성을 하고 있다.

##Slave란 용어는 Redis 5.0.0부터 Clone을 거쳐 Replica라는 단어로 바뀌었습니다. 과거 redis 버전을 구분하기위해서 과거버전은 slave라는 용어를 사용함 

 

Redis의 기본 구조 / 출처 : https://medium.com/opstree-technology/redis-cluster-architecture-replication-sharding-and-failover-86871e783ac0 

 

 

  • Master
    1. Redis에서 두뇌의 역할
    2. 모든 명령어(Read / Write 포함) 처리를 다함 
    3. Master가 failover상황시 Redis는 data loss가 발생할 수 밖에 없는 구조
  • Replica(Slave/Clone)
    1. Master의 복제본
    2. Read only로 data를 읽어갈 순 있음
    3. 대부분의 경우 read 역시 Master가 수행함 ( data consistency 문제가 생길 수 있기 때문에) 

우리는 이 때 Master와 Replica의 서로 어떻게 연결되어 있는가 살펴보려고한다.

 

일단 기본적으로 Master의 데이터를 Replica로 복사하는 방법은 두 가지가 존재한다.

  1. Fullsync (fsync)
  2. Partialsync (psync)

Fullsync와 Partialsync는 사용할 때가 다른데 Partialsync가 Fullsync에 비해서 우선순위를 갖게 되어있다. 

(psync가 fsnc보다 빠르고 Psync는 부분적으로 sync를 맞춰주면 되지만 fsync는 전체를 다 내리기 때문에 psync가 우선순위를 갖게 될 수 밖에 없다.)

 

Replication 과정을 잘 살펴보기 위해서는 src/replication.c를 살펴봐야한다.

실제로 replication 과정을 요청하는 부분은 syncCommand()에서 일어나는데 그 부분을 살펴보려고한다.

아래에 있는 코드는 syncCommand()일부를 가져온 것이다. 

void syncCommand(client *c) {
	...    
    if (!strcasecmp(c->argv[0]->ptr,"psync")) {
        if (masterTryPartialResynchronization(c) == C_OK) {
            server.stat_sync_partial_ok++;
            return; 
            /* psync가 성공적으로 했기 때문에 fsync는 필요없으니 return */
        } else { 
            char *master_replid = c->argv[1]->ptr;
            /*psync가 실패했으므로 err를 증가시키기고 fsync가 되게 해야함*/
            if (master_replid[0] != '?') server.stat_sync_partial_err++;
        }
    } else {
        /* replica가 psync을 요청하는 경우 Replconf ack 을 받지못하게 해야함*/
        c->flags |= CLIENT_PRE_PSYNC;
    }

    /* Full resync */
    server.stat_sync_full++;
	...
}

앞선 코드에서 masterTryPartialResynchronization() 함수는 바로 설명을 하겠지만 psync를 실제로 동작시키는 부분이다.

그래서 masterTryPartialResynchronization()함수로 잘 psync가 되었다면 return을 하게 되고 그렇지 못하게 된다면 이제 fsync가 될 수 있게 stat_sync_full의 값을 하나 올리게 된다.

 

fsync를 보기에 앞서 psync를 조금 더 자세히 보자

 

psync를 조금 더 자세히보려면 아까 말했던 masterTryPartialResynchronization() 이 함수를 깊게 봐야한다.

int masterTryPartialResynchronization(client *c) {
    long long psync_offset, psync_len; /* psync를 어디서부터 해야하는 알려주는 값 */
    char *master_replid = c->argv[1]->ptr; /* Master의 psync runid */
    char buf[128];
    int buflen;
    
    /*replica가 요청을 했지만 offset값을 통해 얻은 data가 깨졌는지 확인 */
    if (getLongLongFromObjectOrReply(c,c->argv[2],&psync_offset,NULL) != C_OK) 
    	goto need_full_resync;
    
    /* master의 id와 replica가 갖고있는 masterd의 id를 비교함 */
    /* replica 하나당 id를 최대 2개씩 갖고 있을 수 있음 (과거의 master id, 현재의 master id)*/
    /* 2번째 과거의 master id가 선택되었을 경우 특정한 offset값만 들고 있음*/
    /* 그것보다 psync가 큰지 확인이되면 fsync가 필요함 (data loss가 일어났다는 말이기 때문에) */
    if (strcasecmp(master_replid, server.replid) &&
        (strcasecmp(master_replid, server.replid2) ||
         psync_offset > server.second_replid_offset))
    {
        ...
        goto need_full_resync;
    }
    
    /* 네크워트 단절 시간이 길어져 마스터의 backlog-buffer가 넘치면 다시 연결되었을 때 fsync 필요 */
	if (!server.repl_backlog ||
        psync_offset < server.repl_backlog_off ||
        psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen))
    {
        ...
        goto need_full_resync;
    }
    
    /* psync가 일어나기 위한 조건들을 셋팅 해줘야함 */
    c->flags |= CLIENT_SLAVE;
    c->replstate = SLAVE_STATE_ONLINE;
    c->repl_ack_time = server.unixtime;
    c->repl_put_online_on_ack = 0;
    listAddNodeTail(server.slaves,c);
    
  	/* 실제로 psync 하는 부분*/
    if (c->slave_capa & SLAVE_CAPA_PSYNC2) {
        buflen = snprintf(buf,sizeof(buf),"+CONTINUE %s\r\n", server.replid);
    } else {
        buflen = snprintf(buf,sizeof(buf),"+CONTINUE\r\n");
    }
    if (connWrite(c->conn,buf,buflen) != buflen) {
        freeClientAsync(c);
        return C_OK;
    }
    
    /*backlog에 쓰여진 것들을 다시 가져오는 부분*/
    psync_len = addReplyReplicationBacklog(c,psync_offset);
    ...
    /*master의 활동영역을 침범하지 않았는지 확인*/
    refreshGoodSlavesCount();

    /* module event로 처리하는부분 */
    moduleFireServerEvent(REDISMODULE_EVENT_REPLICA_CHANGE,
                          REDISMODULE_SUBEVENT_REPLICA_CHANGE_ONLINE,
                          NULL);
                          
/* return C_OK가 되었다면 fsync는 필요없어짐 */
return C_OK; 

need_full_resync:
   
    return C_ERR;
}

여기서 중요한 부분은 backlog에 쓰여진 것들을 다시 가져오는 부분이다. 

master의 경우 데이터를 보내지만 네트워크 상의 문제로 잠깐 끊어졌을 경우 backlog_buf를 사용하여 Replica(slave/clone)에게 미처 전달되지 못한 데이터를 잠깐 보관하고 있다.

 

data loss가 일어나지 않는 replication을 원하다면 이 backlog_buffer를 확인해서 데이터의 유무를 확인해야한다.

addReplyReplicationBacklog() 함수는 하나의 파트를 잡고 설명해야하기에 이부분은 간략히 설명하고 차후에 글을 남기려고한다.

기본적으로 Redis의 Server와 Client의 관계를 알고있어야한다.

 

출처 : http://redisgate.kr/redis/server/bind.php

 위의 그림처럼 server와 client 모두 socket을 만들고 connect부분을 통해서 client와 server 모두 연결된다. 

이제 그러면 connect가 되는 시점에서 connSocketConnect() 함수가 불리고 그 함수 안에서는 server->repl_backlog를 client로 보내게된다. 그 후에 addReplySds()함수를 통해서 기존에 가져왔던 backlog를 확인하고 내부의 prepareClientToWrite()을 통해서 flag값들을 확인하게된다. flag를 다 체크를 하고 내부의 clientInstallWriteHandler()를 통해서 기존 계속해서 돌고있는 handleClientsWithPendingWrites()으로 writeToClient()를 실행시켜서 backlog에 있는 데이터들을 가져오는 과정을 하는 것이다.

 

그렇게 psync과정이 끝나거나 조건이 충족되지 않으면  fsync를 하는 방향으로 넘어가게 된다.. 

 

여기에서 Redis는 fsync를 할 경우를 3가지의 case로 나눠놨다. 

  1. 디스크를 사용하는 동기화
  2. 디스크를 사용하지 않고 소켓을 통한 동기화
  3. BGSAVE 없이 하는 동기화

그 전에 fsync가 되기 전에 해야하는 앞선 작업들이 있다.

(Replica의 상태변화나 backlog를 초기화를 시켜줘야 데이터의 충돌을 없앨수 있음)

void syncCommand(client *c) {
	...
    server.stat_sync_full++;
    
    /* Replica의 상태변경 */ 
    c->replstate = SLAVE_STATE_WAIT_BGSAVE_START;
    if (server.repl_disable_tcp_nodelay)
        connDisableTcpNoDelay(c->conn); 
    c->repldbfd = -1;
    c->flags |= CLIENT_SLAVE;
    listAddNodeTail(server.slaves,c);

    if (listLength(server.slaves) == 1 && server.repl_backlog == NULL) {
    	/* backlog를 처음부터 만들 경우 -> 데이터를 초기화 (data consistency) */
        changeReplicationId();
        clearReplicationId2();
        createReplicationBacklog();
 		...
    }
    ...
 }  

이렇게 앞선 준비들이 끝나게 되면 

case의 따른 실행이 나오게된다. 

void syncCommand(client *c) {
	...
        /* 디스크를 사용하는 경우 */
    if (server.rdb_child_pid != -1 && server.rdb_child_type == RDB_CHILD_TYPE_DISK)
    {
        ...
        if (ln && ((c->slave_capa & slave->slave_capa) == slave->slave_capa)) {
        	/*fsync하는 부분*/
            copyClientOutputBuffer(c,slave);
            replicationSetupSlaveForFullResync(c,slave->psync_initial_offset);
            ...
        } 
        ...

    /* CASE 2: 디스크를 사용하지 않고 socket을 사용하는 경우 */
    } else if (server.rdb_child_pid != -1 && server.rdb_child_type == RDB_CHILD_TYPE_SOCKET)
    {
        /* 다음 BGSAVE가 될때까지 기다려야함 */
        serverLog(LL_NOTICE,"Current BGSAVE has socket target. Waiting for next BGSAVE for SYNC");
    
    /* CASE 3: BGSAVE를 사용하지 않는 경우 */
    } else {
    	...
            if (!hasActiveChildProcess()) {
                startBgsaveForReplication(c->slave_capa);
            } 
            ...
        }
    }
    return;
}

이제 syncCommand함수가 return이 되면 replica 과정은 끝나게 된다. 

 

'Data Base > Redis' 카테고리의 다른 글

Redis에서 Replica가 느려도 되는 이유  (0) 2021.12.10
Redis - Master / Replica 통신 과정  (0) 2020.07.31
Redis - Server와 Client TCP/IP 통신  (0) 2020.07.28