nginx 소스코드 저장소를 열면 처음에는 익숙한 C 프로젝트 구조처럼 보인다. 하지만 src/core, src/event, src/http를 가로질러 읽다 보면 2004년 Igor Sysoev가 C10K 문제를 얼마나 구체적으로 설계에 박아 넣었는지 보이기 시작한다.
핵심은 master-worker 분리다. master process는 요청을 단 하나도 처리하지 않는다. 설정 파일을 읽고, 포트를 바인딩하고, worker process들을 관리하는 게 전부다. 포트 바인딩처럼 root 권한이 필요한 작업은 master가 처리하고, 실제 트래픽을 받는 worker들은 낮은 권한(보통 nobody)으로 실행된다. 기능 분리인 동시에 보안 격리다. `nginx -s reload`가 연결 하나 떨어뜨리지 않고 설정을 반영하는 것도 이 구조에서 나온다. master가 SIGHUP을 받으면 새 설정으로 새 worker들을 먼저 띄우고, 기존 worker들에게는 graceful shutdown 신호를 보낸다. 기존 연결이 자연히 소화되면 종료. downtime 제로.
메모리 관리 방식도 다르다. nginx는 연결이 시작될 때 그 연결 전용 memory pool을 생성하고(src/core/ngx_palloc.c 참조), 연결이 끝나는 시점에 pool 전체를 한 번에 해제한다. 연결 수명 동안 malloc/free를 반복 호출하지 않으니 heap fragmentation이 억제되고, 해제 로직도 단순해진다. 짧은 수명의 객체가 수없이 생겼다 사라지는 HTTP 요청 처리에 최적화된 선택이다. HTTP 요청 처리는 11개의 phase로 나뉘어 있다. post-read, find-config, rewrite, preaccess, access, try-files, content, log 순으로 흐르고, 각 모듈은 관심 있는 phase에만 handler를 등록한다. nginx 모듈이 이토록 다양하게 만들어질 수 있었던 이유가 여기에 있다. 인증, 캐싱, 압축, 로깅이 서로 독립적인 phase handler로 존재하면서 요청 파이프라인에 끼어들 수 있다.
단 하나의 전제 조건이 이 모든 것을 유지한다. blocking I/O를 하지 않는 것. worker 하나가 blocking syscall을 호출하는 순간, 그 worker가 epoll로 감시 중인 수천 개의 연결이 모두 멈춘다. nginx 생태계에서 non-blocking이 거의 교리처럼 강제되는 이유다. ngx_lua의 cosocket API, njs의 Promise 기반 구현이 이 원칙을 언어 레벨에서 강제하려는 시도다. 웹서버 하나를 제대로 읽으면 운영체제, 메모리 관리, 비동기 프로그래밍이 어떻게 맞물리는지 한꺼번에 보인다. 고성능 서버 구현에서 반복해서 등장하는 메모리 풀 + phase 파이프라인 패턴의 원형을 보고 싶다면, Linux kernel 다음 교재로 nginx 소스가 적합하다.