"준비된 것만 알려 달라" 는 커널 기능 위에서 도는 처리 방식.
-
전통적 방식 — 연결마다 스레드를 만들고 그 스레드가 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 바운드 작업에는 이점이 없다.
- ❌ "비동기니까 병렬이다" → 워커 하나 안에서는 한 번에 하나씩 처리한다. 병렬은 워커 수만큼이다.