서버 한 대로 동시 접속 1만 개, 'C10K 문제'란 무엇인가...epoll과 이벤트 구동 구조가 해법으로, 이제는 C10M 시대
동시 접속 1만 개를 처리하는 C10K 문제의 원인과 해법이 정리됐다.
[한국정보기술신문] 서버 한 대가 동시에 1만 개 이상의 클라이언트 연결을 효율적으로 처리할 수 있느냐를 묻는 이른바 'C10K 문제(Connection 10,000 Problem)'가 다시 조명받고 있다. C10K 문제는 서버의 동시성 처리 한계와 관련된 과제로, 특히 네트워크 소켓의 입출력(I/O) 성능과 시스템 자원 관리에서 발생한다. 소켓이란 네트워크로 연결된 두 프로그램이 데이터를 주고받는 통로를 말한다.
이 문제는 1999년 미국의 개발자 댄 케겔(Dan Kegel)이 처음 제기했다. 당시 대부분의 웹 서버와 운영체제는 동시에 많은 클라이언트 요청을 효율적으로 처리하지 못했다. 1990년대부터 2000년대 초반까지는 서버가 수백에서 수천 개의 동시 연결을 처리하는 것이 일반적이었기 때문이다. 그러나 인터넷이 빠르게 보급되고 트래픽이 늘어나는 한편 채팅, 게임 같은 실시간 서비스가 확대되면서 1만 개 이상의 동시 연결을 감당해야 하는 상황이 생겼고, 기존 서버 구조의 한계가 그대로 드러났다. 여기서 1만이라는 숫자는 절대적인 한계라기보다, 하드웨어 성능이 충분한데도 소프트웨어 구조 때문에 연결을 처리하지 못하는 상황을 상징하는 기준으로 쓰인다.

연결 하나에 스레드 하나...메모리와 문맥 전환이 발목
C10K 문제의 첫 번째 원인으로는 전통적인 스레드 기반 모델의 한계가 꼽힌다. 과거의 웹 서버는 클라이언트 연결이 하나 생길 때마다 별도의 프로세스나 스레드를 만들어 처리하는 방식을 썼다. 프로세스는 실행 중인 프로그램 하나를, 스레드는 그 안에서 일을 나눠 맡는 작은 작업 단위를 가리킨다. 연결이 수백 개 수준일 때는 이 방식이 단순하고 직관적이어서 널리 쓰였다.
문제는 연결이 1만 개로 늘어날 때다. 스레드 하나마다 일정량의 메모리가 할당되므로, 1만 개의 스레드를 만들면 메모리 사용량이 감당하기 어려운 수준으로 불어난다. 또 중앙처리장치(CPU)는 수많은 스레드를 번갈아 실행하기 위해 작업 상태를 저장하고 불러오는 '문맥 전환'을 끊임없이 반복해야 하는데, 이 과정 자체가 큰 비용이다. 결국 실제 요청을 처리하는 일보다 스레드를 관리하는 데 더 많은 자원이 쓰이면서 서버 전체의 성능이 급격히 떨어진다. 당시 컴퓨팅 자원이 지금보다 훨씬 비쌌다는 점도 문제를 키웠다. 1999년 무렵에는 서버 한 대 값이 만만치 않아, 장비를 늘리는 대신 한 대로 최대한 많은 연결을 감당하는 것이 운영 비용을 줄이는 핵심 과제였다.
두 번째 원인은 운영체제가 제공하던 입출력 감시 방식의 한계다. 여러 연결을 한꺼번에 지켜보기 위해 유닉스 계열 운영체제는 'select'나 'poll'이라는 기능을 제공해 왔다. 그러나 이 방식은 데이터가 도착했는지 확인하려면 등록된 모든 연결을 처음부터 끝까지 일일이 훑어야 한다. 연결 수가 늘어날수록 검사에 드는 시간이 비례해 늘어나는 구조여서, 수만 개의 연결 앞에서는 사실상 제 기능을 하지 못했다. select는 감시할 수 있는 연결 수 자체에 상한이 있다는 제약도 있었다.
epoll·kqueue·IOCP...운영체제가 먼저 바뀌었다
해법은 운영체제 차원에서 먼저 나왔다. 리눅스는 'epoll', 유닉스 계열인 BSD는 'kqueue', 마이크로소프트 윈도우는 'IOCP'라는 새로운 입출력 처리 방식을 도입했다. 이들 방식의 공통점은 모든 연결을 일일이 검사하는 대신, 실제로 데이터가 도착하는 등 사건(이벤트)이 발생한 연결만 골라 알려 준다는 것이다. 연결이 아무리 많아도 사건이 생긴 것만 처리하면 되므로, 연결 수가 늘어나도 성능이 거의 떨어지지 않는다.
응용 프로그램 쪽에서는 이벤트 구동(event-driven) 구조와 비동기 입출력이 해법으로 자리 잡았다. 비동기 입출력은 데이터를 읽거나 쓰는 작업이 끝날 때까지 멈춰 기다리지 않고, 그동안 다른 일을 처리하다가 작업이 끝나면 이어서 처리하는 방식이다. 적은 수의 스레드만으로도 수만 개의 연결을 번갈아 처리할 수 있어, 연결마다 스레드를 만드는 방식보다 메모리와 CPU를 훨씬 적게 쓴다.
엔진엑스와 노드JS, C10K가 낳은 소프트웨어
이런 구조를 실제 제품으로 구현해 C10K 문제 해결의 상징이 된 것이 웹 서버 엔진엑스(NGINX)다. 러시아 개발자 이고르 시쇼브(Igor Sysoev)는 2002년 자국 대형 포털 람블러(Rambler)의 웹 서버 부하 문제를 풀기 위해 엔진엑스 개발에 착수했다. 당시 람블러는 급증하는 트래픽을 기존 웹 서버로는 감당하기 어려운 상황이었다. 엔진엑스는 처음부터 비동기 이벤트 구동 구조로 설계돼, 적은 자원으로 많은 연결을 동시에 처리할 수 있었다. 연결마다 프로세스나 스레드를 띄우던 기존 아파치(Apache) 웹 서버의 방식과 대비되는 지점이다. 엔진엑스는 이후 세계에서 가장 널리 쓰이는 웹 서버 가운데 하나로 성장했다.
자바스크립트 실행 환경인 노드JS(Node.js)도 같은 흐름에서 나왔다. 노드JS는 단일 스레드 위에서 이벤트 반복 구조로 작동하며, 입출력 작업을 비동기로 처리해 많은 동시 연결을 효율적으로 감당한다. 자바 진영 역시 초기의 성능 한계를 넘기 위해 비동기 입출력을 지원하는 NIO 라이브러리와 네티(Netty) 같은 프레임워크를 발전시켜 왔다. 오늘날 대규모 트래픽을 다루는 서비스 대부분이 이런 이벤트 구동·비동기 구조를 기본으로 채택하고 있다.
끝나지 않은 숙제...C10M·C100M으로 진화
C10K 문제 자체는 사실상 풀린 과제로 평가된다. 하드웨어 성능이 크게 좋아진 데다 epoll 같은 운영체제 기능과 이벤트 구동 소프트웨어가 보편화되면서, 서버 한 대가 1만 개의 연결을 처리하는 일은 더 이상 어렵지 않게 됐다. 그러나 문제의 규모가 함께 커졌다. 이용자 수가 폭발적으로 늘면서 이제는 동시 연결 1천만 개를 다루는 'C10M', 나아가 1억 개를 겨냥하는 'C100M'이 새로운 과제로 거론된다. 삵의 기술 노트 역시 C10K와 함께 C10M, C100M을 관련 주제로 함께 다뤘다.
C10M 수준에서는 운영체제 커널을 거치는 비용조차 부담이 되기 때문에, 커널을 우회해 네트워크 카드와 직접 데이터를 주고받는 기술 등 더 공격적인 최적화가 동원된다. 결국 C10K 문제는 특정 시대의 기술 장애물이라기보다, 늘어나는 이용자와 한정된 서버 자원 사이에서 동시성을 어떻게 감당할 것인가라는 서버 설계의 근본 질문인 셈이다. 27년 전 제기된 물음이 지금도 형태만 바꿔 이어지고 있다는 점에서, C10K 문제는 대규모 서비스를 설계하는 개발자라면 한 번쯤 짚고 넘어가야 할 고전으로 꼽힌다.
한국정보기술신문 정보기술분과 유상헌 기자 news@kitpa.org











