Is Redis Single-Threaded? How Does It Handle Millions of Requests Per Second?
Yes, Redis is primarily single-threaded, but it can still handle millions of requests per second due to its highly optimized design.
blog.stackademic.com
오랜만에 재밌는 글을 가져왔습니다.
Redis는 굉장히 자주 사용되며, 특유의 원자성과 빠른 처리량으로 사랑받는 In-Memory DB 입니다.
원자성을 보장받는 이유는 Singlne Thread이기 때문인데요 "아무리 RAM 에 올라간다 하더라도 어떻게 수백만건의 요청을 처리하지?" 라는 의문을 한번쯤은 가져봤을 것 입니다.
개인적으로 "I/O는 멀티스레드를 써서 빠르다더라" 정도만 알고있었는데요, 이 글이 딥하게 알아볼 수 있는 계기가 되었습니다.
글의 내용
위 글에서 말하는 내용은 다음과 같습니다.
- Redis는 모든 것을 RAM에 저장하므로 느린 디스크 I/O 를 방지
- HashMaps, SortedSets, HyperLogLogs 등 최적화된 데이터 구조 사용
- 고성능 I/O Multiplexing 사용
- Redis 6.0 이상부터 I/O 처리가 멀티스레드 방식으로 변경
이 글을 읽으면서 햇갈렸던 개념은 I/O Multiplexing 인데요, "성능이 좋아진단건 알겠는데 그래서 그게 뭐지?" 라는 생각이 들어 자세히 찾아봤습니다.
Multiplexing (다중화)란 무엇인가?
Multiplexing은 간단히 말해 하나를 여러 개처럼 보이게 하는 기법이며, Redis는 버전과 상관없이 항상 I/O Multiplexing 기반으로 동작합니다.
운영체제 관점에서는, 하나의 프로세스가 여러 파일(혹은 소켓)을 동시에 관리할 수 있도록 하는 방법을 의미합니다.
리눅스에서 파일(File)은 프로세스가 커널에 접근할 수 있게 해주는 인터페이스이며, 네트워크 환경에서는 파일 = 소켓으로 대응됩니다. 즉, 하나의 서버가 여러 개의 소켓을 관리하여 동시에 많은 클라이언트를 처리할 수 있는 구조가 됩니다.
또 프로세스가 특정 파일에 접근할 때는 파일 디스크립터(File Descriptor, FD) 라는 정수 값을 사용하는데요
이 FD는 커널 내부의 파일 객체(소켓)를 가리키며, I/O Multiplexing은 FD의 상태 변화(readable/writable)를 효율적으로 감시하는 데 초점이 맞춰져 있습니다.
리눅스에서의 I/O Multiplexing: epoll
리눅스는 I/O Multiplexing을 위해 여러 메커니즘을 제공해왔습니다.
초기에는 select, poll을 사용했지만, 이들은 FD 개수에 비례하는 오버헤드가 있어 수천~수만 연결을 처리하기엔 비효율적이었습니다.
이를 해결한 것이 epoll입니다. epoll은 다음과 같이 동작합니다.
- 애플리케이션이 epoll_ctl()을 통해 소켓을 등록하고, “이 소켓이 읽기/쓰기 가능해지면 알려달라”고 요청
- 이벤트 루프 스레드는 epoll_wait()을 호출해 대기
- 커널은 수천 개 소켓 중 실제로 준비된 것만 골라 이벤트 루프에 전달
즉, 준비된 소켓 목록만 반환하기 때문에, Redis는 수만 개 연결 중 필요한 소켓만 빠르게 처리할 수 있습니다.
Redis의 이벤트 루프 처리 흐름
Redis는 다음과 같은 순서로 이벤트(사용자 요청)를 처리합니다.
- 소켓을 논블로킹 모드로 오픈
- 일반적으로 소켓은 Blocking 모드
- 소켓 수신 버퍼에 데이터가 없으면 -> 스레드가 멈춘 채 데이터 도착할 때까지 기다림
- 송신 버퍼 가득 차면 -> 버퍼 여유 생길 때까지 기다림
- Non Blocking 모드
- 소켓에 데이터가 있으면 즉시 읽고 반환, 데이터가 없으면 에러코드 EAGAIN 반환
- 송신 버퍼에 들어가는 만큼만 쓰고, 가득 차면 에러코드 EAGAIN 반환
- 즉 준비된 만큼만 처리하고, 나머지는 다음 이벤트에서 처리
- I/O Multiplexing 등록
- Redis 이벤트 루프는 서버 시작 시 OS별로 다중화 방식 선택, Linux(epoll)
- 논블로킹 소켓의 준비됨(readable/writable) 상태를 커널이 모아 이벤트 루프 스레드에 알려줌
- 이벤트 루프 = 스레드가 발생한 이벤트를 지속적으로 감지하고, 콜백을 실행하는구조
- 소켓에 읽을 게 있으면 REDABLE, 소켓에 쓸 게 있으면 WRITABLE
- 이벤트 루프 실행
- 메인 스레드는 준비된 소켓 목록을 받음
- 준비된 소켓마다 등록된 콜백을 실행
- READABLE - 요청 읽기 + 명령 실행
- WRITABLE - 응답 전송
이 과정을 통해 단일 스레드 이벤트 루프만으로도 수천~수만 개 동시 연결을 처리할 수 있으며, 락(lock)도 필요하지 않습니다.
6.0 이전에는위 과정 전체(읽기, 명령 실행, 응답 전송)를 메인 이벤트 루프 단일 스레드가 처리했으며
6.0 이후에는 I/O 전용 스레드를 도입하여 read/write 작업을 여러 스레드로 분산 처리를 하도록 변경됐습니다.
(명령 실행 자체는 여전히 단일 스레드에서 순차적으로 처리됩니다.)
Blocking vs Non-blocking 소켓
여기까지 정리한 후 한가지 의문이 생겼는데요
“어차피 epoll이 준비된 소켓만 알려주면, 소켓을 블로킹 모드로 열어도 괜찮지 않을까?” 라는 생각이었습니다.
하지만 준비된 소켓이라고 해서 read/write가 항상 성공하는 것은 아닙니다.
예를 들어, 수신 버퍼 크기보다 더 많은 데이터를 read() 하거나, 송신 버퍼에 여유가 있음에도 write() 하려는 데이터가 너무 많다면, 버퍼가 다시 준비될 때까지 스레드가 블로킹될 수 있습니다.
따라서 소켓은 논블로킹 모드로 열어주는게 적절하겠네요.
'Article' 카테고리의 다른 글
| [Medium]왜 대부분의 유닛 테스트는 쓸모없는가? (0) | 2025.09.11 |
|---|---|
| [Medium] 초보처럼 if-else 쓰지 않는 법 (0) | 2025.09.07 |
| [Medium] 동기·비동기, IO·NIO, 그리고 Virtual Thread (1) | 2025.09.01 |
| [Medium] 왜 Map은 Iterable이 아닐까? (0) | 2025.08.30 |
| [Medium] 왜 자바 Stream은 대규모 환경에 적합하지 않은가? (1) | 2025.08.29 |
