CS 지식/데이터베이스

Redis는 싱글스레드인데 어떻게 초당 수십만건의 요청을 처리할까? (feat. epoll)

마늘냄새폴폴 2026. 4. 22. 23:44

안녕하세요! 이번 포스팅은 Redis에 대한 포스팅으로 찾아뵙게 되었습니다. 

 

Redis는 업계 표준이라고 해도 될 정도로 많은 기업, 많은 프로젝트에서 사용되고 있는데요. 아무래도 캐싱, 메세지 큐, 실시간 데이터 처리 등 다양한 방면으로 사용되고 HA 방법론도 여러가지 제공해주고 커뮤니티도 많이 형성되어있어서 인기가 좋은 것 같습니다. 

 

흔히 Redis와 Memcached의 차이를 면접 때 물어보면 이런 대답이 100퍼센트 나옵니다. 

 

"Redis는 싱글 스레드이고 Memcached는 멀티 스레드여서 Redis가 성능상 이점이 있습니다."

 

그럼 면접관이 다시 "싱글 스레드는 처리량이 안좋은거 아닌가요?"

 

저도 신입 때 이 둘의 차이에 대해서 공부했지만 한번도 이런 생각까지 사고가 확장되진 않았던 것 같습니다. 

 

"그러게.. Redis가 싱글 스레드인데 어째서 Memcached 보다 성능이 좋다고 말하는걸까..?"

 

이번 포스팅에선 아래와 같은 절차를 통해 이 궁금증을 해결해 보겠습니다. 

 

  1. 전통적인 I/O
  2. 멀티플렉싱 I/O
  3. Redis와 epoll
  4. epoll의 다양한 사례

그럼 본격적으로 시작해보죠!

 

전통적인 I/O

Blocking I/O

전통적인 I/O에서 가장 먼저 등장한 I/O가 바로 Blocking I/O입니다. Blocking I/O의 경우 천개의 요청이 들어오면 천개의 스레드를 만들어서 동시다발적으로 처리하는 방법론을 사용합니다. 

 

이 방식은 스레드를 생성하는 비용이 어마어마했고 스레드를 무한정 많이 만들 수 없어서 동시에 처리할 수 있는 요청이 정해져있다는 문제가 있었습니다. 또한, 엄청나게 많은 스레드간 컨텍스트 스위칭 비용도 무시할 수 없는 수준이었죠. 

 

전통적인 웹서버인 Tomcat이 Blocking I/O를 사용해서 처리를 했었는데 과거에는 Tomcat도 문제없이 동작을 했습니다. 그때는 웹이 이렇게까지 큰 규모가 아니었기에 적당한 요청이 들어왔고 적당한 처리를 진행하면 됐습니다. 

 

하지만 이후 동시 요청이 100만개가 넘어가는 순간 Tomcat은 더이상 사용할 수 없는 지경에 이르렀습니다. 그러면서 Nginx가 등장했는데 이 이야기는 조금 뒤에 마저 풀도록 하겠습니다. 

 

Non-Blocking I/O

Non Blocking I/O는 싱글스레드가 각 연결마다 돌아다니면서 요청이 있는지 확인하는 방식이었습니다. 이 방식의 장점은 많은 스레드간 컨텍스트 스위칭 비용이 없어졌고 스레드 생성 비용에 대한 오버헤드가 사라졌습니다. 

 

하지만 Non Blocking I/O도 문제가 없던 것은 아닌데 스레드가 모든 연결을 돌아다녀야했기 때문에 요청이 없는 연결도 돌아다녀야했고 이로인해 의미없는 CPU 사용이 생겼습니다. 

 

멀티플렉싱 I/O

멀티플렉싱 I/O는 기존의 전통적인  I/O의 문제를 해결하기 위해 등장했습니다. 이번 섹션에서는 멀티플렉싱 I/O 중에서 가장 진보한 방식인 epoll 방식을 알아볼겁니다. 

 

epoll은 멀티플렉싱 I/O의 구현체로서 연결이 아무리 많아도 요청이 들어온 것만 처리하는 로직을 사용한다는 특징이 있습니다. 

 

epoll의 동작방식을 이해하면 조금 더 이해하기가 쉬운데 동작방식은 아래의 동작방식을 따릅니다. 

 

  1. NIC을 타고 들어온 요청을 커널이 리스트의 형태로 만든다. 이 리스트는 Red Black Tree (RBT)의 형태를 따른다. 
  2. epoll이 이벤트 루프를 돌면서 요청이 발생했는지 여부를 판단한다. 
  3. 이벤트가 발생하면 커널이 만든 리스트를 받아서 요청을 처리한다. 
  4. 리스트에는 어떤 소켓이 요청했는지와 데이터가 어디에 저장되어있는지 포인터가 같이 들어있다. 
  5. 요청을 받으면 포인터를 타고 들어가서 응답 버퍼에 데이터를 담아서 요청한 소켓한테 데이터를 돌려준다. 

※ 잠깐! 여기서 연결과 요청의 차이가 뭘까? 

  • 연결 : 연결은 4계층에서 일어나는 것으로 한번 연결되면 연결이 유지된다는 특징이 있습니다. (HTTP 1.1 이후 기준) 
  • 요청 : 요청은 7계층에서 일어나는 것으로 연결된 통로를 따라 패킷이 이동하는 것을 의미합니다. 

이 둘을 예시로 이야기하면 전화통화할 때 내가 전화를 걸면 상대방이 받고 이 상태를 연결이라고 하고 서로 말을 주고 받는 상황이 요청이라고 생각하시면 됩니다. 

 

이 때문에 연결이 유지된 상태 (최초 1회)에서 여러번의 요청이 이동하는 것입니다. 

 

---

 

이 과정에서 만약 연결이 천개면 Blocking I/O는 스레드를 천개 만들어야했고 Non Blocking I/O는 천개의 연결을 계속 돌아다녀야 했기 때문에 연결이 늘어나면 늘어날 수록 성능이 그만큼 떨어지는 현상이 발생했습니다. 이를 시간복잡도로 표현하면 O(n)이 됩니다. 

 

하지만 epoll은 요청이 온 리스트만 가지고 있어서 연결이 아무리 늘어나도 요청온 것만 처리하면 되기 때문에 시간복잡도는 O(1)이 됩니다. 

 

저는 여기서 의문이 들었던 것이 연결이 천개일 때 천개를 다 둘러보는 것이 O(n)인데 epoll 입장에서 요청 온 리스트를 전부 읽어봐야하는 것은 마찬가지이니 이것도 O(n)이 아닐까 했는데, 여기서 말하는 O(1)의 의미는 연결이 천개던 만개던 동일하게 요청된 리스트만큼을 가져오기 때문에 연결과 성능의 상관관계가 y = x가 아닌 y = 상수 로 고정되어있다는 것을 의미합니다. 

 

Redis와 epoll

Redis는 epoll을 적극적으로 사용하는데, 한 가지 상황을 가정하고 Redis가 어떻게 epoll을 사용하는지 알아보겠습니다. 

 

e.g. 어떤 클라이언트가 Redis에서 mykey라는 키를 get 요청으로 조회하길 원합니다. 

 

  1. 클라이언트가 Redis에 mykey라는 키를 저장한다. 
  2. 커널이 mykey를 요청 리스트에 넣는다. 
  3. Redis의 epoll이 이벤트 루프를 돌면서 이벤트를 감지한다.
  4. mykey에 대한 요청이 들어온다. 
  5. 커널이 요청 리스트를 Redis에 던진다
  6. Redis가 요청 리스트를 보고 어디에 데이터가 적혀있는지 포인터를 타고 들어가서 데이터를 가져온다. 이때 데이터는 RAM에 있기 때문에 이 과정이 수ns면 끝난다. 이후 어떤 소켓이 보냈는지에 대한 정보를 읽고 응다 버퍼에 데이터를 실어서 전달해준다. 
  7. 사용자가 mykey에 대한 데이터를 받는다. 

즉, Redis는 싱글스레드로 돌지만 epoll의 특성과 RAM에 저장된다는 특성 때문에 매우 빠르게 데이터를 가져올 수 있게 되는겁니다. 또한, 싱글스레드이니 각 스레드마다 컨텍스트 스위칭 비용이 들지 않는다는 것은 덤이죠. 

 

epoll의 다양한 사례

이대로 끝내기는 아쉬우니 epoll의 다양한 사례를 알아보고 마무리 짓겠습니다. 

 

Nginx

epoll은 전통적인 I/O 이후에 등장한 가장 진보한 I/O인 만큼 다양한 곳에서 epoll을 사용합니다. 가장 유명한 것이 Nginx인데 Nginx가 Tomcat을 대체하고 새로운 표준이 된 데에는 epoll의 역할이 매우 컸습니다. 

 

전통적인 웹서버는 요청을 동시에 처리하기 위해 요청마다 스레드를 잔뜩 만들어서 동시 처리를 했기 때문에 일정 수준 이상의 요청을 동시에 처리할 수 없었습니다. 하지만 epoll을 적용한 Nginx는 100만개의 연결을 하고도 요청을 잘 처리하게 되었습니다. 

 

R2DBC

전통적인 JDBC는 Blocking I/O의 대표주자였고 커넥션 풀의 개수가 곧 데이터베이스의 처리량의 지표가 되었습니다. 하지만 요즘 나오는 R2DBC는 커넥션 풀이 DB에 요청을 던지기만 하고 대기하고 있다가 DB가 처리 후 응답하면 다시 받아서 처리하는 epoll 방식을 사용합니다. 

 

이 때문에 Blocking I/O와 동일한 커넥션 풀을 가지고 있어도 더 많은 요청을 처리할 수 있게 되었습니다. 

 

gRPC

보통 MSA에서 많이 사용하는 gRPC는 헤더 압축, protobuf, 멀티플렉싱 I/O를 무기로 엄청나게 많은 요청을 처리할 수 있습니다. 단순 레이턴시만 놓고 보면 성능이 좋은건 아니지만 gRPC의 힘은 어마어마하게 많은 양의 요청을 처리할 수 있다는 것입니다. 

 

gRPC도 역시 epoll을 사용해서 I/O를 처리하고 있는데 epoll 덕분에 엄청나게 많은 요청을 처리하고 있습니다. 

 

마치며

개발 공부란건 알고 있다고 생각해도 모르는게 계속 튀어나오는 매력을 가지고 있는 것 같습니다. 이번 포스팅에서는 Redis의 내부 동작을 뜯어보면서 어떻게 싱글스레드임에도 성능이 잘 나올 수 밖에 없는지에 대해서 알아봤습니다. 

 

요즘 X(구 트위터)에서 개발 인사이트를 많이 얻고있는데 처음으로 머스크한테 뽀뽀하고싶은 정도입니다. 자동 번역 최고!

 

이번 포스팅은 여기서 마무리짓도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요!