웹 서버·WAS 용어 사전
nginxepoll · kqueue · 이벤트 기반

이벤트 루프

"준비된 연결만 알려 달라"는 커널 기능 위에서 도는 처리 방식. 연결당 스레드를 쓰지 않는다.

"준비된 것만 알려 달라" 는 커널 기능 위에서 도는 처리 방식.

  • 전통적 방식 — 연결마다 스레드를 만들고 그 스레드가 read() 에서 블로킹된다

    • 대기 중인 연결도 스레드·스택을 점유한다
  • 이벤트 기반 — 커널에 "이 중 준비된 게 생기면 알려 줘" 를 등록해 두고

    • 준비된 것만 골라 처리한다
    • 대기 중인 연결은 자료구조 몇 KB 만 차지한다

커널 기능

  • epoll — 리눅스. 관심 있는 디스크립터를 커널에 등록해 두고
    • 변화가 생긴 것만 돌려받는다 → O(1)
  • kqueue — BSD · macOS
  • select/poll — 매번 전체 목록을 넘겨 훑는다 → 개수에 비례해 느려진다 (O(n))

nginx 는 플랫폼에 맞는 것을 자동으로 고른다 확인: nginx -V 2>&1 | tr ' ' '\n' | grep -i epoll

왜 이것이 C10K 를 풀었나

select/poll 은 연결이 늘수록 '확인 자체' 가 비싸졌다 1만 연결 중 10개만 활성이어도 매번 1만 개를 훑는다

epoll 은 변화가 생긴 것만 돌려준다

  • 1만 연결 중 10개가 활성이면 10개만 받는다
  • 유휴 연결이 사실상 공짜가 된다

keep-alive · 롱 커넥션 · WebSocket 처럼 '붙어만 있는' 연결이 많은 워크로드에서 격차가 극적으로 벌어진다

대가

한 지점에서 블로킹하면 그 워커의 모든 연결이 멈춘다

그래서 이벤트 서버에서 하지 않는 것

  • 무거운 계산 (그래서 애플리케이션은 뒤로 보낸다)
  • 동기 디스크 I/O (aio threads 로 뺀다)
  • 동기 DNS 조회 (resolver 를 설정해 비동기로)

"코드가 단순하다" 는 스레드 모델의 장점이 여기서는 사라진다 대신 연결당 비용이 극적으로 낮다

같은 발상이 곳곳에 있다

nginx · Node.js · Redis · Envoy — 모두 이벤트 루프 기반이다 Redis 가 단일 스레드로도 빠른 이유가 이것이다

  • 명령 하나하나가 매우 짧아서 블로킹이 사실상 없다
  • 반대로 O(n) 명령(KEYS · 큰 SORT)을 쓰면 전체가 멈춘다

"짧은 작업이 많을 때 강하고, 긴 작업 하나에 취약하다" 가 공통 성질이다

워커가 막히는지 어떻게 아나

  • 증상 — 특정 요청만 느린 게 아니라 '동시에' 다 느려진다
    • CPU 는 한 코어만 100% 에 붙어 있다
확인
  top -H            스레드/프로세스별 CPU — 한 워커만 튀는지 본다
  perf top          어느 함수에서 시간을 쓰는지
  error log         "worker process ... exited on signal" 는 크래시다

흔한 원인

  • 정규식 백트래킹 폭발 (location 정규식 · map 규칙)
  • 큰 파일을 aio 없이 읽는다
  • 서드파티 모듈의 동기 호출 (인증·로깅 모듈)

왜 nginx 는 캐시를 별도 프로세스로 두나

$ ps -ef | grep nginx ... nginx: cache manager process ... nginx: cache loader process

캐시 디렉터리를 훑고 오래된 항목을 지우는 일은 디스크 I/O 가 크다 워커에서 하면 그 워커의 모든 연결이 영향을 받는다

  • 별도 프로세스로 분리했다

'블로킹할 수 있는 일은 이벤트 루프 밖으로' 라는 원칙의 사례다

면접 함정

  • "이벤트 기반이 항상 빠르다" → CPU 바운드 작업에는 이점이 없다.
  • "비동기니까 병렬이다" → 워커 하나 안에서는 한 번에 하나씩 처리한다. 병렬은 워커 수만큼이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — nginx 프로세스 모델 — 만 개의 연결