A : Redis 왜 사용해요?
B : 인메모리여서 빠르고, 다양한 자료구조 제공....
A : 틀린 말은 아니지만...🥱
사실 여기서 B는 필자다.
누가 나에게 Redis를 왜 사용했냐고 물어보면 B와 똑같이 대답을 할 것 같다...
그래서! 이번 기회에 Redis에 대해 완벽히 공부를 하고자 Redis에 대한 사실과 오해를 작성하게 됐다.
+ 우아한 스터디😁
redis_version : 7.2.4
1. 인메모리 데이터베이스
Redis는 인메모리 데이터베이스입니다.
인메모리 데이터베이스의 특징을 알아보기 전에 디스크와 메모리에 대해 알아보겠습니다.
디스크 & 메모리
위 사진은 하나의 데이터 패킷이 출발지에서 도착지까지 가는 데 걸리는 시간을 보여주고 있습니다.
메모리는 컴퓨터의 RAM을 의미합니다. 사진에서는 100ns의 지연시간을 가지고 있다고 표현하고 있습니다.
RAM은 휘발성 메모리입니다. 즉 전원이 꺼지면 가지고 있던 데이터가 사라지는 메모리입니다.
(영속성을 위해 Redis의 데이터를 디스크에 저장하는 기능도 있습니다)
CPU와 직접적인 연결을 통해 빠른 데이터 처리를 가능하게 합니다.
디스크는 HDD와 SSD와 같은 저장 장치를 말하며, 데이터를 영구적으로 저장합니다.
디스크는 물리적인 읽기 / 쓰기 작업을 필요로 하기 때문에 메모리에 비해 지연시간이 큽니다.
사진에서는 SSD 100us, HDD 10ms의 지연시간을 가지고 있다고 표현하고 있습니다.
지연 시간이란?
쉽게 생각해 "데이터를 요청한 뒤 응답까지 걸리는 시간"이라고 생각하시면 됩니다.
메모리와 디스크의 지연 시간 비교
RAM <-> SSD (1,000배), RAM <-> HDD (100,000배)
이해는 했는데 왜 메모리는 휘발성, 빠르고 디스크는 영속성, 느려요?
이 개념을 이해하려면 디스크와 메모리의 저장 방식에 대해 알아야 합니다.
먼저 메모리는 데이터를 전자적으로 저장합니다. 즉 전기적인 신호를 이용해 데이터를 저장합니다.
이로 인해 속도는 매우 빠르지만 전원(전기)이 끊긴다면 데이터가 휘발되는 것입니다.
그에 비해 디스크는 물리적으로 저장을 합니다.
디스크의 한 종류인 HDD는 회전하는 디스크 위에 데이터를 기록하고, SSD는 플래시 메모리를 사용하여 데이터를 저장합니다.
이로 인해 속도는 느리지만 전원의 공급 여부와 상관없이 데이터가 영속적으로 저장됩니다.
Redis는 인메모리 데이터베이스이므로 빠른 처리가 가능합니다.
그리고 메모리의 특징인 휘발성도 가지고 있으나, Redis에서 지원하는 데이터 영속성 기능이 있습니다.
2. 다양한 자료형 & 명령어
Redis의 대표적인 자료형으로는 String, List, Hash, Set, SortedSet이 있습니다.
보조 자료형으로는 Bitmap, BitArray가 있으며 지리적 공간 인덱스도 있습니다.
또한 다양한 기능에 대한 명령어가 있습니다.
다음 편에서 자료형과 명령어에 대해 자세히 설명을 하기 때문에 간단하게 설명하겠습니다.
3. 백업을 통한 데이터 영속화
Redis는 In-Memory DB 즉 메모리 휘발될 수 있다.. 그런데 영속성?!
Redis는 AOF, RDB 백업 방식을 통해 데이터의 영속화를 할 수 있습니다.
1. AOF
AOF(Append Only File)은 서버에서 수신한 모든 쓰기 작업을 기록합니다.
그런 다음 서버 시작 시 기록된 작업을 통해 원래 데이터 세트를 재구성할 수 있습니다.
Redis의 aof.c 파일에 있는 aof 관련 코드를 보면서 설명하겠습니다.
1. 버퍼 초기화
sds buf = sdsempty();
명령어를 임시로 저장할 버퍼를 생성합니다.
2. 데이터베이스 ID 검증
serverAssert(dictid == -1 || (dictid >= 0 && dictid < server.dbnum));
명령이 실행되는 DB의 id가 유효한지 검사를 합니다.
3. 데이터베이스 선택 명령 추가
if (dictid != -1 && dictid != server.aof_selected_db) {
char seldb[64];
snprintf(seldb,sizeof(seldb),"%d",dictid);
buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
(unsigned long)strlen(seldb),seldb);
server.aof_selected_db = dictid;
}
현재 명령의 DB가 이전 명령의 DB와 다를 경우 SELECT 명령을 통해 데이터베이스를 전환합니다.
이를 통해 올바른 DB에서 명령을 실행할 수 있습니다.
DB가 다른 경우란?
Redis는 기본으로 생성된 DB가 16개 있습니다.
예를 들어 개발, 테스트, 프로덕션 환경이 있을 때 보통의 경우에는 DB를 분리합니다.
이때 DB를 분리해서 사용하는 것을 DB가 다른 경우라고 생각하시면 됩니다.
또는 다른 목적으로 사용하는 것도 DB가 다른 경우라고 생각할 수 있습니다.
4. 일반 명령 추가
buf = catAppendOnlyGenericCommand(buf,argc,argv);
주어진 명령('argv')를 버퍼에 추가합니다.
이때 catAppendOnlyGenericCommand 함수는 명령을 Redis 프로토콜 형식으로 변환하여 버퍼에 추가합니다.
5. 버퍼에 AOF 명령 추가
if (server.aof_state == AOF_ON ||
(server.aof_state == AOF_WAIT_REWRITE && server.child_type == CHILD_TYPE_AOF))
{
server.aof_buf = sdscatlen(server.aof_buf, buf, sdslen(buf));
}
AOF가 활성화 됐거나, AOF 재작성 중일 때 명령을 AOF 버퍼에 추가합니다.
이 버퍼는 클라이언트에게 응답을 보내기 전에 디스크에 플러시 됩니다.(이를 통해 영속성 보장을 하는 것)
6. 버퍼 해제
sdsfree(buf);
임시 버퍼를 해제하여 메모리를 반환합니다.
정리
AOF는 SET, DEL, LPUSH 등의 쓰기 명령이 실행될 때마다 호출되며,
함수 내에서 catAppendOnlyGenericCommand 함수를 통해 명령을 Redis 프로토콜로 변환하고,
이를 AOF 버퍼에 추가합니다. 이후 이벤트 루프를 재진입하기 직전에 디스크에 명령을 저장합니다.
AOF 전체 코드
void feedAppendOnlyFile(int dictid, robj **argv, int argc) {
sds buf = sdsempty();
serverAssert(dictid == -1 || (dictid >= 0 && dictid < server.dbnum));
/* Feed timestamp if needed */
if (server.aof_timestamp_enabled) {
sds ts = genAofTimestampAnnotationIfNeeded(0);
if (ts != NULL) {
buf = sdscatsds(buf, ts);
sdsfree(ts);
}
}
/* The DB this command was targeting is not the same as the last command
* we appended. To issue a SELECT command is needed. */
if (dictid != -1 && dictid != server.aof_selected_db) {
char seldb[64];
snprintf(seldb,sizeof(seldb),"%d",dictid);
buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
(unsigned long)strlen(seldb),seldb);
server.aof_selected_db = dictid;
}
/* All commands should be propagated the same way in AOF as in replication.
* No need for AOF-specific translation. */
buf = catAppendOnlyGenericCommand(buf,argc,argv);
/* Append to the AOF buffer. This will be flushed on disk just before
* of re-entering the event loop, so before the client will get a
* positive reply about the operation performed. */
if (server.aof_state == AOF_ON ||
(server.aof_state == AOF_WAIT_REWRITE && server.child_type == CHILD_TYPE_AOF))
{
server.aof_buf = sdscatlen(server.aof_buf, buf, sdslen(buf));
}
sdsfree(buf);
}
Q&A
A : 명령어가 실행될 때마다 저장된다고..?! 약간 좋은 기능 같긴 하지만 단점도 있을 것 같아
B : 맞아, AOF 방식은 모든 쓰기 작업을 로그 파일에 기록하기 때문에 쓰기 작업이 많은 경우 로그 파일의 커기가 매우 커지고, 데이터의 안정성을 보장하기 위해 fsync(동기화)를 자주 하게 된다면 성능 이슈가 생길 수 있어!
A : 아.. 그렇구나
B : 하지만, AOF 파일의 크기가 너무 커지면 백그라운드에서 자동으로 AOF 파일을 재작성해. 재작성을 하는 로직은 밑에서 자세히 설명할게
AOF 재작성
Redis 7.0 전까지는 하나의 파일에서 재작성을 했는데 7.0 이후로는 멀티파트 AOF 기능을 도입했습니다.
https://github.com/redis/redis/pull/9788
멀티파트 AOF 기능을 도입한 PR입니다.
멀티 파트 AOF에서 AOF 재작성 처리 과정은 다음과 같습니다.
- 요청 처리 중인 프로세스에서 AOF를 재작성하기 위해 자식 프로세스를 포크 하여 생성합니다.
- 부모 프로세스는 추가용 AOF 파일을 생성합니다.
- 자식 프로세스는 재작성 로직을 실행하여 새로운 베이스 AOF 파일을 생성합니다.
- 부모 프로세스가 새롭게 생성된 베이스 파일과 추가 파일의 정보를 임시로 생성된 메니패스트 파일에 업데이트합니다.
- 새로운 베이스 파일과 추가 파일이 준비되면 메니페스트 파일을 반영하기 위해 교체합니다.
- 오래된 베이스 파일과 추가 파일을 히스토리 파일로 변환하고 원래 파일을 삭제합니다.
메니페스트 파일은 컴퓨터에서 파일을 관리하기 위한 메타데이터를 포함하는 파일을 의미합니다.
이로 인해 AOF의 재작성으로 인해 메모리 소비나 입출력이 많아지는 문제가 해결되었습니다.
7.0 이전의 방식
7.0 이전의 방식에서는 임시파일을 AOF 파일로 변경(덮어쓰기)을 통해 재작성을 수행했습니다.
- 요청 중인 프로세스에서 AOF를 작성하기 위해 자식 프로세스를 포크 하여 생성합니다.
- 자식 프로세스는 새로운 AOF 파일을 생성한 후 재작성 결과를 저장합니다.
- 자식 프로세스가 새로운 AOF 파일의 재작성을 완료하면 부모 프로세스에 신호를 보냅니다.
- 신호를 받은 부모 프로세스는 포크 이후의 쓰기 작업 내용을 AOF 재작성 버퍼에 저장해 두었다가 이를 자식 프로세스에 전송합니다.
자식 프로세스는 이 데이터의 차이를 새롭게 생성된 AOF 파일에 반영합니다. - 오래된 AOF 파일을 새 AOF 파일을 교체합니다.
파일의 크기가 커지면 재작성을 한다고 했는데 특정 시점에 재작성을 하게 설정할 수 있어?
# Automatic rewrite of the append only file.
# Redis is able to automatically rewrite the log file implicitly calling
# BGREWRITEAOF when the AOF log size grows by the specified percentage.
#
# This is how it works: Redis remembers the size of the AOF file after the
# latest rewrite (if no rewrite has happened since the restart, the size of
# the AOF at startup is used).
#
# This base size is compared to the current size. If the current size is
# bigger than the specified percentage, the rewrite is triggered. Also
# you need to specify a minimal size for the AOF file to be rewritten, this
# is useful to avoid rewriting the AOF file even if the percentage increase
# is reached but it is still pretty small.
#
# Specify a percentage of zero in order to disable the automatic AOF
# rewrite feature.
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
auto-aof-rewrite-percentage를 통해 AOF 파일이 자동으로 재작성될 때 기준이 되는 비율을 지정합니다.
쉽게 설명하자면 이 값을 100으로 설정하면 현재 AOF 파일의 크기가 이전 AOF 파일의 크기보다 100% 증가했을 때 재작성을 합니다.
그리고 auto-aof-rewrite-min-size를 통해 재작성이 되는 파일의 크기를 설정할 수 있습니다.
현재 64mb로 설정되어 있는데 이 값으로 하면 AOF 파일의 크기가 최소 64mb를 넘어야 재작성을 할 수 있습니다.
AOF 재작성에 대한 사실과 오해
사실 Redis 5.0.0 이후의 버전을 사용하시는 분들은 이미 기본적으로 AOF와 RDB를 동시에 사용하고 계십니다.
https://github.com/redis/redis/blob/7.2/redis.conf#L1514~L1517
# Redis can create append-only base files in either RDB or AOF formats. Using
# the RDB format is always faster and more efficient, and disabling it is only
# supported for backward compatibility purposes.
aof-use-rdb-preamble yes
Redis는 RDB 또는 AOF 형식으로 append-only 기본 파일을 생성할 수 있습니다. RDB 형식을 사용하는 것이 항상 더 빠르고 효율적이며, 이를 비활성화하는 것은 오직 하위 호환성을 위해서만 지원됩니다.
5.0.0 이후 버전에서는 aof-use-rdb-preamble 옵션의 기본값이 yes입니다.
StackOverFlow에 올라온 글을 예시로 들어 설명하겠습니다.
https://stackoverflow.com/questions/73415771/is-redis-aof-file-a-mix-of-aof-and-rdb
SELECT 0
SET firstKey “I’m number one”
SET secondKey “I’m number two”
SET firstKey “I’m still number one”
글쓴이는 이렇게 Redis 명령어를 실행시킨 다음 BGREWRITEAOF 명령어를 실행시켰습니다.
AOF 재작성은 기존의 AOF 파일을 더 작고 효율적으로 만드는 과정이므로 SET firstKey “I’m number one” 명령어는 무시될 것으로 예상하였습니다.
BGREWRITEAOF는 AOF 재작성 기능에서 처리되는 명령어다.
하지만 appendonlyfile.aof를 본 결과 다음과 같았습니다.
파일의 앞에는 REDIS**** 문자가 있었으며 이는 rdb의 header와 동일했습니다.
이를 통해 aof-use-rdb-preamble라는 값을 사용자가 false로 변경하지 않는 이상 Redis에서는 rdb와 aof를 이용해 appendonly base 파일을 만들 수 있음을 알 수 있습니다. 이렇게 생성된 파일은 앞 5글자가 REDIS이므로 redis에서 파일을 읽을 때 스냅샷을 먼저 읽고 그다음에 aof 코드를 읽습니다.
이해가 잘 안 되시는 분들은 밑의 링크를 방문하시면 좋을 것 같습니다.
2. RDB
RDB는 AOF와 달리 특정한 시점의 스냅샷으로 백업하는 방식입니다.
쓰기 명령이 실행될 때마다 저장하는 AOF에 비해 데이터의 크기가 작다는 장점이 있지만, 만약 스냅샷을 저장하기 전에 Redis가 저장된다면 마지막으로 저장한 스냅샷 이후의 데이터는 사라지게 됩니다.
코드와 함께 살펴보겠습니다.
1. 함수 호출
int rdbSave(int req, char *filename, rdbSaveInfo *rsi, int rdbflags) {
RDB는 저장 요청, 파일 경로와 이름, 저장 정보 구조체, 저장 플래그를 인자로 받습니다.
2. 저장 프로세스 호출
startSaving(rdbflags);
rdbflags와 함께 저장 프로세스를 시작하는 startSaving 함수를 호출합니다.
3. 임시 파일 이름 생성
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
temp-{프로세스 id}.rdb 형식으로 임시 파일 이름을 생성합니다.
4. 디스크에 파일 저장
if (rdbSaveInternal(req,tmpfile,rsi,rdbflags) != C_OK) {
stopSaving(0);
return C_ERR;
}
rdbSaveInternal 함수를 통해 실제 디스크에 파일을 저장합니다.
만약 실패한다면 C_ERR를 반환하고 stopSaving()을 통해 저장 작업이 실패했다는 것을 나타내며 저장 프로세스를 중단합니다.
5. 임시 파일을 최종 파일로 이동
/* Use RENAME to make sure the DB file is changed atomically only if the generate DB file is ok. */
if (rename(tmpfile,filename) == -1) {
char *str_err = strerror(errno);
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Error moving temp DB file %s on the final "
"destination %s (in server root dir %s): %s",
tmpfile,
filename,
cwdp ? cwdp : "unknown",
str_err);
unlink(tmpfile);
stopSaving(0);
return C_ERR;
}
rename 함수를 통해 3번에서 생성했던 임시파일 이름을 인자로 받았던 filename으로 변환합니다.
이때 rename 함수에서 -1이 반환된 것은 오류가 발생한 것이므로 if 조건문 안의 내용이 실행됩니다.
조건문 안에서는 strerror 함수를 통해 에러를 문자열로 바꾸고, 로그에 함께 출력을 합니다.
그리고 임시 파일을 삭제하고 stopSaving(0)을 통해 저장 작업이 실패했다는 것을 나타내며 저장 프로세스를 중단합니다.
6. 파일 동기화
if (fsyncFileDir(filename) != 0) {
serverLog(LL_WARNING,
"Failed to fsync directory while saving DB: %s", strerror(errno));
stopSaving(0);
return C_ERR;
}
데이터 손실을 방지하기 위해 파일을 동기화시킵니다.
위에서 rdbSaveInternal을 통해 파일을 저장을 했으나, 캐시, 버퍼등에 임시 파일이 남아있을 수 있어 동기화를 시킵니다.
만약 오류가 발생하면 에러를 포함한 로그를 남기고 stopSaving(0)을 통해 저장 작업이 실패했다는 것을 나타내며 저장 프로세스를 중단합니다.
7. 성공 로그
serverLog(LL_NOTICE,"DB saved on disk");
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
stopSaving(1);
return C_OK;
마지막으로 성공적으로 디스크에 파일을 저장하면 성공했다는 로그를 남기고 stopSaving(1)을 통해 저장 작업이 성공적으로 완료 됐다고 나타냅니다.
정리
RDB는 특정한 시점마다 데이터 세트의 특정 시점 스냅 샷을 디스크에 저장합니다.
RDB 전체 코드
/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
int rdbSave(int req, char *filename, rdbSaveInfo *rsi, int rdbflags) {
char tmpfile[256];
char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
startSaving(rdbflags);
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
if (rdbSaveInternal(req,tmpfile,rsi,rdbflags) != C_OK) {
stopSaving(0);
return C_ERR;
}
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
if (rename(tmpfile,filename) == -1) {
char *str_err = strerror(errno);
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Error moving temp DB file %s on the final "
"destination %s (in server root dir %s): %s",
tmpfile,
filename,
cwdp ? cwdp : "unknown",
str_err);
unlink(tmpfile);
stopSaving(0);
return C_ERR;
}
if (fsyncFileDir(filename) != 0) {
serverLog(LL_WARNING,
"Failed to fsync directory while saving DB: %s", strerror(errno));
stopSaving(0);
return C_ERR;
}
serverLog(LL_NOTICE,"DB saved on disk");
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
stopSaving(1);
return C_OK;
}
Q&A
A : 만약에 어떤 프로젝트에 RDB 방식을 사용하고 있었고, 쓰기 작업이 일어난 후 1ms 뒤에 Redis가 다운 됐어. 그러면 파일은 저장 안 되지 않나..?!
B : 맞아. 아무래도 데이터 손실 가능성을 최소화해야 하는 프로젝트에서는 RDB보다 AOF가 적합한 것 같아. 하지만, 백업 용도로는 RDB가 더 적합한 방식인 것 같아. 예를 들어 지난 24시간 동안 매 시간마다 RDB 파일을 보관한다면 에러가 발생했을 때 데이터 집합의 다른 버전을 쉽게 복원할 수 있어.
필자의 의견
만약 몇 분의 데이터 손실을 감수할 수 있다면 RDB만 사용해도 됩니다.
하지만 데이터 손실을 감수할 수 없다면 보통의 경우 AOF만을 사용할 텐데 AOF 엔진에 버그가 발생했을 때를 대비해 AOF + RDB를 같이 사용하는 것을 추천합니다.
3. 싱글 스레드 & 부분적 멀티 스레드
I / O Theads
Redis는 싱글 스레드?
https://raw.githubusercontent.com/antirez/redis/6.0/00-RELEASENOTES
Redis 6의 새로운 기능들입니다.
여기서 어떤 것을 봐야할까요?
* Redis can now optionally use threads to handle I/O, allowing to serve 2 times as much operations per second in a single instance when pipelining cannot be used.
Redis는 이제 선택적으로 스레드를 사용하여 I/O를 처리할 수 있으며, 이를 통해 파이프라이닝을 사용할 수 없는 경우에도 단일 인스턴스에서 처리할 수 있는 작업 수를 2배로 늘릴 수 있습니다.
여전히 명령의 실행은 Single Thread입니다. 하지만 I/O를 스레드를 이용해 처리할 수 있으므로 Multi Thread라고 볼 수 있겠습니다.
이런 방식으로 운영을 하면 Single Thread를 통해 atomic을 보장할 수 있으며, Multi Thead를 통해 더 빠른 I/O를 할 수 있게 됩니다.
# Redis is mostly single threaded, however there are certain threaded
# operations such as UNLINK, slow I/O accesses and other things that are
# performed on side threads.
#
# Now it is also possible to handle Redis clients socket reads and writes
# in different I/O threads. Since especially writing is so slow, normally
# Redis users use pipelining in order to speed up the Redis performances per
# core, and spawn multiple instances in order to scale more. Using I/O
# threads it is possible to easily speedup two times Redis without resorting
# to pipelining nor sharding of the instance.
#
# By default threading is disabled, we suggest enabling it only in machines
# that have at least 4 or more cores, leaving at least one spare core.
# Using more than 8 threads is unlikely to help much. We also recommend using
# threaded I/O only if you actually have performance problems, with Redis
# instances being able to use a quite big percentage of CPU time, otherwise
# there is no point in using this feature.
#
# So for instance if you have a four cores boxes, try to use 2 or 3 I/O
# threads, if you have a 8 cores, try to use 6 threads. In order to
# enable I/O threads use the following configuration directive:
#
# io-threads 4
#
# Setting io-threads to 1 will just use the main thread as usual.
# When I/O threads are enabled, we only use threads for writes, that is
# to thread the write(2) syscall and transfer the client buffers to the
# socket. However it is also possible to enable threading of reads and
# protocol parsing using the following configuration directive, by setting
# it to yes:
#
# io-threads-do-reads no
#
# Usually threading reads doesn't help much.
#
# NOTE 1: This configuration directive cannot be changed at runtime via
# CONFIG SET. Also, this feature currently does not work when SSL is
# enabled.
#
# NOTE 2: If you want to test the Redis speedup using redis-benchmark, make
# sure you also run the benchmark itself in threaded mode, using the
# --threads option to match the number of Redis threads, otherwise you'll not
# be able to notice the improvements.
Redis는 대부분 단일 스레드로 작동하지만, UNLINK와 같은 일부 스레드 작업, 느린 I/O 접근 등은 별도의 스레드에서 수행됩니다.
이제 Redis 클라이언트의 소켓 읽기와 쓰기를 다른 I/O 스레드에서 처리하는 것도 가능합니다. 특히 쓰기가 매우 느리기 때문에, 일반적으로 Redis 사용자들은 Redis의 성능을 향상하기 위해 파이프라이닝을 사용하고, 더 많이 확장하기 위해 여러 인스턴스를 생성합니다. I/O 스레드를 사용하면 파이프라이닝이나 샤딩에 의존하지 않고도 Redis를 쉽게 두 배 빠르게 만들 수 있습니다.
기본적으로 스레딩은 비활성화되어 있으며, 우리는 적어도 4개 이상의 코어를 가진 기계에서만 활성화하는 것을 권장합니다. 8개 이상의 스레드를 사용하는 것은 크게 도움이 되지 않을 것입니다. 또한 Redis 인스턴스가 CPU 시간의 상당 부분을 사용할 수 있을 때만 스레드 I/O를 사용하는 것을 권장합니다. 그렇지 않으면 이 기능을 사용할 이유가 없습니다.
예를 들어, 4개의 코어를 가진 박스가 있다면 2개 또는 3개의 I/O 스레드를 사용해 보세요. 8개의 코어가 있다면 6개의 스레드를 사용해 보세요. I/O 스레드를 활성화하려면 다음의 설정 지시사항을 사용하세요:
io-threads 4
io-threads를 1로 설정하면 평소처럼 메인 스레드만 사용합니다. I/O 스레드가 활성화되면, 우리는 쓰기를 위한 스레드만 사용합니다. 즉, write(2) 시스템 호출을 스레드화하고 클라이언트 버퍼를 소켓으로 전송합니다. 그러나 읽기와 프로토콜 파싱의 스레딩도 가능하며, 다음의 설정 지시사항을 사용하여 yes로 설정할 수 있습니다:
io-threads-do-reads no
일반적으로 읽기 스레딩은 크게 도움이 되지 않습니다.
참고 1: 이 설정 지시사항은 CONFIG SET을 통해 런타임에 변경할 수 없습니다. 또한, 이 기능은 현재 SSL이 활성화되어 있을 때 작동하지 않습니다.
참고 2: redis-benchmark를 사용하여 Redis 속도를 테스트하려면, 반드시 벤치마크 자체도 스레드 모드에서 실행해야 합니다. --threads 옵션을 사용하여 Redis 스레드의 수를 맞추세요. 그렇지 않으면 개선 사항을 알아차릴 수 없습니다.
redis.conf의 io-threads 설정을 통해 I/O 스레드의 개수를 설정할 수 있습니다.
Multiplex
https://medium.com/@tiffany1101/why-is-redis-so-fast-5f21fcbcbeff
Medium 글의 예시를 인용해 설명하겠습니다. (단언컨대 이 글보다 Multiplexing에 대해 쉽게 설명한 글은 없을 것입니다)
점원 (단일 스레드)
손님 (소켓 연결)
주문 (Redis의 작업)
주문 목록(Redis의 작업 대기열)
바리스타 (Redis의 작업이나 요청을 실행하는 스레드, 쿼리 스레드)
스타벅스에 한 명의 점원이 있다고 가정하겠습니다.
이 점원은 손님이 없을 때는 잠시 낮잠을 잡니다. 그러다 손님이 가게에 들어오면 즉시 깨어나서 손님을 응대합니다.
당신은 손님에게 "자바칩 프라푸치노 한잔 주세요"라고 주문을 합니다.
그러면 점원은 주문을 받은 뒤 주문 목록에 당신이 주문한 자바칩 프라푸치노를 기록합니다.
뒤에서는 바리스타가 주문 목록에 맞춰 커피를 제조하고 있습니다.
바리스타가 작업을 마친 뒤 점원에게 커피가 준비되었다고 말합니다. 이후, 점원은 커피를 건네받아 당신에게 건네줍니다.
하지만 Redis의 세계에서는 차이점이 있습니다.
동적으로 바리스타를 고용 (Redis가 여러 작업을 병렬로 처리하기 위해 쿼리 스레드를 동적으로 생성할 수 있는 능력을 반영합니다)
이때 현실 세계와 다르게 Redis 세계에서는 점원이 동적으로 바리스타를 추가할 수 있습니다.
만약 손님이 많이 몰리게 된다면 점원은 바리스타를 추가해 커피를 만들게 할 수 있습니다.
이러한 동적 고용을 통해 점원은 이전 손님의 커피 주문이 완료될 때까지 기다리지 않고 다음 손님을 바로 응대합니다.
동적으로 바리스타를 고용하므로 점원은 막히지 않고 주문을 효율적으로 받습니다.
이를 "Non-Blocking I/O"라고 합니다.
바리스타가 여러 손님을 맞이하고 응대하는 것처럼 여러 소켓을 단일 스레드가 효율적으로 모니터링하고 완료된 작업을 한번에 알려주는 것을 " I/O Multiplex"라고 합니다.
정리
정리를 하자면 Redis는 I/O에서는 멀티 스레드를 사용하고 명령 처리에서는 싱글 스레드를 사용합니다.
이때 싱글 스레드에서 Multiplex를 사용해 명령을 효율적으로 처리를 할 수 있습니다.
전체적인 흐름을 설명한 그림입니다.
참고 문서
- https://www.linkedin.com/pulse/why-heck-single-threaded-redis-lightning-fast-beyond-in-memory-kapur/
- http://www.redisgate.kr/redis/configuration/redis_thread.php
- https://medium.com/@tiffany1101/why-is-redis-so-fast-5f21fcbcbeff
- https://charsyam.wordpress.com/2020/05/05/%EC%9E%85-%EA%B0%9C%EB%B0%9C-redis-6-0-threadedio%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90/
- https://github.com/redis/redis
- https://m.yes24.com/Goods/Detail/126528836
- https://stackoverflow.com/questions/73415771/is-redis-aof-file-a-mix-of-aof-and-rdb
- https://medium.com/redis-with-raphael-de-lio/understanding-persistence-in-redis-aof-rdb-on-docker-dcc176ea439
다음 글 - 다양한 시각에서 바라본 Redis (2) 비교
memcached, mysql 등 사람들이 redis를 설명할 때마다 가만히 놔두지 않는 친구들을 소개하고 비교해보려고 합니다...