싱글스레드 Redis가 수십만 QPS를 내는 구조적 비밀 | 멤버십 영상공개

⚡️ 핵심 요약

1. 싱글이라 빠른 게 아니다

Redis는 싱글 쓰레드'라서' 빠른 게 아니라, 싱글 쓰레드'여도' 빠르게 동작하도록 설계됐다. 인메모리·IO 멀티플렉싱·O(1) 자료구조·RESP·파이프라인이 진짜 이유이고, 락이 없어 대기도 없는 단순함이 더해진다.

2. 의도적 선택이었다

멀티스레드는 락·컨텍스트 스위칭·캐시 일관성·자원 경쟁이라는 비용을 동반한다. 게다가 Redis의 병목은 CPU가 아니라 메모리 대역폭·네트워크 IO였기에, 만든 사람 antirez가 단순함을 위해 싱글 쓰레드를 의도적으로 선택했다(불가능해서가 아니라).

3. '싱글'의 정확한 의미

'Redis는 싱글 쓰레드'라는 말은 명령 실행 경로가 싱글이라는 뜻이지 프로세스 전체가 싱글이라는 뜻이 아니다. 4.0의 bio 백그라운드 쓰레드, 6.0의 선택적 IO 쓰레드로 일부 멀티스레드를 쓰지만 명령 실행 자체는 지금도 싱글이다.

목차

1. 오해 깨기: 싱글 쓰레드여도 빠른 이유 (벤치마크)

"CPU 코어가 8개인데 1개만 쓰면 나머지는 노는 것 아닌가"라는 질문은 절반만 맞다. 멀티코어 활용이 곧 빠른 시스템이라는 가정이 Redis에는 통하지 않는다. 공식 벤치마크상 Redis의 초당 처리량은 수만~수백만 건, 평균 레이턴시는 1ms 이하다.

작업 파이프라인 미사용 파이프라인 사용
SET(랜덤 키) 초당 약 7만 건
SET(단순 키) 초당 약 18만 건 초당 약 153만 건
GET(조회) 쓰기보다 높음 초당 약 181만 건

Redis가 느려지는 대부분의 경우는 싱글 쓰레드여서가 아니라 연산이 무겁거나 너무 큰 데이터를 조작할 때다. 즉 싱글 쓰레드는 약점이 아니라, 빠르게 동작하도록 설계된 결과다.

⚠️ 벤치마크 수치는 환경·키 종류·파이프라인 여부에 따라 크게 달라진다. "싱글 쓰레드라서 빠르다"가 아니라 "싱글 쓰레드여도 빠를 수 있게 설계했다"가 정확한 표현이다 — 인과의 방향을 뒤집지 않는 것이 이해의 출발점이다.
▶️ 관련 스크립트

2. 멀티스레드의 비용과 싱글 쓰레드를 택한 이유

멀티스레드는 공짜가 아니다. 쓰레드가 여러 개면 공유 자원을 잡기 위한 락, 쓰레드 간 컨텍스트 스위칭(레지스터·스택·캐시 저장/복원), CPU 캐시 일관성 유지, 자원 경쟁 비용이 발생한다. 요청이 많을수록 이 오버헤드가 누적된다. 쓰레드 수에 비례해 처리량이 선형으로 늘어난다는 가정은 공유 자원이 없을 때만 성립한다.

Redis는 처음부터 공유 자원 경쟁이 없는 구조로 설계됐다 — 락이 없으면 대기도 없다. 제작자 antirez가 FAQ·블로그에서 밝힌, 멀티스레드가 가능했음에도 싱글을 택한 이유는 세 가지다.

이유 내용
① CPU는 병목이 아님 실제 병목은 메모리 대역폭·네트워크 IO. 코어를 더 써도 이 병목은 안 풀림
② 단순함이 최우선 멀티스레드 프로그래밍은 어렵고 같은 기능 개발이 느려짐. 락·데드락이 없다고 가정하면 코드가 근본적으로 단순해짐
③ 얻을 것보다 잃을 것 리스트 한 쪽에서 LPUSH, 다른 쪽에서 RPOP을 동기적으로 맞추려면 복잡한 조율 코드가 필요

그래서 Redis는 멀티스레드 대신 여러 인스턴스로 수평 확장하기를 권장한다.

⚠️ antirez는 "기술적으로 멀티스레드 구현이 불가능했던 것이 아니다"라고 분명히 했다. 핵심은 병렬화로 CPU 코어를 더 써도 메모리 대역폭·네트워크 IO라는 실제 병목은 해결되지 않으므로, 락이 만드는 복잡성·오버헤드만 떠안는다는 판단이다.
📝 적용 예시

락 오버헤드 비중 예시: GET 연산의 지연이 약 100ns 이하 → 멀티스레드 도입 시 뮤텍스 락 비용이 최대 50ns까지 추가 → 락으로 인한 오버헤드가 단일 연산 시간의 약 50%를 차지할 수 있음.

▶️ 관련 스크립트

3. 빠른 이유 ① 인메모리 — 디스크 IO 제거

첫 번째 이유는 인메모리 기반이라 디스크 IO를 아예 없앤 것이다. 전형적인 관계형 DB는 버퍼 풀(일부 성능용 메모리)이 있어도 기본적으로 디스크 IO가 발생하는 구조다. 반면 Redis는 모든 데이터의 조회·수정 접근이 무조건 RAM에서 일어난다.

저장소 접근 레이턴시 RAM 대비
RAM 50~100ns 기준
NVMe SSD(초고속) 20~200μs 최소 수백 배 느림
SATA SSD ~0.5ms 약 9,000배 느림
HDD 최대 10ms 최대 10만 배 느림

SSD가 아무리 빨라도 RAM과의 격차는 압도적이다 — NVMe가 가장 빠른 경우라도 RAM 100ns 대비 2만ns로 수백 배 차이가 난다. 디스크 기반이 아니어서 데이터 유실 리스크는 있지만, Redis는 RDB 스냅샷과 AOF 로그로 디스크에 영속 보관하는 메커니즘을 제공한다.

⚠️ 인메모리는 속도의 대가로 휘발성이라는 trade-off를 안는다. 그래서 RDB(스냅샷)와 AOF(append-only 로그)는 '성능을 위한 메모리'가 아니라 '유실 방지를 위한 영속화 장치'로, 역할이 버퍼 풀과 정반대임을 구분해야 한다.
📝 적용 예시

단위 환산으로 격차 체감하기: 1μs = 1000ns. NVMe SSD의 가장 빠른 지연 20μs → ns로 환산하면 20×1000 = 20,000ns. 메모리 100ns와 비교하면 약 200배 차이. SATA SSD는 RAM 대비 약 9,000배, HDD는 최대 100,000배 느림.

▶️ 관련 스크립트

4. 빠른 이유 ② IO 멀티플렉싱과 이벤트 루프

두 번째 이유는 IO 멀티플렉싱이다. 수만 개 클라이언트 연결 중 '준비된 소켓만' 골라 블로킹 없이 처리한다. Redis는 클라이언트 소켓(파일 디스크립터)을 커널에 등록하고, 리눅스 커널의 고성능 이벤트 알림 메커니즘인 epoll을 사용한다. 그리고 이벤트 루프를 돌려 커널이 "읽기/쓰기 준비됨"이라고 알려준 소켓만 즉시 처리한다.

핵심은 커널이 수만 개 FD를 대신 감시하므로 Redis는 소켓마다 별도 쓰레드를 두고 감시할 필요 없이, 단일 쓰레드의 이벤트 루프만으로 준비된 FD만 반환받아 처리한다. 이 논블로킹 IO + IO 멀티플렉싱은 1999년 댄 케겔이 제기한 C10K 문제(서버 하나로 1만 동시 커넥션 처리)의 해법이며, Node.js·Nginx·FastAPI도 같은 원리를 쓴다.

⚠️ "커넥션 1개당 쓰레드 1개" 블로킹 방식은 커넥션 수만큼 쓰레드를 할당해 메모리가 폭증한다. 반면 IO 멀티플렉싱은 감시 작업을 커널(epoll)에 위임해, 쓰레드를 늘리는 대신 '준비된 것만 골라 처리'하는 방식으로 단일 쓰레드가 수만 연결을 감당한다 — 이것이 동시성을 병렬성 없이 푸는 방법이다.
▶️ 관련 스크립트

5. 빠른 이유 ③ O(1) 자료구조 (해시 테이블·스킵리스트)

세 번째 이유는 키 조회의 시간 복잡도가 $O(1)$이라는 것이다. Redis의 키는 내부적으로 전역 해시 테이블로 탐색한다. 키의 해시값을 계산해 어떤 슬롯에 있는지 바로 점프하고, 버킷이 참조하는 실제 데이터를 즉시 찾는다 — 처음부터 스캔할 필요가 없다. 해시 충돌은 체이닝으로 해결한다.

전역 키 조회뿐 아니라 Redis 내부 타입들도 최적화된 자료구조로 구현돼 대부분의 연산이 상수 시간에 끝난다. 대표적으로 Sorted Set은 보통 쓰이는 레드-블랙 트리 대신 스킵리스트를 사용해, 성능은 비슷하게 유지하면서 구현은 더 단순하게 만들었다.

⚠️ antirez는 스킵리스트 선택에 대해 "성능 최적이 아니라 단순함을 선택했다"고 밝혔다. 이는 ②의 IO 설계, ④의 프로토콜 설계와 일관된 Redis의 철학으로 — 한계 성능을 짜내기보다 '충분히 빠르면서 단순한' 구조를 택하는 트레이드오프가 코드베이스 전반을 관통한다.
▶️ 관련 스크립트

6. 빠른 이유 ④ RESP 프로토콜 — 길이 프리픽스 파싱

네 번째 이유는 RESP(REdis Serialization Protocol), 즉 Redis가 클라이언트와 주고받는 통신 규약이다. 파싱 비용을 극단적으로 줄여 싱글 쓰레드에서도 초당 수십만~수백만 건을 처리하게 한다. 핵심은 "길이를 먼저 알려주면 스캔할 필요가 없다"이다.

방식 동작 문제/이점
일반 텍스트 프로토콜 쉼표·스페이스 같은 구분자가 나올 때까지 한 글자씩 스캔 데이터 크기를 미리 모름 → 버퍼 사전 할당 불가 → 메모리 낭비
RESP 인자 크기·명령 길이를 먼저 명시 ($5 → 뒤 5바이트) 스캔 불필요 + 버퍼 크기 사전 확정 → 파싱 루프 단순·오버헤드 감소

예컨대 $5를 읽으면 정확히 그 뒤 패킷이 5바이트임을 알 수 있어, 일일이 스캔하지 않고 버퍼를 미리 확정한다. 초당 100만 건을 처리하려면 명령 하나하나의 파싱 시간조차 줄여야 하며, RESP는 그 목적으로 설계된 통신 규약이다.

⚠️ 길이 프리픽스 설계의 본질은 '읽기 전에 크기를 안다'는 것이다. 구분자 스캔 방식은 끝을 모른 채 읽으며 파악해야 해 버퍼를 미리 잡지 못하지만, RESP는 길이를 먼저 받아 메모리 할당과 파싱을 한 번에 결정한다 — 이는 직렬화 프로토콜 설계의 일반 원리다.
▶️ 관련 스크립트

7. 빠른 이유 ⑤ 파이프라인과 배치 처리

다섯 번째 이유는 파이프라인이다. 여러 명령을 한 번에 묶어 전달하는 기능으로, 명령마다 응답을 주고받을 때 발생하는 네트워크 왕복 시간(RTT)을 제거한다. 파이프라인 없이는 명령→응답→명령→응답으로 RTT가 매번 추가되지만, 파이프라인은 여러 명령을 모아 한 번에 전달·실행하고 결과를 한 번에 반환한다. 공식 벤치마크상 파이프라인 미사용 시 초당 약 10만 건이 사용 시 약 180만 건으로 뛴다.

⚠️ 파이프라인의 이득은 연산 속도가 아니라 네트워크 왕복 횟수를 줄이는 데서 온다. 즉 명령 처리 자체는 이미 빠르므로, 다수 명령을 보낼 때 진짜 병목은 RTT이고 파이프라인은 그 왕복을 N회에서 1회로 줄이는 배치 전략이다.
📝 적용 예시

RTT 절감 예시: 네트워크 지연(RTT)이 1ms인 환경에서 명령 100개를 단건으로 전송 → 왕복 100회 → 100ms 이상 소요. 같은 100개를 파이프라인으로 묶어 한 번에 전송 → 왕복 1회 → 약 1ms로 단축.

▶️ 관련 스크립트

8. 멀티스레드는 어디에 쓰이나 (4.0 bio / 6.0 IO 쓰레드)

Redis는 사실 내부적으로 멀티스레드를 일부 지원한다. 단, 명령 실행은 여전히 싱글 쓰레드다.

버전 추가된 쓰레드 역할
초기 메인 쓰레드 1개 소켓 IO·파싱·실행·응답을 전부 처리
4.0 bio 백그라운드 쓰레드(bio.c) 비동기 삭제, AOF 파일 fsync 등 느린 디스크 작업을 분리 → 대량 삭제 시에도 메인 쓰레드 블로킹 없음
6.0 IO 쓰레드(N개, 선택 옵션) 네트워크 읽기·쓰기를 메인 쓰레드에서 분리

IO 쓰레드는 클라이언트가 많거나 응답 크기가 클 때 처리량을 30~60% 올릴 수 있다. 하지만 클라이언트가 적거나 명령이 가벼우면 오히려 쓰레드 컨텍스트 비용 때문에 느려질 수 있다. 그래서 'Redis는 싱글 쓰레드'라는 말의 정확한 의미는 명령 실행 경로가 싱글이라는 것이지, 프로세스 전체가 싱글이라는 뜻이 아니다.

⚠️ 멀티스레드가 도입된 영역은 명령 실행이 아니라 '주변부'(디스크 작업, 네트워크 IO)다. IO 쓰레드는 항상 이득이 아니라 워크로드 의존적이다 — 무거운 응답엔 도움이 되지만 가벼운 명령엔 컨텍스트 스위칭 비용이 이득을 넘어설 수 있어, 켜는 것 자체가 튜닝 판단이다.
📝 적용 예시

IO 쓰레드 설정 가이드(권장값): IO 쓰레드는 보통 CPU 코어 수보다 1~2개 작게 설정 → 8코어 서버라면 6 또는 7로 시작 → 실제 벤치마크로 최적값을 탐색.

▶️ 관련 스크립트

9. [마무리 정리] 5가지 이유와 Redis가 느려지는 경우

화자가 직접 정리한 '싱글 쓰레드인데 왜 빠른가'의 다섯 가지 이유:

# 이유 한 줄 요약
1 인메모리 RAM이 디스크보다 최대 10만 배 빠름
2 IO 멀티플렉싱 epoll로 준비된 소켓만 즉시 처리
3 $O(1)$ 자료구조 해시 테이블로 대부분 연산이 상수 시간
4 RESP 프로토콜 첫 바이트로 길이 결정, 스캔 불필요
5 파이프라인 명령을 묶어 RTT 비용 절감

여기에 싱글 쓰레드라 락이 없고, 락이 없어 대기도 없으며, 그래서 단순하다는 이점이 더해진다.

반대로 Redis가 느려지는 원인은 쓰레드 수가 아니라, ① 시간 복잡도 $O(N)$ 명령에서 N이 클 때(예: KEYS), ② 특정 키 하나의 값이 매우 커진 빅 키를 한 번에 불러와 연산할 때, ③ 핫 키 — 특정 키에 요청이 몰릴 때다. 싱글 쓰레드에서는 큰 N 연산이 다른 요청을 막으므로, 무거운 연산을 항상 조심해야 한다.

⚠️ 이 정리는 영상 전체의 복습 핵심이다. 느림의 진단을 '싱글 쓰레드'로 돌리는 흔한 오해를 바로잡는 것이 결론 — 원인은 쓰레드 수가 아니라 O(N) 연산·빅 키·핫 키이며, 이는 싱글 쓰레드 모델을 이해한 사람만이 올바로 대응할 수 있다.
▶️ 관련 스크립트

개발 가이드라인