최근에 지인에게서 문의가 왔습니다. 뭔가 특별히 한게 없는데, Redis 서버가 죽어버리고 재시작했다고, 이것저것 알아보니, 일단 메모리를 많이 사용하고 있던 상태였습니다. 웬지 느낌은, 아주 전통적인 Redis가 데이터 저장을 위해서 fork하면서 메모리를 추가로 사용하고, 이걸로 인해서 OOM등이 발생한게 아닐까라고 생각을 했는데…
지인의 얘기로는 다음과 같은 –rdb 명령을 사용했다고 합니다.
redis-cli --rdb
일단 저도 한번도 해당 옵션을 써본적이 없어서 소스를 확인해보니 다음과 같습니다. 해당 소스는 redis-cli.c를 보시면 됩니다.
} else if (!strcmp(argv[i],"--rdb") && !lastarg) { config.getrdb_mode = 1; config.rdb_filename = argv[++i]; }
getrdb_mode라는 옵션을 셋팅합니다. 해당 부분을 다시 찾아보면 getrdb_mode가 켜져있을 때 sendCapa()라는 함수와 getRDB를 호출합니다.
/* Get RDB mode. */ if (config.getrdb_mode) { if (cliConnect(0) == REDIS_ERR) exit(1); sendCapa(); getRDB(NULL); }
sendCapa를 호출해서 eof를 설정하면 diskless replication을 시도합니다. 여기서 주의할 것은 diskless replication은 서버에 RDB파일을 만들지 않을뿐, fork를 해야하는 것은 동일합니다. 그리고 이 부분이 문제를 일으키게 됩니다. 다만 여기서는 실제 RDB를 만들지 않고 해당 클라이언트에게는 diskless replication을 하라고 설정만 하는 단계입니다.(다만 서버가 repl_diskless_sync를 켜두지 않으면 만구땡…)
void sendCapa() { sendReplconf("capa", "eof"); } void sendReplconf(const char* arg1, const char* arg2) { printf("sending REPLCONF %s %s\n", arg1, arg2); redisReply *reply = redisCommand(context, "REPLCONF %s %s", arg1, arg2); /* Handle any error conditions */ if(reply == NULL) { fprintf(stderr, "\nI/O error\n"); exit(1); } else if(reply->type == REDIS_REPLY_ERROR) { fprintf(stderr, "REPLCONF %s error: %s\n", arg1, reply->str); /* non fatal, old versions may not support it */ } freeReplyObject(reply); }
getRDB 함수는 이제 실제로 SYNC 명령을 보내서 replication을 수행합니다. 원래의 Replication은 그 뒤로 계속 master로 부터 데이터를 받아야 하지만, 여기서 getRDB는 그냥 현재 메모리 상태의 스냅샷만 전달하고 종료하게 됩니다.
코드를 보면 sendSync 후에 payload를 가져오게 되는데, 해당 payload는 RDB생성시 diskless_sync 방식이 아닐경우에만 파일 사이즈를 전달해줘서 가져올 수 있고, diskless_sync 방식을 쓸때는 eof_mark 라는 40바이트의 랜덤값을 처음에 전달해주고, 마지막에 해당 값이 나오면 RDB생성을 종료하게 됩니다.
static void getRDB(clusterManagerNode *node) { int s, fd; char *filename; if (node != NULL) { assert(node->context); s = node->context->fd; filename = clusterManagerGetNodeRDBFilename(node); } else { s = context->fd; filename = config.rdb_filename; } static char eofmark[RDB_EOF_MARK_SIZE]; static char lastbytes[RDB_EOF_MARK_SIZE]; static int usemark = 0; unsigned long long payload = sendSync(s, eofmark); char buf[4096]; if (payload == 0) { payload = ULLONG_MAX; memset(lastbytes,0,RDB_EOF_MARK_SIZE); usemark = 1; fprintf(stderr,"SYNC sent to master, writing bytes of bulk transfer " "until EOF marker to '%s'\n", filename); } else { fprintf(stderr,"SYNC sent to master, writing %llu bytes to '%s'\n", payload, filename); } /* Write to file. */ if (!strcmp(filename,"-")) { fd = STDOUT_FILENO; } else { fd = open(filename, O_CREAT|O_WRONLY, 0644); if (fd == -1) { fprintf(stderr, "Error opening '%s': %s\n", filename, strerror(errno)); exit(1); } } while(payload) { ssize_t nread, nwritten; nread = read(s,buf,(payload > sizeof(buf)) ? sizeof(buf) : payload); if (nread = RDB_EOF_MARK_SIZE) { memcpy(lastbytes,buf+nread-RDB_EOF_MARK_SIZE,RDB_EOF_MARK_SIZE); } else { int rem = RDB_EOF_MARK_SIZE-nread; memmove(lastbytes,lastbytes+nread,rem); memcpy(lastbytes+rem,buf,nread); } if (memcmp(lastbytes,eofmark,RDB_EOF_MARK_SIZE) == 0) break; } } if (usemark) { payload = ULLONG_MAX - payload - RDB_EOF_MARK_SIZE; if (ftruncate(fd, payload) == -1) fprintf(stderr,"ftruncate failed: %s.\n", strerror(errno)); fprintf(stderr,"Transfer finished with success after %llu bytes\n", payload); } else { fprintf(stderr,"Transfer finished with success.\n"); } close(s); /* Close the file descriptor ASAP as fsync() may take time. */ fsync(fd); close(fd); fprintf(stderr,"Transfer finished with success.\n"); if (node) { sdsfree(filename); return; } exit(0); }
다시 정리하자면 redis-cli에서 –RDB 옵션을 주면 서버에 diskless_sync를 요청합니다.(서버가 설정이 되어있어야만 동작합니다.) 그러나 해당 작업은 Redis 서버를 fork하게 만들기 때문에, 메모리 사용량이 높고 Write가 많은 상황에서는 절대적으로 피하시는게 좋습니다.