티스토리 뷰

개요 

C10K 문제란, 단일 서버가 동시에 10,000명(Concurrent 10K connections) 이상의 클라이언트와 연결을 유지하며 통신할 수 있도록 네트워크 소켓 처리 성능을 최적화하는 문제를 말한다.

이 용어는 1999년 Dan Kegel이 처음 제시했으며, 당시에는 대부분의 서버가 수백-수천 개의 동시 연결만으로도 성능 병목에 부딪히는 것이 일반적이었다. 하지만 인터넷의 폭발적인 성장과 함께, 수만 명의 사용자가 동시에 접속하는 환경이 점점 일상화되면서, C10K 문제의 해결은 고성능 서버 설계의 필수 요소가 되었다.

오늘날에는 수만 개의 연결을 동시에 처리하는 것은 기술적으로 어렵지 않지만, 이 문제를 어떻게 극복해 왔고, 그 과정에서 서버 아키텍처와 커널은 어떻게 발전해 왔는지를 이해하는 일은 여전히 의미가 있다고 생각한다.

본 글에서는 C10K 문제를 기점으로 서버 아키텍처가 어떻게 변화해 왔는지 살펴보고, 멀티 프로세싱 -> 멀티 스레딩 -> 멀티플렉싱으로 이어지는 흐름과 함께, 그에 따라 운영체제 커널이 어떻게 진화되었는지를 정리하고자 한다.

 

멀티 프로세싱 기반 서버 - 프로세스로 때우던 시절

멀티 프로세싱 기반의 서버는 클라이언트 요청이 오면, 부모 프로세스가 리스닝 소켓으로 accept()를 호출해서 새로운 연결을 수락한다. 그 후, fork()로 자식 프로세스를 생성하고, 새로 생성된 소켓의 파일 디스크립터(커널에 등록된 소켓의 번호표)를 자식 프로세스에게 넘긴다.

 

자식 프로세스는 이 소켓을 이용해 해당 클라이언트의 요청을 처리하게된다. 이 구조에서는 커넥션 하나당 프로세스가 하나 할당되게 된다.

멀티 프로세싱 기반 서버(출처: https://engineering.linecorp.com/ko/blog/do-not-block-the-event-loop-part1#mcetoc_1gdcaies0s)

이 구조는 장점은 구현 코드가 단순하고, 각 프로세스가 메모리를 독립적으로 사용하니 안전성도 좋다. 그러나 프로세스를 매번 복사 fork()하는 비용은 너무 크고, 컨텍스트 스위칭 비용도 만만치 않다. 따라서 C10K 문제를 해결하기에는 무리가 있다. 10,000명의 사용자가 접속하면 10,000개의 프로세스가 생성되는 것이니말이다. 서버는 더 가벼운 실행 단위를 찾아야했다.

 

멀티 스레딩 기반 서버

하나의 프로세스 안에서 여러개의 스레드를 생성해 요청을 처리하게 한다. 메인 스레드는 리스닝 소켓으로 accept()를 호출해서 연결 요청을 수락한다. 

 

이때 얻는 소켓의 파일 디스크립터(커널에 등록된 소켓의 번호표)를 별도의 워커 스레드를 생성해 넘겨준다. 워커 스레드는 전달 받은 파일 디스크립터 바탕으로 서비스를 제공하게된다.(thread per connection)

멀티스레딩 기반 서버(출처: https://engineering.linecorp.com/ko/blog/do-not-block-the-event-loop-part1#mcetoc_1gdcaies0s)

프로세스 복사 비용보다 스레드 생성 비용이 적다는 장점이 있으나, 여전히 스레드 관리에 많은 리소스가 필요하다. 즉, 스레드 풀을 생성해 관리하며 운영할 수 있지만 요청마다 스레드를 무한정 생성, 대기할 수 없기에 여전히 C10K 문제를 해결하기엔 무리가 있다.

 

멀티플렉싱 기반 서버

멀티플렉싱 기반 서버는 하나의 스레드 또는 소수의 스레드가 수많은 클라이언트의 연결을 동시에 감지하고 처리할 수 있도록 입출력(I/O) 이벤트를 감시하는 구조이다. 

 

운영체제 커널은 이러한 요구에 대응하기 위해 select, poll, epoll과 같은 I/O 멀티플렉싱 시스템 콜을 발전시켜 왔으며, 이는 C10K 문제와 같은 대규모 동시 연결 처리 성능을 향상시키기 위한 핵심 기술로 자리잡았다.

 

전통적인 블로킹 I/O 모델에서는 클라이언트 요청을 처리할 때마다 read()같은 함수 호출이 소켓 단위로 블로킹되었고, 해당 데이터가 네트워크를 통해 커널 공간에 도착하고 사용자 공간으로 복사될 때까지 스레드는 대기해야했다. 이 방식은 연결 수가 많아질수록 스레드 수도 늘어나고 블로킹되야한다는 한계가 있었다.

블로킹 I/O모델(출처: https://engineering.linecorp.com/ko/blog/do-not-block-the-event-loop-part1#mcetoc_1gdcaies0s)

 

 

반면, 멀티플렉싱 I/O모델은 select, poll, epoll 같은 시스템 콜을 통해 여러 소켓을 동시에 감시하고, 실제로 읽을 준비가 된 소켓에 대해서만 I/O작업을 수행한다. 예를 들어 select()를 호출하면 커널은 이벤트가 발생한 소켓만을 선별해 애플리케이션에 알려주며, 애플리케이션은 그 후에 해당 소켓에 대해 실제 I/O작업을 수행한다.

멀티플렉싱 I/O모델(출처: https://engineering.linecorp.com/ko/blog/do-not-block-the-event-loop-part1#mcetoc_1gdcaies0s)

이로 인해 입출력 함수 자체는 여전히 블로킹이지만, 실제로 데이터가 준비된 소켓에 대해서만 호출되므로, 스레드가 소켓마다 블로킹되어야 했던 구조적 제약을 완화하고, 이벤트 기반으로 필요한 순간에만 워커 스레드를 투입할 수 있도록 했다. 또한, 멀티플렉싱 시스템 콜은 하나의 스레드에서 수천 개의 소켓 상태를 동시에 감시할 수 있게 해주어, 적은 수의 스레드로도 높은 동시성을 달성할 수 있도록 한다.

 

 

select 시스템 콜 기반

select 시스템 콜 기반의 I/O 멀티플렉싱에서는, 소켓은 커널의 프로토콜 스택 내에 메모리 공간을 할당받고, 사용자 공간에서는 이를 파일 디스크립터(file descriptor)를 통해 접근한다.

이벤트 감지를 위해 사용자 공간에서 준비한 파일 디스크립터 집합(fd_set)은 select() 호출 시 커널 공간으로 복사된다. 커널은 이 fd_set을 기반으로 각 파일 디스크립터의 상태(읽기 가능, 쓰기 가능 등)를 감시하며, 이벤트가 발생하면 select()를 호출한 스레드에게 이를 알려준다.

 

이때 select()는 다음 두 가지 방식으로 결과를 반환한다.

  1. 리턴값으로 이벤트가 발생한 파일 디스크립터의 개수를 반환하고,
  2. 입력받은 fd_set을 수정하여, 이벤트가 발생한 디스크립터만 남긴 상태로 다시 사용자 공간에 되돌려준다.

즉, 사용자 공간으로 수정된 fd_set이 반환되며, 프로그램은 이를 순회하며 FD_ISSET() 등의 매크로로 어떤 디스크립터에서 이벤트가 발생했는지를 직접 확인해야 한다. 이후 사용자 공간에서는 이벤트가 발생한 소켓에 대해서만 워커 스레드가 read()를 호출하여 데이터를 처리하게 된다.


이때 워커 스레드는 read() 호출로 인해 블로킹될 수 있지만, I/O 감시 스레드는 select 호출을 통해 블로킹 없이 계속해서 다른 소켓 상태를 감시할 수 있으므로, 전체적으로 스레드 효율성과 동시 처리 성능이 크게 향상된다.

select 시스템 콜 동작(출처: https://engineering.linecorp.com/ko/blog/do-not-block-the-event-loop-part1#mcetoc_1gdcaies0s)

예를 들어, 6개의 소켓을 감시할 때 select()는 각 소켓의 상태를 검사하다가, 읽기 가능한 소켓을 발견하면 워커 스레드에게 해당 소켓을 넘겨 처리하게 된다.
즉, read(3) → 처리 → write(3), read(5) → 처리 → write(5)와 같은 식으로 동작하며, 감시 스레드는 블로킹 없이 연속적인 감시가 가능하게 된다.

 

하지만 select() 시스템 콜은 다음과 같은 구조적인 한계를 갖고 있다.

 

감시 대상인 모든 파일 디스크립터를 매번 전체 순회해야하므로, 선형 시간복잡도 O(n)이 된다. 또한, 검사할 수 있는 파일 디스크립터 개수가 최대 1024개로 제한이 있다. 그리고 변경감지 방식에 비효율성이 있다. select()가 반환하는 것은 이벤트가 발생한 파일 디스크립터 개수이기 때문에 어떤 파일 디스크립터에서 이벤트가 발생했는지 직접 fd_set을 확인해야한다.

또한 매번 사용자 공간과 커널 공간 간에 데이터를 복사해야하므로 컨텍스트 전환과 메모리 복사 비용이 존재한다.

 

이러한 이유로 select는 초창기에는 널리 사용되었으나, 이후 poll, epoll, kqueue등의 개선된 시스템 콜 기술로 대체 되었다.

 

 

poll 시스템 콜 기반

poll도 select와 마찬가지로 멀티플렉싱 시스템 콜이다. select의 구조적인 단점을 개선하기 위해 등장한 I/O멀티플렉싱 방식이다. 기본적인 동작 방식은 select와 유사하지만, 감시 대상 소켓을 비트셋(fd_set)이 아닌 구조체 배열(pollfd[])로 표현하므로써 유연성과 확장성을 높였다.

poll의 동작 방식은 select처럼 모든 파일 디스크립터의 상태를 매번 검사해야하므로 여전히 O(n)의 시간 복잡도를 갖지만, 감시 가능한 디스크립터 수의 제한이 사라졌다. select는 내부적으로 고정된 크기의 비트 배열을 사용하기 때문에 감시 가능한 디스크립터 수가 1024개로 제한되지만 poll은 사용자가 pollfd[]배열을 직접 구성하므로 사실상 제한이 없다.

하지만 poll도 여전히 근본적인 구조는 select 와 비슷하다. 감시 대상인 파일 디스크립터 배열을 매 호출마다 커널에 전달해야하고, 이벤트 발생 여부를 판단하기 위해 배열 전체를 순회해야한다. 즉 시간복잡도는 여전히 O(n)이며, 대량의 소켓을 동시에 처리해야할 경우 성능저하가 발생할 수 있다. 또한 여전히 매번 사용자 공간과 커널 공간 간에 데이터를 복사해야하므로 컨텍스트 전환과 메모리 복사 비용이 존재한다.

결과적으로 poll은 디스크립터 수 제한을 제거하고 인터페이스의 유연성을 확보했다는 점에서 select 대비 명확한 발전이지만, 여전히 매 호출마다 전체 디스크립터를 순회해야 한다는 구조적 한계와 사용자-커널 간 복사 비용은 그대로 남아 있다.

 

 

epoll 시스템 콜 기반

epoll은 select와 poll의 구조적인 한계를 해결하기 위해 Linux에서 도입된 고성능 I/O 멀티플렉싱 시스템 콜이다.


epoll의 가장 큰 특징은 감시 대상인 파일 디스크립터들을 커널 내부에 등록해 상태를 추적한다는 점이다. 즉, 이전처럼 매 호출 마다 모든 디스크립터를 사용자 공간에서 커널 공간으로 복사하지 않아도 되며, 변경된 디스크립터 수를 반환하는 것이 아닌 목록 자체를 반환하기 때문에 추가 탐색할 필요가 없다.

이 구조 덕분에 epoll은 감시 대상의 수가 수천-수만 개에 달하더라도 성능이 O(1)에 가까운 이벤트 탐지가 가능하다.
더 정확히 말하자면 이벤트가 발생한 디스크립터만 연결 리스트(rdllist)로 관리하며, epoll_wait()는 이 리스트만 접근한다. 따라서 이벤트가 실제로 발생한 디스크립터 수에 비례한 시간이 소요가 된다. O(n_ready)

epoll은 C10K 문제를 사실상 해결 가능한 수준으로 만든 핵심 기술 중 하나로, 오늘날 대부분의 고성능 서버(Linux 기반)에서 표준처럼 사용되고 있다. Node.js, Nginx, Netty 등이 Linux 환경에서 이 epoll기반 멀티플렉싱을 활용하여 수만 개의 동시 연결을 효과적으로 처리한다.

 

 

참고 자료

- C10K Problem: Understanding and Overcoming the 10,000 Concurrent Connections Challenge

- wikipedia, C10K problem, https://en.wikipedia.org/wiki/C10k_problem

- 고전 돌아보기, C10K 문제 (C10K Problem), https://oliveyoung.tech/2023-10-02/c10-problem/

- 비동기 서버에서 이벤트 루프를 블록하면 안 되는 이유 1부 - 멀티플렉싱 기반의 다중 접속 서버로 가기까지

- [네이버클라우드 기술&경험] IO Multiplexing (IO 멀티플렉싱) 기본 개념부터 심화까지 -1부-

- wikipedia, epoll, https://en.wikipedia.org/wiki/Epoll

- [I/O multiplexing] select vs kqueue, https://gencomi.tistory.com/entry/IO-multiplexing-select-vs-kqueue

- stackoverflow, https://stackoverflow.com/questions/17615272/java-selector-is-asynchronous-or-non-blocking-architecture/17619830

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함