C10K 문제에 대하여

http://www.kegel.com/c10k.html

참고: 이 문서는 고전이다. 꼭 한번 읽어보길 바란다. (물론 2011년 이후로는 업데이트가 되지 않고 있다)

C10K 문제제기

C10K(Concurrently handling 10 Thousand connections) 문제는 1999년 단 케겔(Dan Kegel)이 처음 제시한 것으로 1만 개 (10K)의 클라이언트(커넥션)를 동시(Concurrently)에 처리할 수 있는 네트워크 I/O 모델 설계 방법을 묻는 문제이다.

Concurrent Connection은 RPS(Request per Second)와는 조금 다르다. RPS를 높일 수 있는 방법으로는 request당 처리 속도를 높이는 방법이 있는 반면, connection의 경우 빠르게 처리되는 개념 보다는 효율적인 스케줄링이 필요한 문제다.

C10K 문제를 해결하기 위해서는

  1. 운영체제(OS) 상의 제약
  2. 웹서버 소프트웨어의 한계
  3. 물리 장비의 성능

이 셋을 극복해야 한다.

한편 물리장비는 빠르게 발전하고 있고, 무어의 법칙에 따라 1.5년에서 2년마다 2배씩 개선되고 있다. 따라서, 하드웨어적 제약보다는 소프트웨어적(OS, WS) 제약을 해결하는 것이 더 문제의 핵심에 가깝다. C10K가 제기되던 시기에는 64bit 운영체제가 일반적이지 않아서 운영체제/물리장비 상 가용할 수 있는 메모리 주소에 한계가 있었고, 무한히 스레드를 늘리고 각 스레드 별로 충분한 스택 메모리를 할당할 수 없기도 했다.

1.하나의 클라이언트를 하나의 서버 스레드에서 처리

만약 하나의 클라이언트의 요청을 처리하기 위해 하나의 스레드가 필요하다면 어떨까? 이 방식은 가장 클래식한 설계지만 리소스 낭비가 심하다.

각 클라이언트 별로 stack frame을 사용하기 때문에 메모리 낭비가 심하다. 32bit 운영체제의 경우 2MB의 스택 메모리를 각 스레드에 할당한다고 가정할 경우, 512개 스레드에서 메모리가 고갈될 수 있다. (1GB VM 가정)

물론 64bit 운영체제를 이용할 경우, 이론상 2의 41승 개까지 스레드를 생성하고 스택 메모리를 부여할 수 있다. (최대 스레드의 개수는 하드웨어 스펙에 종속적이다)

즉, 이 방식으로 C10K 문제를 해결하는 것은 바람직하지 않다.

2. 각각의 스레드가 Nonblocking I/O를 활용해 여러 개의 클라이언트 요청을 처리

하나의 스레드가 복수의 클라이언트 요청을 처리하는 구조다. 이 방식은 아래 두 가지의 서로 다른 적용 방법으로 나뉜다.

  1. level-triggered readiness notification을 활용하는 방법
  2. readiness change notification을 활용하는 방법

Level-triggered Readiness Notification

모든 네트워크 핸들에 nonblocking 모드를 적용하며 select()poll()을 사용해 어떤 네트워크 핸들이 데이터를 기다리는지 확인하는 방법이다. 커널이 어떤 File Descriptor가 준비된 상태인지 알려준다.

중요한 점은, 커널이 알려주는 readiness notification은 힌트에 가깝다는 것이다. File Descriptor는 우리가 읽어오고 싶은 시점에 어쩌면 더 이상 준비된 상태가 아닐 수도 있다. 그래서 nonblocking 모드를 이용해야 하는 것이다.

여기서 사용되는 select()poll()은 몇가지 문제를 가진다.

select()

select()FD_SET_SIZE 핸들 수가 제약이 있다. 즉, 동시에 커넥션을 맺고 관리할 수 있는 최대 클라이언트 수에 제약이 있다.(1024개 혹은 2048개) 따라서 C10K 문제를 해결할 수 없다.

poll()

poll()은 핸들 수에 제약이 없다. 하지만, 핸들 수가 증가하면 느려지는 문제가 있다. 대부분의 File Descriptoer는 언제든 대기상태가 될 수 있는데, 어떤 File Descriptor가 대기 상태인지를 확인하기 위해서는 사실상 풀스캔을 해야 하기 때문이다.

Readiness Change Notification (Edge-triggered Readiness Notification)

우리가 커널에 File Descriptor를 커널에 넘기고 커널이 ‘not ready’, ‘ready’와 같은 상태 변화를 우리에게 알려주는 방법이다.

kqueue()

FreeBSD에서는 kqueue()를 사용한다. 참고로 kqueue()는 위에서 살펴본 level-triggered readiness notification도 지원한다. poll()의 대안이다.

epoll()

Linux에서는 edge-triggered를 위한 poll의 대안으로 epoll이 활용된다.

3. Asynchronous I/O

비동기적으로 I/O를 처리하는 방식이다.

AIO는 대개 edge-triggered(readiness change notification)와 함께 사용되곤 하는데, operation이 완료되면 signal이 큐(queue)에 적재되는 식이다.

Web server의 C10K 문제 대응 방법

NginX

NGINX는 Apache의 C10K 문제를 해결하도록 Event Driven 구조를 채택하고 있다. 고정된 프로세스만 생성하고 각 프로세스 내에서 비동기적으로 작업을 처리한다. epoll, kqueue 등 I/O 멀티플렉싱 모델을 활용한다.

Java 사례

Java의 경우 NIO, NIO2가 있다. NIO는 블로킹, 논블로킹을 지원하며 NIO2는 블로킹, 논블로킹, 동기, 비동기를 모두 지원한다.

Node.js 사례

NodeJS의 경우 libuv를 사용하는 데, 내부적으로 epoll을 지원한다. 싱글 스레드 기반이지만 비동기 방식을 지원하기 때문에 쉽게 C10K 문제를 해결할 수 있다.

Go 사례

To be updated.

Reference

http://www.kegel.com/c10k.html

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=manhdh&logNo=120164273361

https://en.wikipedia.org/wiki/C10k_problem

https://manhyuk.github.io/c10k-problem/

https://applefarm.tistory.com/137

http://highscalability.com/blog/2013/5/13/the-secret-to-10-million-concurrent-connections-the-kernel-i.html