[입 개발] Redis Master/Slave 연결이 계속 끊어집니다.

크, 오래간만에 블로그 포스팅을 다시 하게 되었습니다. 이제 Redis 관련 이슈들은 대부분 한 번씩 다뤄서, 남은건 현재 열심히 개발중인 Cluster 에 대해서 이야기를 하던지, 아니면 좀 더 내부 구조에 대해서 깊게 들어가야 할지 고민중입니다.

오늘의 주제는 “Redis Master/Slave의 연결이 계속 끊어집니다.”, 물론 다양한 이유로 접속이 끊어질 수 있습니다. Master가 죽었을 수도 있고, Slave가 죽었을 수도 있고, 하지만, 다음과 같은 이유가 가장 많을 것이라고 생각합니다.

그리고 점점 메모리 사이즈가 커지는 추세로 봐서 일어날 가능성이 높습니다. 일단 가장 큰 이유는 Redis 에는 클라이언트 종류별로(일반, 슬레이브, pubsub) Soft limit 와 Hard limit라는 것을 설정합니다. 그런 설정을 본적이 없으시다구요? 그럼 소스를 봐서 redis.conf를 열어서 끝부분을 보시길 바랍니다.

client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

요런게 있는데 각각 [type] [hard limit] [soft limit] [time] 입니다.

hard limit 은 이 값 이상 사용하게 되면 해당 클라이언트를 종료시킵니다.
soft limit 은 이 값 이상으로 time만큼 지속되면 종료시킵니다.

앞에 지시어가 client-output-buffer-limit 인걸로 봐서, 뭔가 client 에게 전달하려다 못하는 상황일 때에 대한 제한입니다. 그런데 이게, 네트웍 상황이 안좋거나, 또는 너무나 큰 데이터가 전달되어야 될 때, 넘칠 수가 있습니다. 즉 위에 “Slave”의 겨우 최대 256mb, 또는 64mb 로 60초가 지속되면 slave와의 접속을 끊어버립니다. 그런데 이 값에 대해서 변경하기 위해서는 고려해야할 것이 있습니다. 아, 그럼 무조건 증가시키면 좋게네라고 생각하고 hard limit 과 soft limit을 막 증가시키면, slave 가 많을 경우 어떤 일이 벌어질까요? 넵, 생각하신대로 서버의 메모리가 부족해서 종료하게 됩니다. 그래서, 실제로 모, 서버에서는 슬레이브가 32개 정도가 있는데, 8G 머신에 256MB 가까이 먹어서 4G를 넘어버린 어처구니 없는 일도 벌어진적이 있습니다. 그리고 한가지 더, 이 값이 또 너무 작아서 계속 접속이 끊어지면, 끊어진 슬레이브는 계속 마스터로 연결을 요청하고, 데이터 전달로 인해서, 마스터가 해야할 일을 못하고 느려지는 경우가 발생하게 됩니다. CPU 100%를 보실 수도 있습니다.

그럼 이 코드는 실제로 어디서 동작할까요?
networking.c 의 asyncCloseClientOnOutputBufferLimitReached() 함수에서 내부적으로 다시 checkClientOutputBufferLimits() 를 통해서 처리하게 됩니다. 코드는 간단합니다.

void asyncCloseClientOnOutputBufferLimitReached(redisClient *c) {
    redisAssert(c->reply_bytes < ULONG_MAX-(1024*64)); if (c->reply_bytes == 0 || c->flags & REDIS_CLOSE_ASAP) return;
    if (checkClientOutputBufferLimits(c)) {
        sds client = getClientInfoString(c);

        freeClientAsync(c);
        redisLog(REDIS_WARNING,"Client %s scheduled to be closed ASAP for overcoming of output buffer limits.", client);
        sdsfree(client);
    }
}

int checkClientOutputBufferLimits(redisClient *c) {
    int soft = 0, hard = 0, class;
    unsigned long used_mem = getClientOutputBufferMemoryUsage(c);

    class = getClientLimitClass(c);
    if (server.client_obuf_limits[class].hard_limit_bytes &&
        used_mem >= server.client_obuf_limits[class].hard_limit_bytes)
        hard = 1;
    if (server.client_obuf_limits[class].soft_limit_bytes &&
        used_mem >= server.client_obuf_limits[class].soft_limit_bytes)
        soft = 1;

    /* We need to check if the soft limit is reached continuously for the
     * specified amount of seconds. */
    /* We need to check if the soft limit is reached continuously for the
     * specified amount of seconds. */
    if (soft) {
        if (c->obuf_soft_limit_reached_time == 0) {
            c->obuf_soft_limit_reached_time = server.unixtime;
            soft = 0; /* First time we see the soft limit reached */
        } else {
            time_t elapsed = server.unixtime - c->obuf_soft_limit_reached_time;

            if (elapsed <= server.client_obuf_limits[class].soft_limit_seconds) { soft = 0; /* The client still did not reached the max number of seconds for the soft limit to be considered reached. */ } } } else { c->obuf_soft_limit_reached_time = 0;
    }
    return soft || hard;
}

마지막으로 다시 중요한 핵심만 다시 정리하자면, 다음과 같습니다.
1. client output buffer limit 이라는 게 존재해서 이 값에 따라서 컨넥션이 끊어질 수 있다.
2. 메모리가 큰 서버에서 가동 중이면 값을 증가시키는 것이 좋다.
3. 슬레이브가 너무 많은 경우는 반대로 값을 줄이는게 마스터 안전성에 도움이 된다.
4. 너무 접속이 빈번하게 끊어지면, 마스터에 부담이 가게 된다.(Sync 작업이 비쌉니다.)

다음 내용을 주의해야 합니다. 2.6.x 버전의 경우 telnet에서 “, ‘ 의 처리가 되지 않아서 config set client-output-buffer-limit 은 redis-cli에서 사용해야 합니다.(오늘 이것 때문에 고생을…)