Redis 6.0+ 멀티스레딩 — I-O 스레드와 메인 스레드
Redis는 싱글 스레드라서 빠르다고 하는데, 6.0부터 멀티스레딩을 지원한다면 그 장점을 포기한 건가요?
Redis 멀티스레딩이란
Redis 6.0에서 도입된 I/O 멀티스레딩은 네트워크 읽기/쓰기를 여러 스레드로 병렬 처리하는 기능입니다. 핵심은 명령 실행은 여전히 싱글 스레드라는 것입니다. 락이 필요 없는 단순한 실행 모델은 그대로 유지하면서, 네트워크 I/O 병목만 해결하는 것이 목표입니다.
왜 필요한가
Redis의 처리 과정을 단계별로 나눠보면 병목이 어디인지 보입니다.
1. 소켓에서 데이터 읽기 (read) ← 네트워크 I/O
2. 요청 파싱 (parse) ← CPU
3. 명령 실행 (execute) ← CPU (데이터 접근)
4. 응답 생성 (format) ← CPU
5. 소켓으로 데이터 쓰기 (write) ← 네트워크 I/O
단순한 GET/SET 명령에서는 1번과 5번(네트워크 I/O)이 전체 시간의 상당 부분을 차지합니다. 명령 실행 자체는 나노초 단위로 빠르기 때문입니다. 클라이언트가 수천 개 이상이면 이 I/O 처리가 싱글 스레드의 병목이 됩니다.
6.0 이전: 완전한 싱글 스레드
Main Thread:
read(client1) → parse → execute → write
read(client2) → parse → execute → write
read(client3) → parse → execute → write
...
(모든 작업을 한 스레드가 순차 처리)
이벤트 루프(epoll/kqueue)로 다중 클라이언트를 처리하지만, 실제 I/O 연산은 한 스레드에서 순차적으로 수행됩니다.
6.0 이후: I/O 멀티스레딩
I/O Thread 1: read(client1), read(client4), read(client7)...
I/O Thread 2: read(client2), read(client5), read(client8)...
I/O Thread 3: read(client3), read(client6), read(client9)...
Main Thread:
parse + execute(client1)
parse + execute(client2)
parse + execute(client3)
...
(명령 실행은 여전히 싱글 스레드)
I/O Thread 1: write(client1), write(client4)...
I/O Thread 2: write(client2), write(client5)...
I/O Thread 3: write(client3), write(client6)...
처리 흐름 상세
단계 1: [I/O 스레드들] 클라이언트 소켓에서 데이터 읽기 (병렬)
↓ (동기화 포인트)
단계 2: [메인 스레드] 요청 파싱 + 명령 실행 (순차)
↓ (동기화 포인트)
단계 3: [I/O 스레드들] 응답 데이터를 소켓에 쓰기 (병렬)
↓ (동기화 포인트)
단계 1로 반복
핵심 포인트: 각 단계 사이에 동기화 포인트가 있어서, 스레드 간 데이터 경쟁(race condition)이 발생하지 않습니다. 메인 스레드가 명령을 실행하는 동안 I/O 스레드는 대기하고, I/O 스레드가 읽기/쓰기하는 동안 메인 스레드가 대기합니다.
설정 방법
redis.conf
# I/O 스레드 수 (메인 스레드 포함)
# 기본값: 1 (멀티스레딩 비활성화)
io-threads 4
# I/O 스레드가 읽기(파싱)도 처리할지 여부
# 기본값: no (쓰기만 병렬 처리)
io-threads-do-reads yes
런타임 설정
# 런타임에는 변경 불가 — redis.conf에서만 설정
# Redis 재시작 필요
권장 설정
# 4코어 머신
io-threads 2 # 또는 3
# 8코어 머신
io-threads 4 # 최대 6
# 규칙: CPU 코어 수의 절반 이하
# 8 이상은 거의 효과 없음
싱글 스레드를 유지하는 이유
1. 락이 필요 없음
멀티스레드 명령 실행이라면:
Thread 1: INCR counter → read(100), 100+1=101, write(101)
Thread 2: INCR counter → read(100), 100+1=101, write(101)
→ 결과: 101 (올바른 결과는 102)
→ 락이 필요 → 성능 저하
싱글 스레드 명령 실행:
Main: INCR counter → read(100), write(101)
Main: INCR counter → read(101), write(102)
→ 결과: 102 (정확)
→ 락 불필요
2. 컨텍스트 스위칭 없음
스레드 간 전환 비용이 없으므로 마이크로초 단위의 명령 실행에서 유리합니다.
3. 원자성 보장
MULTI/EXEC, Lua 스크립트 등의 원자성이 자연스럽게 보장됩니다.
성능 향상 벤치마크
Redis 공식 벤치마크에서 I/O 멀티스레딩의 효과입니다.
io-threads 1 (싱글): ~100,000 ops/sec
io-threads 2: ~170,000 ops/sec (+70%)
io-threads 4: ~250,000 ops/sec (+150%)
io-threads 6: ~280,000 ops/sec (수확 체감)
실제 효과는 워크로드, 네트워크 환경, 명령 복잡도에 따라 다릅니다.
효과가 큰 경우
- 대량의 클라이언트 연결 (수천 개 이상)
- 작은 값의 대량 GET/SET
- 네트워크 대역폭이 충분하지만 I/O 처리가 병목인 경우
효과가 적은 경우
- 클라이언트가 적은 경우 (수십 개)
- 큰 값을 다루는 경우 (명령 실행 자체가 병목)
- 복잡한 명령 (SORT, ZUNIONSTORE 등)
- Lua 스크립트가 많은 경우
다른 백그라운드 스레드들
I/O 스레드 외에도 Redis는 이전부터 백그라운드 스레드를 사용하고 있었습니다.
메인 스레드: 명령 실행, 이벤트 루프
I/O 스레드: 네트워크 읽기/쓰기 (6.0+)
Bio 스레드 1: 파일 close() 처리
Bio 스레드 2: AOF fsync() 처리
Bio 스레드 3: Lazy free (UNLINK, 비동기 삭제)
Fork 프로세스: RDB 저장, AOF 리라이트
Lazy Free (4.0+)
큰 키를 삭제할 때 메인 스레드를 블로킹하지 않고 백그라운드에서 처리합니다.
# DEL 대신 UNLINK 사용
127.0.0.1:6379> UNLINK large-key # 백그라운드에서 삭제
# 자동 Lazy Free 설정
lazyfree-lazy-eviction yes # maxmemory eviction 시
lazyfree-lazy-expire yes # TTL 만료 삭제 시
lazyfree-lazy-server-del yes # RENAME 등 내부 삭제 시
lazyfree-lazy-user-del yes # DEL도 UNLINK처럼 동작 (Redis 6.0+)
모니터링
# I/O 스레드 활동 확인
127.0.0.1:6379> INFO stats
# io_threaded_reads_processed: I/O 스레드가 처리한 읽기 수
# io_threaded_writes_processed: I/O 스레드가 처리한 쓰기 수
# 현재 설정 확인
127.0.0.1:6379> CONFIG GET io-threads
1) "io-threads"
2) "4"
Thread-Safe 클라이언트 설정
I/O 멀티스레딩은 서버 측 변경이므로 클라이언트 코드 변경은 필요 없습니다. 하지만 최대 성능을 위해 클라이언트도 적절히 설정해야 합니다.
// Lettuce — 기본적으로 넌블로킹, 파이프라이닝 지원
// 별도 설정 없이 I/O 멀티스레딩의 효과를 누릴 수 있음
// 커넥션 풀은 불필요 (Lettuce는 단일 연결로 멀티플렉싱)
// 하지만 블로킹 명령이 있다면 풀 사용
LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
.poolConfig(new GenericObjectPoolConfig<>())
.build();
정리
- Redis 6.0의 멀티스레딩은 네트워크 I/O만 병렬화하고, 명령 실행은 싱글 스레드를 유지합니다
- 이를 통해 락 없는 단순한 실행 모델의 장점을 보존하면서 I/O 병목을 해결합니다
io-threads는 CPU 코어 수의 절반 이하로 설정하고, 보통 2~4가 적절합니다- 대량 클라이언트의 단순 명령(GET/SET)에서 가장 효과적이며, 복잡한 명령이 많으면 효과가 적습니다
- Lazy Free, Bio 스레드 등 이전부터 존재하던 백그라운드 스레드도 함께 이해하면 Redis의 전체 스레드 모델을 파악할 수 있습니다