TCP concurrency models

동시 접속 모델은 연결을 누가 기다리고 누가 처리하는지의 선택이다

단일 서버 루프는 연결을 받은 뒤 한 클라이언트의 recv/send 처리에 머물면 새 accept로 돌아오지 못한다. 여러 클라이언트를 다루려면 연결 처리 흐름을 프로세스, 스레드, 풀, 이벤트 루프 중 어디로 분리할지 결정해야 한다.

accept 뒤의 fd를 어디로 넘기느냐가 모델을 가른다

listen socket은 계속 새 연결을 받아야 하고, accepted socket fd는 별도의 실행 주체나 이벤트 루프가 짧게 처리해야 병목을 피한다.

listen socket SYN backlog와 accept queue에서 새 연결을 기다린다.
accept() connected socket fd를 만들고 처리 경로로 넘긴다.
single loop 같은 루프가 recv/send까지 맡아 느린 클라이언트가 accept 복귀를 늦춘다.
fork 자식 프로세스가 fd를 처리하고 부모는 listen fd로 돌아간다.
thread 스레드가 블로킹 I/O를 맡지만 스택 메모리와 락 비용을 남긴다.
pool worker 수를 고정해 폭주를 막고, 큐에서 처리 순서를 관리한다.
event loop epoll/kqueue가 ready fd만 깨워 짧게 읽고 다시 감시 상태로 돌린다.
single loop

한 연결을 끝까지 처리

구현은 가장 단순하지만 처리 중인 클라이언트가 느리면 다음 accept로 돌아가는 시간이 늦어진다.

fork

연결마다 프로세스

주소 공간과 장애 격리에 유리하다. 다만 프로세스 생성, 스케줄링, fd 정리, 자식 회수 비용이 생긴다.

thread

연결마다 스레드

코드 흐름은 직관적이다. 대신 스택 메모리, wake-up 빈도, 락 경합, 공유 상태 동기화를 관리해야 한다.

thread pool

worker 수를 제한

폭주는 막지만 블로킹 장기 연결이 worker를 점유하면 새 작업이 밀린다. 요청 단위 작업에 더 잘 맞는다.

event loop

readiness를 감시

적은 실행 흐름이 많은 fd를 감시한다. 소켓은 non-blocking으로 두고 짧게 읽고 쓰며 backpressure를 처리한다.

확장성은 막대 점수가 아니라 대기 방식과 블로킹 위치로 판단한다

프로세스와 스레드

  • fork는 copy-on-write지만 프로세스별 관리 비용은 남는다.
  • 스레드는 대부분 recv에서 잠들 수 있으나 메모리와 스케줄러 비용을 차지한다.

이벤트 루프

  • epoll도 non-blocking 설계를 대신하지 않는다.
  • partial read/write, EAGAIN, 느린 클라이언트 큐를 직접 다룬다.

CPU 작업

  • 이벤트 루프 안에서 CPU-bound 작업을 오래 하면 모든 fd 처리가 늦어진다.
  • 무거운 계산은 worker thread나 process pool로 분리한다.