namalzi

← 글 목록

nginx 소스 파헤치기 2: 전체 지도 — 마스터와 워커, 모듈, cycle, 설정 문맥

nginx · 게시

nginx 소스를 처음 여는 중급 프로그래머를 위한 연재 전체의 지도입니다. 마스터 하나와 워커 여럿이 각각 어디서 기다리는지, 모든 기능이 ngx_module_t 하나로 꽂히는 모듈 구조와 "CORE"·"HTTP" 글자로 된 모듈 종류, 실행 중인 nginx의 전부를 담은 ngx_cycle_t와 conf_ctx, nginx.conf의 main·events·http·server·location 문맥이 비트 플래그로 검사되는 방법, 한 워커의 이벤트 루프가 수백 개의 연결을 도는 모습을 실제 소스와 ps·gdb 예제로 확인합니다.

이 글에서 다루는 것

"nginx 소스 파헤치기" 연재의 두 번째 글입니다. 1편에서 소스를 받아 빌드하고, 폴더 지도와 요청 하나가 지나는 큰 길을 펴 보았습니다. 이번 글은 코드를 한 줄씩 읽기 전에 펴 놓는 전체 지도입니다. 3편부터 12편까지는 이 글의 지도에서 상자 하나씩을 골라 확대합니다.

nginx를 써 본 사람이라면 ps에서 nginx: master process와 nginx: worker process를 본 적이 있고, nginx.conf에 events {}·http {}·server {}·location {}을 겹겹이 써 보았을 것입니다. 이 글은 그 익숙한 모습을 다리 삼아 다음 질문에 답합니다.

항목 내용
대상 nginx
버전 nginx 1.31.9 (master, 개발판)
커밋 9a7439fd79 (2026-10-23)
주로 읽는 파일 src/core/nginx.c, src/os/unix/ngx_process_cycle.c, src/core/ngx_module.h, src/core/ngx_conf_file.[ch], src/core/ngx_cycle.[ch], src/http/ngx_http.c, src/http/ngx_http_config.h, src/event/ngx_event.c

글에 나오는 줄 번호는 모두 이 커밋 기준입니다. 같은 커밋의 소스를 받고 예제용 도커 이미지를 만드는 방법은 1편에 있습니다. 경로는 저장소 루트에서 시작합니다(src/core/…).

지도를 그리는 글이라 이 글은 큰 줄기만 따라갑니다. 각 부분을 자세히 읽는 글은 절마다 "N편에서 봅니다"로 이어 두었습니다.

먼저 알아 둘 말

마스터 프로세스(master process)는 nginx를 띄우면 남는 첫 프로세스입니다. 보통 root로 돌고, 설정을 읽고, 포트를 열고, 워커를 만들고, 시그널(nginx -s reload 같은 명령)을 받습니다. 요청은 처리하지 않습니다.

워커 프로세스(worker process)는 마스터가 fork()로 만든 자식입니다. worker_processes 값만큼 생기고, 실제 연결을 받아 요청을 처리합니다. 워커 하나는 스레드 하나로 돕니다(2·6절에서 확인합니다).

모듈(module)은 nginx의 기능 단위입니다. HTTP 처리, 로그, gzip, 프록시는 물론이고 "설정 파일의 맨 바깥 디렉티브를 처리하는 코어"와 "epoll을 쓰는 이벤트 처리"도 모듈입니다. C에서는 ngx_module_t 구조체 변수 하나입니다(3절).

cycle(사이클)은 실행 중인 nginx 하나의 상태 전체를 담은 구조체 ngx_cycle_t입니다. 읽은 설정, 열린 리스닝 소켓, 연결 배열, 모듈 목록이 여기 매달립니다. 설정을 다시 읽으면(reload) 새 cycle을 하나 더 만듭니다 — 이름에 "주기"가 들어간 이유입니다(4절).

설정 문맥(configuration context)은 디렉티브가 놓인 자리입니다. 파일 맨 바깥(main), events {}, http {}, server {}, location {} 같은 것입니다. 디렉티브마다 "놓여도 되는 문맥"이 비트 플래그로 정해져 있습니다(5절).

이벤트 루프(event loop)는 "준비된 소켓이 있으면 깨어나 처리하고, 없으면 잠든다"를 끝없이 되풀이하는 반복문입니다. 리눅스에서 nginx는 epoll(소켓 수천 개를 한꺼번에 등록해 두고 읽거나 쓸 수 있게 된 것만 돌려받는 리눅스 시스템 콜)로 잠들어 기다립니다. 소켓은 논블로킹(읽을 데이터가 없으면 기다리지 않고 바로 "지금은 없음"을 돌려주는 모드)으로 다룹니다(6절).

1. 연재 전체의 지도

먼저 한 장으로 보겠습니다. nginx 소스는 크게 세 부분으로 나눠 읽으면 편합니다.

연재 전체의 지도. 세 구역으로 나뉜다. A 준비 구역에는 기동·cycle(4편), 모듈·디렉티브(5편), 메모리 풀·자료구조(3편)가 있다. B 프로세스와 이벤트 구역에는 마스터·워커(6편)와 이벤트 루프(7편)가 있다. C HTTP 요청 하나 구역에는 요청 읽기(8편)에서 phase·location(9편)으로, 거기서 필터·정적 파일(10편) 또는 upstream·proxy(11편)로 가고, upstream·proxy는 부하 분산·keepalive·stream(12편)으로 이어진다. A에서 B로, B에서 C로 화살표가 있다. 연재의 지도입니다. 3~12편은 이 그림의 상자 이름으로 "2편 지도의 ○○ 부분"을 가리킵니다. 노랑은 메모리, 파랑은 설정, 초록은 프로세스·이벤트, 회색은 요청 처리입니다.

지도의 상자 구역 주로 읽는 곳 nginx.conf에서 보던 것 글
메모리 풀·자료구조 A src/core/ngx_palloc.c, ngx_buf.h, ngx_string.h connection_pool_size, request_pool_size 3편
기동·cycle A src/core/nginx.c, ngx_cycle.c, ngx_conf_file.c nginx -t, -c, -g 4편
모듈·디렉티브 A src/core/ngx_module.c, src/http/ngx_http.c load_module, 모든 디렉티브 5편
마스터·워커 B src/os/unix/ngx_process_cycle.c, ngx_process.c worker_processes, nginx -s reload 6편
이벤트 루프 B src/event/ngx_event.c, modules/ngx_epoll_module.c events {}, worker_connections 7편
요청 읽기 C src/http/ngx_http_request.c, ngx_http_parse.c server_name, client_header_buffer_size 8편
phase·location C src/http/ngx_http_core_module.c, ngx_http.c location, rewrite, allow/deny 9편
필터·정적 파일 C ngx_http_static_module.c, ngx_http_write_filter_module.c root, gzip, sendfile 10편
upstream·proxy C src/http/ngx_http_upstream.c, ngx_http_proxy_module.c proxy_pass, proxy_read_timeout 11편
부하 분산·keepalive·stream C ngx_http_upstream_round_robin.c, src/stream/ upstream {}, keepalive, stream {} 12편

이 글의 나머지는 지도 전체에 걸쳐 있는 뼈대 다섯 가지를 차례로 봅니다 — 프로세스(2절), 모듈(3절), cycle(4절), 설정 문맥(5절), 이벤트 루프(6절). 마지막으로 HTTP·stream·mail이 같은 코어 위에 어떻게 서 있는지를 봅니다(7절).

2. 프로세스 — 마스터 하나, 워커 여럿

main()의 끝: 어느 쪽 루프로 들어가나

main()은 설정을 읽어 cycle을 만든 뒤(4편), 마지막에 두 루프 가운데 하나로 들어가 다시 나오지 않습니다.

    if (ngx_process == NGX_PROCESS_SINGLE) {
        ngx_single_process_cycle(cycle);

    } else {
        ngx_master_process_cycle(cycle);
    }

    return 0;
}

src/core/nginx.c 399–407행

어느 쪽인지는 master_process 디렉티브가 정합니다. 바로 위에서 ccf->master가 켜져 있으면 ngx_process를 NGX_PROCESS_MASTER로 바꿉니다(346–348행). 기본값은 켬입니다 — ngx_core_module_init_conf()의 ngx_conf_init_value(ccf->master, 1);(1192행). master_process off;로 두면 프로세스 하나가 이벤트 루프를 직접 돌리는 ngx_single_process_cycle()로 갑니다. gdb로 디버깅할 때 편해서 뒤의 글들에서 가끔 씁니다.

같은 함수에 worker_processes의 기본값도 있습니다. ngx_conf_init_value(ccf->worker_processes, 1);(1196행) — 적지 않으면 워커는 하나입니다.

마스터: 워커를 만들고, 시그널을 기다린다

    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);

    ngx_start_worker_processes(cycle, ccf->worker_processes,
                               NGX_PROCESS_RESPAWN);
    ngx_start_cache_manager_processes(cycle, 0);
    /* ... */
    for ( ;; ) {
        /* ... */
        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "sigsuspend");

        sigsuspend(&set);

        ngx_time_update();
        /* ... */
        if (ngx_reconfigure) {
            ngx_reconfigure = 0;
            /* ... */
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");

            cycle = ngx_init_cycle(cycle);
            if (cycle == NULL) {
                cycle = (ngx_cycle_t *) ngx_cycle;
                continue;
            }

            ngx_cycle = cycle;
            ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,
                                                   ngx_core_module);
            ngx_start_worker_processes(cycle, ccf->worker_processes,
                                       NGX_PROCESS_JUST_RESPAWN);
            ngx_start_cache_manager_processes(cycle, 1);

            /* allow new processes to start */
            ngx_msleep(100);

            live = 1;
            ngx_signal_worker_processes(cycle,
                                        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
        }

src/os/unix/ngx_process_cycle.c 128–266행

마스터가 하는 일은 이것이 전부라고 해도 됩니다.

  1. worker_processes 개수만큼 워커를 만듭니다. ngx_start_worker_processes()는 반복문에서 ngx_spawn_process(cycle, ngx_worker_process_cycle, …, "worker process", type)을 부르고(358–371행), ngx_spawn_process() 안에서 socketpair()로 부모·자식 사이의 통신 통로(채널)를 만든 뒤 fork()합니다. 자식은 넘겨받은 함수 포인터 proc, 곧 ngx_worker_process_cycle()을 부릅니다(src/os/unix/ngx_process.c 186–200행).
  2. sigsuspend()에서 잠듭니다. 시그널이 오면 깨어납니다.
  3. 시그널 처리기 ngx_signal_handler()는 일을 하지 않고 전역 플래그만 세웁니다 — SIGHUP이면 ngx_reconfigure = 1(src/os/unix/ngx_process.c 364–365행), SIGQUIT이면 ngx_quit = 1, SIGCHLD(자식이 끝남)면 ngx_reap = 1. 깨어난 루프가 플래그를 보고 실제 일을 합니다. 시그널 처리기 안에서는 할 수 있는 일이 매우 제한되기 때문에 쓰는 흔한 방식입니다.
  4. reload(ngx_reconfigure)라면 ngx_init_cycle(cycle)로 옛 cycle을 받아 새 cycle을 만들고, 새 워커를 띄우고, 옛 워커에게 "그만"(NGX_SHUTDOWN_SIGNAL, 곧 QUIT)을 보냅니다.

nginx -s reload를 치면 새로 뜬 nginx 프로세스가 pid 파일에서 마스터의 pid를 읽어 kill(pid, SIGHUP)을 보내고 끝납니다(ngx_signal_process() → ngx_os_signal_process(), src/core/ngx_cycle.c 1097–1145행). 이름(reload)과 시그널(SIGHUP)의 짝은 signals[] 표에 있습니다(src/os/unix/ngx_process.c 39–83행).

reload 때 프로세스 사이에 오가는 것. 마스터가 fork로 옛 워커를 만든 뒤 sigsuspend에서 잠들고, 옛 워커는 epoll_wait에서 기다린다. 운영자 셸이 nginx -s reload로 마스터에게 SIGHUP을 보내면, ngx_signal_handler가 ngx_reconfigure를 1로 세우고, 마스터가 ngx_init_cycle로 새 cycle을 만든 뒤 새 워커를 fork한다. 그리고 채널로 옛 워커에게 NGX_CMD_QUIT을 보낸다. 옛 워커는 gracefully shutting down 상태로 리스닝 소켓을 닫고 남은 연결을 마친 뒤 끝나고, 마스터는 SIGCHLD를 받아 ngx_reap_children을 부른다. reload 한 번의 큰 순서입니다. 새 워커가 먼저 뜨고 옛 워커가 나중에 끝나므로 받던 연결이 끊기지 않습니다. 자세한 것은 6편에서 봅니다.

마스터가 워커에게 "그만"을 전할 때는 시그널보다 채널을 먼저 씁니다. ngx_signal_worker_processes()는 QUIT을 NGX_CMD_QUIT 명령으로 바꿔 채널에 쓰고(src/os/unix/ngx_process_cycle.c 471행, 519행), 채널에 쓰지 못했을 때만 kill()로 시그널을 보냅니다(534행). 워커 쪽 ngx_channel_handler()가 명령을 읽어 ngx_quit = 1을 세웁니다(1091–1093행). 채널은 워커의 이벤트 루프에 소켓 하나로 등록되어 있으므로, 워커는 "연결이 왔다"와 똑같은 방식으로 "마스터가 말을 걸었다"를 알게 됩니다.

워커: 이벤트 루프를 끝없이 돈다

static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
    ngx_int_t worker = (intptr_t) data;

    ngx_process = NGX_PROCESS_WORKER;
    ngx_worker = worker;

    ngx_worker_process_init(cycle, worker);

    ngx_setproctitle("worker process");

    for ( ;; ) {

        if (ngx_exiting) {
            if (ngx_event_no_timers_left() == NGX_OK) {
                ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
                ngx_worker_process_exit(cycle);
            }
        }

        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "worker cycle");

        ngx_process_events_and_timers(cycle);

        if (ngx_terminate) {
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
            ngx_worker_process_exit(cycle);
        }

        if (ngx_quit) {
            ngx_quit = 0;
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
                          "gracefully shutting down");
            ngx_setproctitle("worker process is shutting down");

            if (!ngx_exiting) {
                ngx_exiting = 1;
                ngx_set_shutdown_timer(cycle);
                ngx_close_listening_sockets(cycle);
                ngx_close_idle_connections(cycle);
                ngx_event_process_posted(cycle, &ngx_posted_events);
            }
        }
        /* ... */
    }
}

src/os/unix/ngx_process_cycle.c 728–779행

워커의 몸통은 ngx_process_events_and_timers() 한 줄을 끝없이 부르는 반복문입니다. 그 한 줄이 6절의 이벤트 루프 한 바퀴입니다. 바퀴 사이사이에 마스터와 같은 이름의 플래그(ngx_terminate, ngx_quit, ngx_reopen)를 봅니다. ngx_quit을 받으면 곧바로 끝나지 않고 ngx_exiting = 1로 바꾼 뒤, 리스닝 소켓을 닫고 쉬는 연결을 닫고, 남은 일이 없어질 때(ngx_event_no_timers_left())까지 계속 돕니다. 이것이 로그의 "gracefully shutting down"입니다.

그 전에 한 번 부르는 ngx_worker_process_init()은 user 디렉티브의 사용자로 권한을 내리고(그래서 워커는 nobody로 돕니다), 모든 모듈의 init_process 콜백을 부릅니다(934–940행). 이벤트 모듈의 init_process인 ngx_event_process_init()이 여기서 연결 배열을 만들고 리스닝 소켓을 epoll에 등록합니다(6절).

예제 1: ps와 gdb로 두 루프 확인하기

이 글의 예제는 모두 기준 커밋(9a7439fd79)을 그대로 빌드한 nginx 1.31.9로 도커 컨테이너(Debian bookworm, --with-debug)에서 실행했습니다. 실행 스크립트는 ex/u02-architecture/run.sh 하나이고, 출력의 === 줄은 스크립트가 찍은 제목입니다. 설정은 워커 둘에 events·http·server·location을 하나씩 둔 가장 작은 것입니다.

# 이 글을 위해 쓴 예제입니다 (nginx 의 소스가 아닙니다)
# 2편: 마스터 하나 + 워커 둘, events·http·server·location 문맥을 하나씩 둔 가장 작은 설정
worker_processes 2;
error_log /tmp/u02/error.log notice;
pid /tmp/u02/nginx.pid;

events {
    worker_connections 1024;
}

http {
    access_log off;

    server {
        listen 8080;

        location = /hello {
            return 200 "hello from $nginx_version\n";
        }

        location = /status {
            stub_status;
        }
    }
}

기동한 뒤 ps로 프로세스를 보고, 마스터와 워커에 gdb를 붙여 bt(지금 어느 함수에서 멈춰 있는지 호출 순서를 거꾸로 찍는 명령)를 했습니다.

nginx -c /work/nginx.conf
ps -o pid,ppid,user,nlwp,cmd -C nginx
gdb -batch -p "$MASTER" -ex 'bt'
gdb -batch -p "$WORKER" -ex 'bt'
=== 3. start: one master, two workers ===
hello from 1.31.9
    PID    PPID USER     NLWP CMD
     45       1 root        1 nginx: master process nginx -c /work/nginx.conf
     46      45 nobody      1 nginx: worker process
     47      45 nobody      1 nginx: worker process
...
=== 5. where is each process waiting? (gdb bt) ===
--- master (pid 45) ---
#0  0x00007f94ed8482d5 in sigsuspend () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x000055a5987780be in ngx_master_process_cycle (cycle=0x55a599b7d120) at src/os/unix/ngx_process_cycle.c:163
#2  0x000055a59872d1c0 in main (argc=3, argv=0x7ffdcc903668) at src/core/nginx.c:403
--- worker (pid 46) ---
#0  0x00007f94ed914ef3 in epoll_wait () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x000055a59877c608 in ngx_epoll_process_events (cycle=0x55a599b7d120, timer=18446744073709551615, flags=1) at src/event/modules/ngx_epoll_module.c:800
#2  0x000055a5987690b9 in ngx_process_events_and_timers (cycle=0x55a599b7d120) at src/event/ngx_event.c:248
#3  0x000055a598779a4c in ngx_worker_process_cycle (cycle=0x55a599b7d120, data=0x0) at src/os/unix/ngx_process_cycle.c:751
#4  0x000055a598776148 in ngx_spawn_process (cycle=0x55a599b7d120, proc=0x55a598779956 <ngx_worker_process_cycle>, data=0x0, name=0x55a59889af0a "worker process", respawn=-3) at src/os/unix/ngx_process.c:199
#5  0x000055a598778826 in ngx_start_worker_processes (cycle=0x55a599b7d120, n=2, type=-3) at src/os/unix/ngx_process_cycle.c:366
#6  0x000055a598777eef in ngx_master_process_cycle (cycle=0x55a599b7d120) at src/os/unix/ngx_process_cycle.c:130
#7  0x000055a59872d1c0 in main (argc=3, argv=0x7ffdcc903668) at src/core/nginx.c:403

pid와 주소는 실행마다 다릅니다.

헷갈리기 쉬운 곳: 마스터와 워커의 cycle 주소가 같다

위 출력에서 마스터의 cycle=0x55a599b7d120과 워커의 cycle=0x55a599b7d120이 같습니다. 같은 메모리를 함께 쓰는 것이 아닙니다. fork()는 부모의 주소 공간을 통째로 복사하므로 자식에서도 같은 주소에 같은 내용의 cycle이 있을 뿐이고, 그 뒤 한쪽이 고친 것은 다른 쪽에 보이지 않습니다. 워커들이 정말로 함께 보는 값(upstream_zone, limit_req_zone, accept_mutex 같은 것)은 따로 공유 메모리에 둡니다(3편에서 맛보기, 12편에서 upstream_zone).

3. 모든 것이 모듈이다

모듈 하나 = ngx_module_t 변수 하나

nginx에서 기능을 더하는 단위는 모듈이고, 모듈은 C에서 이 구조체 하나입니다.

struct ngx_module_s {
    ngx_uint_t            ctx_index;
    ngx_uint_t            index;

    char                 *name;

    ngx_uint_t            spare0;
    ngx_uint_t            spare1;

    ngx_uint_t            version;
    const char           *signature;

    void                 *ctx;
    ngx_command_t        *commands;
    ngx_uint_t            type;

    ngx_int_t           (*init_master)(ngx_log_t *log);

    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);

    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);
    void                (*exit_thread)(ngx_cycle_t *cycle);
    void                (*exit_process)(ngx_cycle_t *cycle);

    void                (*exit_master)(ngx_cycle_t *cycle);

    uintptr_t             spare_hook0;
    /* ... */
};

src/core/ngx_module.h 223–258행

필드를 세 무리로 나누면 읽기 쉽습니다.

무리 필드 무엇
번호 index, ctx_index, name 전체 모듈 배열에서의 번호, 같은 종류 안에서의 번호, 이름. 시작할 때 채워진다
무엇을 하나 type, ctx, commands 모듈의 종류, 종류마다 모양이 다른 모듈 컨텍스트(콜백 묶음), 이 모듈이 처리하는 디렉티브 표
언제 불리나 init_master … exit_master 프로세스의 생애에서 불리는 콜백. 대부분의 모듈은 NULL

ctx가 void *인 것이 요점입니다. 종류가 CORE인 모듈이면 ngx_core_module_t(이름, create_conf, init_conf)를 가리키고(src/core/ngx_module.h 261–265행), HTTP 모듈이면 ngx_http_module_t(create_loc_conf, merge_loc_conf 등 콜백 8개)를 가리킵니다. 코어는 type을 보고 ctx를 알맞은 구조체로 바꿔 읽습니다. C에는 상속이 없으니 "공통 머리 + void * + 종류 표시"로 다형성을 흉내 내는 것입니다(C의 구조체와 함수 포인터가 낯설다면 httpd 2편이 도움이 됩니다).

모듈의 종류는 네 글자다

type에 들어가는 값은 이렇게 정의되어 있습니다.

종류 정의 값 주석 정의된 곳
CORE NGX_CORE_MODULE 0x45524F43 "CORE" src/core/ngx_conf_file.h 70행
CONF NGX_CONF_MODULE 0x464E4F43 "CONF" src/core/ngx_conf_file.h 71행
EVENT NGX_EVENT_MODULE 0x544E5645 "EVNT" src/event/ngx_event.h 396행
HTTP NGX_HTTP_MODULE 0x50545448 "HTTP" src/http/ngx_http_config.h 39행
STREAM NGX_STREAM_MODULE 0x4d525453 "STRM" src/stream/ngx_stream.h 315행
MAIL NGX_MAIL_MODULE 0x4C49414D "MAIL" src/mail/ngx_mail.h 355행

값을 바이트로 쪼개 보면 아스키 글자입니다. 0x45524F43은 45 52 4F 43 = E R O C이고, x86처럼 작은 쪽부터 저장하는 CPU(리틀 엔디언)의 메모리에는 43 4F 52 45 = C O R E 순서로 놓입니다. 그래서 메모리 덤프에서 모듈 구조체를 보면 "CORE"라는 글자가 그대로 보입니다. 종류를 구분하는 데는 아무 수나 써도 되지만, 이렇게 해 두면 디버거로 메모리를 볼 때 알아보기 쉽습니다.

CORE 모듈이 블록을 열고, 그 안을 다른 종류가 맡는다

http {} 블록은 누가 처리할까요. HTTP 모듈이 아니라 CORE 모듈 ngx_http_module입니다.

static ngx_command_t  ngx_http_commands[] = {

    { ngx_string("http"),
      NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
      ngx_http_block,
      0,
      0,
      NULL },

      ngx_null_command
};


static ngx_core_module_t  ngx_http_module_ctx = {
    ngx_string("http"),
    NULL,
    NULL
};


ngx_module_t  ngx_http_module = {
    NGX_MODULE_V1,
    &ngx_http_module_ctx,                  /* module context */
    ngx_http_commands,                     /* module directives */
    NGX_CORE_MODULE,                       /* module type */
    NULL,                                  /* init master */
    NULL,                                  /* init module */
    NULL,                                  /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};

src/http/ngx_http.c 86–119행

모듈 하나를 정의하는 전형적인 모양입니다. 디렉티브 표(ngx_command_t 배열, ngx_null_command로 끝남), 모듈 컨텍스트, 그리고 그 둘을 묶은 ngx_module_t. 맨 앞의 NGX_MODULE_V1 매크로가 ctx_index·index를 "아직 안 정함"으로, name을 NULL로 채웁니다(src/core/ngx_module.h 216–218행).

이 모듈의 디렉티브는 http 하나이고, 맨 바깥(NGX_MAIN_CONF)에 놓이는 블록(NGX_CONF_BLOCK)이며 인자가 없습니다(NGX_CONF_NOARGS). 설정 파서가 http {를 만나면 ngx_http_block()을 부르고, 이 함수가 HTTP 종류의 모듈들을 세어(ngx_count_modules(cf->cycle, NGX_HTTP_MODULE)) 설정 구조체를 만들게 한 뒤 블록 안을 읽습니다(5절). events {}는 CORE 모듈 ngx_events_module이(src/event/ngx_event.c 84–89행), stream {}은 ngx_stream_module이(src/stream/ngx_stream.c 57–62행), mail {}은 ngx_mail_module이(src/mail/ngx_mail.c 32–37행) 같은 방식으로 엽니다.

모듈의 종류. ngx_modules 배열에 이 빌드의 모듈 89개가 있다. 그 가운데 CORE 9개가 맨 바깥 main 문맥을 맡고, CONF 1개(ngx_conf_module, include 디렉티브)가 따로 있다. CORE 가운데 ngx_events_module이 events 블록을 열어 EVNT 2개(ngx_event_core_module, ngx_epoll_module)에, ngx_http_module이 http 블록을 열어 HTTP 57개에, ngx_stream_module이 stream 블록을 열어 STRM 20개에 이어진다. 파란 상자가 블록을 여는 CORE 모듈입니다. 블록 안의 디렉티브는 그 블록의 종류에 속한 모듈만 처리합니다. 숫자는 예제 2에서 센 이 빌드의 값입니다.

디렉티브 표 한 줄: ngx_command_t

commands 배열의 원소는 이렇게 생겼습니다.

struct ngx_command_s {
    ngx_str_t             name;
    ngx_uint_t            type;
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    ngx_uint_t            conf;
    ngx_uint_t            offset;
    void                 *post;
};

src/core/ngx_conf_file.h 77–84행

필드 무엇 http 디렉티브의 값
name 디렉티브 이름 "http"
type 놓일 수 있는 문맥 비트 + 인자 개수 비트 + 블록 여부 (5절) NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS
set 이 디렉티브를 만나면 부를 함수 ngx_http_block
conf HTTP 같은 블록 안에서, main·srv·loc 가운데 어느 설정 구조체에 쓸지 0
offset 그 구조체 안의 어느 필드에 쓸지(offsetof) 0
post 값 검사 같은 덧붙임 NULL

nginx.conf의 한 줄은 결국 "이름이 같은 ngx_command_t를 찾아 set 함수를 부르는 것"입니다. 많은 디렉티브는 set에 ngx_conf_set_flag_slot(on/off), ngx_conf_set_msec_slot(시간) 같은 표준 setter를 쓰고 offset으로 필드 자리만 알려 줍니다. 예를 들어 master_process는 ngx_conf_set_flag_slot과 offsetof(ngx_core_conf_t, master)입니다(src/core/nginx.c 51–56행). 디렉티브 한 줄이 설정 구조체의 필드가 되기까지는 5편에서 자세히 봅니다.

예제 2: 이 빌드에 들어간 모듈을 종류별로 세기

모듈 배열 ngx_modules[]와 이름 배열 ngx_module_names[]는 configure가 만드는 objs/ngx_modules.c에 있습니다(1편). nginx를 실행하지 않은 채 gdb로 바이너리를 열어 배열을 훑었습니다. type의 네 바이트를 작은 쪽부터 글자로 찍습니다.

# 이 글을 위해 쓴 예제입니다 (nginx 의 소스가 아닙니다)
# 실행 전의 nginx 바이너리에서 모듈 배열(objs/ngx_modules.c 가 만든 것)을 훑어
# 순번, 이름(ngx_module_names[]), type 값(16진)과 그 네 바이트를 글자로 찍는다.
set $i = 0
while ngx_modules[$i] != 0
  set $t = ngx_modules[$i]->type
  printf "%2d %-36s 0x%08lx %c%c%c%c\n", $i, ngx_module_names[$i], $t, $t & 0xff, ($t >> 8) & 0xff, ($t >> 16) & 0xff, ($t >> 24) & 0xff
  set $i = $i + 1
end

ex/u02-architecture/modules.gdb이고, gdb -batch -x /work/modules.gdb /opt/nginx/sbin/nginx로 돌립니다.

=== 1. modules compiled into this binary (from ngx_modules[] / ngx_module_names[]) ===
 0 ngx_core_module                      0x45524f43 CORE
 1 ngx_errlog_module                    0x45524f43 CORE
 2 ngx_conf_module                      0x464e4f43 CONF
 3 ngx_openssl_module                   0x45524f43 CORE
 4 ngx_openssl_cache_module             0x45524f43 CORE
 5 ngx_regex_module                     0x45524f43 CORE
 6 ngx_events_module                    0x45524f43 CORE
 7 ngx_event_core_module                0x544e5645 EVNT
 8 ngx_epoll_module                     0x544e5645 EVNT
 9 ngx_thread_pool_module               0x45524f43 CORE
10 ngx_http_module                      0x45524f43 CORE
11 ngx_http_core_module                 0x50545448 HTTP
12 ngx_http_log_module                  0x50545448 HTTP
...
66 ngx_http_range_body_filter_module    0x50545448 HTTP
67 ngx_http_not_modified_filter_module  0x50545448 HTTP
68 ngx_stream_module                    0x45524f43 CORE
69 ngx_stream_core_module               0x4d525453 STRM
70 ngx_stream_log_module                0x4d525453 STRM
...
88 ngx_stream_upstream_zone_module      0x4d525453 STRM
--- count by type ---
     57 HTTP
     20 STRM
      9 CORE
      2 EVNT
      1 CONF
total: 89

헷갈리기 쉬운 곳: 실행 전에는 name이 비어 있다

처음에는 gdb 스크립트에서 ngx_modules[$i]->name을 찍으려 했습니다. 그런데 이 필드는 실행 전에는 NULL입니다. 위에서 본 NGX_MODULE_V1이 name을 NULL로 두고, 실행이 시작되면 ngx_preinit_modules()가 index와 name을 채웁니다.

    for (i = 0; ngx_modules[i]; i++) {
        ngx_modules[i]->index = i;
        ngx_modules[i]->name = ngx_module_names[i];
    }

src/core/ngx_module.c 30–33행

그래서 예제는 같은 순서로 만들어진 ngx_module_names[]를 읽습니다. index(전체 배열의 번호)도 이때 정해지고, ctx_index(같은 종류 안의 번호)는 http {} 같은 블록을 읽을 때 ngx_count_modules()가 정합니다. 두 번호가 어디에 쓰이는지는 다음 절에서 봅니다.

4. ngx_cycle_t — 실행 중인 nginx의 전부

cycle에 매달린 것

struct ngx_cycle_s {
    void                  ****conf_ctx;
    ngx_pool_t               *pool;

    ngx_log_t                *log;
    ngx_log_t                 new_log;
    /* ... */
    ngx_connection_t         *free_connections;
    ngx_uint_t                free_connection_n;

    ngx_module_t            **modules;
    ngx_uint_t                modules_n;
    /* ... */
    ngx_array_t               listening;
    ngx_array_t               paths;
    /* ... */
    ngx_list_t                open_files;
    ngx_list_t                shared_memory;

    ngx_uint_t                connection_n;
    ngx_uint_t                files_n;

    ngx_connection_t         *connections;
    ngx_event_t              *read_events;
    ngx_event_t              *write_events;

    ngx_cycle_t              *old_cycle;

    ngx_str_t                 conf_file;
    /* ... */
};

src/core/ngx_cycle.h 39–86행

필드 무엇 nginx.conf와의 관계 자세히
conf_ctx 모든 모듈의 설정 구조체로 가는 길의 뿌리 파일 전체 아래, 5편
pool 이 cycle이 쓰는 메모리 풀. cycle을 버리면 한꺼번에 사라진다 3편
modules, modules_n 이 cycle의 모듈 목록(load_module로 읽은 동적 모듈 포함) load_module 5편
listening 리스닝 소켓 배열 listen 4편, 7편
connections, read_events, write_events, connection_n 워커마다 미리 만들어 두는 연결·이벤트 배열과 그 크기 worker_connections 7편
free_connections, free_connection_n 그 가운데 비어 있는 것들의 목록과 개수 7편
shared_memory 워커들이 함께 쓰는 공유 메모리 영역 목록 *_zone 디렉티브 4편
old_cycle reload 때 이 cycle을 만들 때 받은 옛 cycle nginx -s reload 4편

현재 cycle은 전역 변수 ngx_cycle이 가리킵니다(2절의 마스터 코드에서 reload 뒤 ngx_cycle = cycle;). 함수 대부분이 cycle을 첫 인자로 받는 것도 이 구조체가 "지금의 nginx" 그 자체이기 때문입니다.

conf_ctx: 포인터 네 겹의 이유

void ****conf_ctx는 nginx 소스에서 가장 유명한 줄일 것입니다. 별이 넷인 것은 이 배열의 칸마다 매달리는 것의 모양이 모듈 종류마다 다르기 때문입니다. 출발점은 ngx_init_cycle()입니다.

    cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));

src/core/ngx_cycle.c 200행

칸 수는 ngx_max_module(정적 모듈 수 + 동적 모듈 자리 128, src/core/ngx_module.c 36행)이고, 칸 번호는 모듈의 index 입니다. 곧 이어 CORE 모듈의 create_conf를 불러 자기 칸을 채웁니다.

    for (i = 0; cycle->modules[i]; i++) {
        if (cycle->modules[i]->type != NGX_CORE_MODULE) {
            continue;
        }

        module = cycle->modules[i]->ctx;

        if (module->create_conf) {
            rv = module->create_conf(cycle);
            if (rv == NULL) {
                ngx_destroy_pool(pool);
                return NULL;
            }
            cycle->conf_ctx[cycle->modules[i]->index] = rv;
        }
    }

src/core/ngx_cycle.c 233–248행

ngx_core_module의 칸(0번)에는 ngx_core_conf_t(worker_processes, user, pid …)가 곧바로 들어갑니다. 그런데 ngx_http_module은 create_conf가 NULL이라(3절 발췌의 ngx_http_module_ctx) 이때는 칸이 비어 있습니다. 그 칸은 설정 파일에서 http {를 만났을 때 ngx_http_block()이 채웁니다. 들어가는 것은 설정 구조체 하나가 아니라 배열 셋을 가리키는 구조체입니다.

typedef struct {
    void        **main_conf;
    void        **srv_conf;
    void        **loc_conf;
} ngx_http_conf_ctx_t;

src/http/ngx_http_config.h 17–21행

세 배열의 칸 수는 HTTP 모듈의 수(ngx_http_max_module)이고, 칸 번호는 모듈의 ctx_index 입니다(src/http/ngx_http.c 150–181행). server {}를 만날 때마다 main_conf는 http의 것을 그대로 가리키고 srv_conf·loc_conf 배열만 새로 만들며(src/http/ngx_http_core_module.c 3037–3048행), location {}도 같은 식으로 자기 loc_conf 배열을 만듭니다. events {}의 칸은 그 중간 모양으로, void * 한 칸이 이벤트 모듈 배열을 가리킵니다(src/event/ngx_event.c 973–983행).

ngx_cycle_t에서 모듈 설정까지 가는 길. ngx_cycle_t의 conf_ctx 배열은 칸 번호가 모듈의 index다. 0번 칸(ngx_core_module)은 ngx_core_conf_t를 바로 가리킨다. ngx_events_module의 칸은 void 포인터 한 칸을 거쳐 이벤트 모듈 배열(칸 번호는 ctx_index)을 가리킨다. 10번 칸(ngx_http_module)은 ngx_http_conf_ctx_t를 가리키고, 이것이 main_conf, srv_conf, loc_conf 세 배열(각 57칸)을 가리킨다. main_conf는 http 블록에 하나, srv_conf는 server 블록마다, loc_conf는 location 블록마다 한 벌씩 있다. 칸 번호가 두 단계라는 것이 요점입니다. conf_ctx는 index로, 그 아래의 종류별 배열은 ctx_index로 찾습니다. 0·10·57은 예제 3에서 찍은 이 빌드의 값입니다.

그래서 요청을 처리하는 HTTP 모듈은 자기 설정을 이렇게 꺼냅니다.

#define ngx_http_get_module_main_conf(r, module)                             \
    (r)->main_conf[module.ctx_index]
#define ngx_http_get_module_srv_conf(r, module)  (r)->srv_conf[module.ctx_index]
#define ngx_http_get_module_loc_conf(r, module)  (r)->loc_conf[module.ctx_index]

src/http/ngx_http_config.h 55–58행

요청 구조체 r이 "이 요청에 맞는 server·location의 배열"을 들고 있고, 모듈은 자기 ctx_index 칸만 봅니다. 요청이 어느 location에 걸리느냐에 따라 r->loc_conf가 다른 배열을 가리키게 되는 것이 location 찾기의 결과입니다(9편). httpd에서 ap_get_module_config(r->per_dir_config, &my_module)이 모듈 번호로 void * 배열을 찾던 것과 같은 생각입니다(httpd 3편).

예제 3: 워커 안에서 cycle 들여다보기

돌고 있는 워커에 gdb를 붙여 cycle의 필드와 모듈 번호를 찍었습니다.

gdb -batch -p "$WORKER" \
    -ex 'p ngx_cycle->modules_n' \
    -ex 'p ngx_cycle->connection_n' \
    -ex 'p ngx_cycle->free_connection_n' \
    -ex 'p ngx_cycle->listening.nelts' \
    -ex 'p ngx_core_module.index' \
    -ex 'p ((ngx_core_conf_t *) ngx_cycle->conf_ctx[ngx_core_module.index])->worker_processes' \
    -ex 'p ngx_http_module.index' \
    -ex 'p ngx_http_core_module.index' \
    -ex 'p ngx_http_core_module.ctx_index' \
    -ex 'p ngx_http_max_module' \
    -ex 'p ngx_event_max_module'
=== 6. the cycle seen from a worker (gdb print) ===
$1 = 89
$2 = 1024
$3 = 1022
$4 = 1
$5 = 0
$6 = 2
$7 = 10
$8 = 11
$9 = 0
$10 = 57
$11 = 2
찍은 것 값 읽는 법
modules_n 89 예제 2에서 센 모듈 수와 같다
connection_n 1024 worker_connections 1024; 그대로
free_connection_n 1022 아직 아무도 접속하지 않았는데 2개를 쓰고 있다 — 리스닝 소켓 하나와 마스터와의 채널 하나
listening.nelts 1 listen 8080; 하나
conf_ctx[0]의 worker_processes 2 ngx_core_module.index가 0이고, 그 칸의 ngx_core_conf_t에 설정값이 들어 있다
ngx_http_module.index / ngx_http_core_module.index 10 / 11 예제 2의 배열 순번과 같다
ngx_http_core_module.ctx_index 0 HTTP 모듈 가운데 첫째 — main_conf[0]이 core 모듈의 것이다
ngx_http_max_module / ngx_event_max_module 57 / 2 종류별 모듈 수 = 종류별 배열의 칸 수

free_connection_n이 1022인 이유는 소스에서 확인했습니다. 워커가 시작할 때 ngx_event_process_init()이 리스닝 소켓마다 ngx_get_connection()으로 연결 하나를 쓰고(src/event/ngx_event.c 809행), ngx_add_channel_event()가 채널 소켓에 하나를 씁니다(src/os/unix/ngx_channel.c 215행). nginx에서 "연결"(ngx_connection_t)은 클라이언트 연결만이 아니라 이벤트 루프가 지켜보는 소켓 하나를 뜻합니다. 그래서 worker_connections에는 리스닝 소켓과 프록시할 때의 백엔드 연결도 들어갑니다.

헷갈리기 쉬운 곳: worker_connections의 기본값은 512다

배포판의 기본 nginx.conf에는 흔히 worker_connections 1024;가 적혀 있지만, 적지 않았을 때의 기본값은 512입니다. ngx_event_core_init_conf()가 ngx_conf_init_uint_value(ecf->connections, DEFAULT_CONNECTIONS);로 채우고(src/event/ngx_event.c 1330–1331행), DEFAULT_CONNECTIONS는 512입니다(13행). 같은 함수에서 accept_mutex의 기본값도 꺼짐(ngx_conf_init_value(ecf->accept_mutex, 0);, 1339행)입니다. 옛 글에서 "accept_mutex는 기본으로 켜져 있다"고 본 기억이 있다면 이 판의 소스와 다릅니다. 이것이 연결 분배에 주는 영향은 7편에서 봅니다.

5. 설정 문맥 — main, events, http, server, location

문맥은 두 값의 짝이다

설정 파서는 지금 어느 블록 안을 읽고 있는지를 ngx_conf_t의 두 필드로 압니다.

블록을 여는 함수들이 블록 안을 읽기 직전에 이 둘을 바꿉니다.

블록 바꾸는 곳 module_type cmd_type
파일 맨 바깥 ngx_init_cycle() — src/core/ngx_cycle.c 273–274행 NGX_CORE_MODULE NGX_MAIN_CONF
events {} ngx_events_block() — src/event/ngx_event.c 1003–1004행 NGX_EVENT_MODULE NGX_EVENT_CONF
http {} ngx_http_block() — src/http/ngx_http.c 238–239행 NGX_HTTP_MODULE NGX_HTTP_MAIN_CONF
server {} ngx_http_core_server() — src/http/ngx_http_core_module.c 3100행 (그대로 HTTP) NGX_HTTP_SRV_CONF
location {} ngx_http_core_location() — 3358행 (그대로 HTTP) NGX_HTTP_LOC_CONF
upstream {} ngx_http_upstream() — src/http/ngx_http_upstream.c 6502행 (그대로 HTTP) NGX_HTTP_UPS_CONF
stream {} ngx_stream_block() — src/stream/ngx_stream.c 189–190행 NGX_STREAM_MODULE NGX_STREAM_MAIN_CONF

문맥 비트는 type 필드의 윗자리 한 바이트에 들어갑니다. ngx_conf_file.h 머리의 주석이 type을 "TT FF AAAA"(문맥 · 플래그 · 인자 개수)로 나눠 설명합니다(src/core/ngx_conf_file.h 16–20행).

#define NGX_HTTP_MAIN_CONF        0x02000000
#define NGX_HTTP_SRV_CONF         0x04000000
#define NGX_HTTP_LOC_CONF         0x08000000
#define NGX_HTTP_UPS_CONF         0x10000000
#define NGX_HTTP_SIF_CONF         0x20000000
#define NGX_HTTP_LIF_CONF         0x40000000
#define NGX_HTTP_LMT_CONF         0x80000000

src/http/ngx_http_config.h 41–47행

SIF는 server 안의 if, LIF는 location 안의 if, LMT는 limit_except 블록입니다. 디렉티브는 놓여도 되는 문맥의 비트를 OR로 모아 둡니다. 몇 가지를 보면 nginx 문서의 "Context:" 줄이 바로 이 비트라는 것을 알 수 있습니다.

디렉티브 type의 문맥 비트 문서의 Context 정의된 곳
worker_processes NGX_MAIN_CONF main src/core/nginx.c 79–84행
worker_connections NGX_EVENT_CONF events src/event/ngx_event.c 123–128행
listen NGX_HTTP_SRV_CONF server src/http/ngx_http_core_module.c 290–295행
keepalive_timeout NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF http, server, location src/http/ngx_http_core_module.c 519–524행
proxy_pass NGX_HTTP_LOC_CONF|NGX_HTTP_LIF_CONF|NGX_HTTP_LMT_CONF location, if in location, limit_except src/http/modules/ngx_http_proxy_module.c 210–215행
include NGX_ANY_CONF any src/core/ngx_conf_file.c 21–22행

설정 문맥과 비트. 파일 맨 바깥 main은 module_type이 CORE, cmd_type이 NGX_MAIN_CONF 0x01000000이다. 그 안에 events(EVNT, NGX_EVENT_CONF 0x02000000)와 http(HTTP, NGX_HTTP_MAIN_CONF 0x02000000)가 있다. http 안에 server(NGX_HTTP_SRV_CONF 0x04000000)와 upstream(NGX_HTTP_UPS_CONF 0x10000000)이, server 안에 location(NGX_HTTP_LOC_CONF 0x08000000)이 있다. ngx_conf_handler는 모듈 type이 cf->module_type과 같은지, cmd->type과 cf->cmd_type의 비트가 겹치는지 두 가지를 검사하고, 어긋나면 directive is not allowed here를 낸다. events와 http의 문맥 비트가 같은 0x02000000이라는 데 주목하세요. 비트만으로는 둘을 구별할 수 없어서 모듈 종류를 함께 검사합니다.

검사하는 곳: ngx_conf_handler()

토큰 한 줄(디렉티브 이름과 인자)을 읽을 때마다 ngx_conf_handler()가 불립니다. 예외는 types {}·map {}처럼 블록을 여는 함수가 자기 처리 함수(cf->handler)를 걸어 둔 블록 안으로, 그 안의 줄은 이 검사를 거치지 않고 그 함수로 갑니다(src/core/ngx_conf_file.c 298–325행).

    for (i = 0; cf->cycle->modules[i]; i++) {

        cmd = cf->cycle->modules[i]->commands;
        if (cmd == NULL) {
            continue;
        }

        for ( /* void */ ; cmd->name.len; cmd++) {

            if (name->len != cmd->name.len) {
                continue;
            }

            if (ngx_strcmp(name->data, cmd->name.data) != 0) {
                continue;
            }

            found = 1;

            if (cf->cycle->modules[i]->type != NGX_CONF_MODULE
                && cf->cycle->modules[i]->type != cf->module_type)
            {
                continue;
            }

            /* is the directive's location right ? */

            if (!(cmd->type & cf->cmd_type)) {
                continue;
            }
            /* ... */
            /* set up the directive's configuration context */

            conf = NULL;

            if (cmd->type & NGX_DIRECT_CONF) {
                conf = ((void **) cf->ctx)[cf->cycle->modules[i]->index];

            } else if (cmd->type & NGX_MAIN_CONF) {
                conf = &(((void **) cf->ctx)[cf->cycle->modules[i]->index]);

            } else if (cf->ctx) {
                confp = *(void **) ((char *) cf->ctx + cmd->conf);

                if (confp) {
                    conf = confp[cf->cycle->modules[i]->ctx_index];
                }
            }

            rv = cmd->set(cf, cmd, conf);
            /* ... */
        }
    }

    if (found) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "\"%s\" directive is not allowed here", name->data);

        return NGX_ERROR;
    }

    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                       "unknown directive \"%s\"", name->data);

src/core/ngx_conf_file.c 374–494행

  1. 모든 모듈의 디렉티브 표를 처음부터 훑어 이름이 같은 것을 찾습니다. 해시가 아니라 선형 검색입니다. 설정을 읽을 때 한 번만 하는 일이라 빠를 필요가 없습니다.
  2. 이름이 같으면 found = 1을 세운 뒤 두 가지를 봅니다. 모듈의 종류가 지금 블록의 module_type과 같은가(CONF 모듈은 예외 — 그래서 include는 어디서나 됩니다), 디렉티브의 type에 지금 블록의 cmd_type 비트가 있는가.
  3. 통과하면 설정 구조체를 찾아 set 함수에 넘깁니다. 여기서 4절의 번호 둘이 쓰입니다. 맨 바깥 디렉티브는 conf_ctx[index]를, HTTP 블록 안의 디렉티브는 cmd->conf(main·srv·loc 가운데 어느 배열인지의 offsetof)로 배열을 고른 뒤 [ctx_index] 칸을 씁니다.
  4. 끝까지 맞는 것이 없으면, 이름은 있었는지(found)에 따라 "not allowed here" 또는 "unknown directive"를 냅니다.

예제 4: 문맥이 틀린 디렉티브와 nginx -T

events { }와 server { listen 8081; }만 둔 작은 설정을 만들고, http {} 바로 안(파일의 5행)에 네 가지 디렉티브를 하나씩 넣어 nginx -t(설정 검사만 하고 끝내는 옵션)를 돌렸습니다.

=== 2. directives in the wrong context (nginx -t) ===
--- core directive: http { worker_processes 4; } ---
nginx: [emerg] "worker_processes" directive is not allowed here in /tmp/u02/bad.conf:5
--- event directive: http { worker_connections 512; } ---
nginx: [emerg] "worker_connections" directive is not allowed here in /tmp/u02/bad.conf:5
--- loc directive: http { proxy_pass http://127.0.0.1:9000; } ---
nginx: [emerg] "proxy_pass" directive is not allowed here in /tmp/u02/bad.conf:5
--- typo: http { proxy_pas http://127.0.0.1:9000; } ---
nginx: [emerg] unknown directive "proxy_pas" in /tmp/u02/bad.conf:5

nginx -T는 검사에 더해, 읽은 설정 파일 전체(include한 파일까지)를 # configuration file …: 머리와 함께 그대로 찍습니다. 운영 중인 서버의 "실제로 읽힌 설정"을 볼 때 쓰는 그 옵션입니다.

=== 4. nginx -T (first lines) ===
nginx: the configuration file /work/nginx.conf syntax is ok
nginx: configuration file /work/nginx.conf test is successful
# configuration file /work/nginx.conf:
# 이 글을 위해 쓴 예제입니다 (nginx 의 소스가 아닙니다)
# 2편: 마스터 하나 + 워커 둘, events·http·server·location 문맥을 하나씩 둔 가장 작은 설정
worker_processes 2;
error_log /tmp/u02/error.log notice;
pid /tmp/u02/nginx.pid;

"syntax is ok" 줄은 4절의 ngx_init_cycle()이 ngx_conf_parse()를 마친 직후에 찍습니다(src/core/ngx_cycle.c 292–295행). 파일을 읽어 둔 내용을 덤프하는 장치(cycle->config_dump)와 -t·-T의 처리 순서는 4편에서 봅니다.

헷갈리기 쉬운 곳: http에 적은 값은 server·location으로 "물려받는" 것이 아니다

keepalive_timeout을 http {}에 한 번 적으면 모든 location에 적용됩니다. 이것을 "아래 블록이 위 블록의 값을 들여다본다"고 생각하기 쉽지만, 소스는 그렇게 하지 않습니다. 4절에서 본 것처럼 http {}·server {}·location {}마다 설정 구조체를 따로 만들어 두고, 설정 파일을 다 읽은 뒤 merge_srv_conf·merge_loc_conf 콜백이 "아래 것이 비어 있으면 위 것으로 채운다"를 미리 해 둡니다. 요청을 처리할 때는 위를 들여다보지 않고 자기 loc_conf만 봅니다. 이 merge가 어떻게 도는지는 5편에서 봅니다.

6. 한 워커 안의 이벤트 루프

한 바퀴의 순서

2절의 워커 반복문이 부르던 ngx_process_events_and_timers()입니다.

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ngx_uint_t  flags;
    ngx_msec_t  timer, delta;

    if (ngx_timer_resolution) {
        timer = NGX_TIMER_INFINITE;
        flags = 0;

    } else {
        timer = ngx_event_find_timer();
        flags = NGX_UPDATE_TIME;
        /* ... */
    }

    if (ngx_use_accept_mutex) {
        /* ... */
    }

    if (!ngx_queue_empty(&ngx_posted_next_events)) {
        ngx_event_move_posted_next(cycle);
        timer = 0;
    }

    delta = ngx_current_msec;

    (void) ngx_process_events(cycle, timer, flags);

    delta = ngx_current_msec - delta;

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "timer delta: %M", delta);

    ngx_event_process_posted(cycle, &ngx_posted_accept_events);

    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }

    ngx_event_expire_timers();

    ngx_event_process_posted(cycle, &ngx_posted_events);
}

src/event/ngx_event.c 194–264행

워커의 이벤트 루프 한 바퀴. ngx_worker_process_cycle의 무한 반복 안에서 1. ngx_event_find_timer로 가장 가까운 타이머까지 남은 시간을 구하고, 2. ngx_process_events(epoll_wait)로 그 시간만큼 잠들어 기다리고, 3. 준비된 이벤트마다 ev->handler(ev)를 바로 부르고, 4. posted accept 이벤트를 처리하고, 5. ngx_event_expire_timers로 시간이 다 된 타이머의 handler를 부르고, 6. posted 이벤트를 처리한 뒤, 종료 플래그(ngx_quit, ngx_terminate, ngx_reopen)를 확인하고 처음으로 돌아간다. 연결 수천 개는 ngx_connection_t 배열이고 각자 read·write 이벤트와 handler를 가진다. 한 워커가 하는 일의 전부입니다. 잠드는 곳은 2번 한 곳뿐이고, 나머지는 모두 잠들지 않고 바로 끝나야 합니다.

  1. 얼마나 잘까 — ngx_event_find_timer()가 가장 가까운 타이머(타임아웃)까지 남은 밀리초를 돌려줍니다. 타이머는 레드블랙 트리(넣고 빼고 가장 작은 값을 찾는 것이 모두 빠른 균형 이진 트리)에 들어 있습니다. 예제 1의 timer=18446744073709551615는 걸린 타이머가 하나도 없을 때의 값이었습니다.
  2. 기다린다 — ngx_process_events는 함수가 아니라 매크로로, ngx_event_actions.process_events라는 함수 포인터입니다(src/event/ngx_event.h 371행). epoll 모듈이 초기화될 때 ngx_event_actions = ngx_epoll_module_ctx.actions;로 자기 함수들을 꽂아 두므로(src/event/modules/ngx_epoll_module.c 369행), 리눅스에서는 ngx_epoll_process_events() → epoll_wait()이 됩니다. FreeBSD에서는 같은 자리에 kqueue 모듈이 꽂힙니다.
  3. 처리한다 — epoll이 돌려준 이벤트마다 그 이벤트의 handler 함수 포인터를 부릅니다(rev->handler(rev);, src/event/modules/ngx_epoll_module.c 901행). 리스닝 소켓이면 ngx_event_accept, HTTP 연결이면 요청을 읽는 함수가 거기 꽂혀 있습니다(7절). accept_mutex를 쥔 경우에만 바로 부르지 않고 posted 큐(나중에 처리할 이벤트를 모아 두는 줄)에 넣었다가 4·6번에서 처리합니다.
  4. ~6. 뒷정리 — 모아 둔 accept 이벤트, 시간이 다 된 타이머(proxy_read_timeout 같은 것이 여기서 터집니다), 모아 둔 일반 이벤트를 차례로 처리합니다.

핸들러는 절대 오래 붙잡지 않는다는 약속 위에서 이 구조가 돕니다. 읽을 데이터가 아직 없으면 논블로킹 recv()가 EAGAIN을 돌려주고, 핸들러는 "다음에 읽을 수 있게 되면 다시 불러 줘"라고 epoll에 등록한 채 바로 돌아옵니다. 그래서 스레드 하나가 수천 개 연결을 번갈아 처리할 수 있습니다. 반대로 핸들러 하나가 디스크를 오래 읽거나 무거운 계산을 하면 그 워커의 다른 연결이 모두 멈춥니다 — nginx에 aio·스레드 풀(ngx_thread_pool_module, 예제 2의 9번)이 있는 이유입니다. 한 바퀴를 줄 단위로 읽는 것은 7편에서 합니다.

예제 5: 연결 300개, 스레드는 하나

정말 스레드 하나가 연결 수백 개를 쥐고 있는지 보았습니다. 파이썬으로 8080에 TCP 연결 300개를 열고 아무 요청도 보내지 않은 채 붙잡아 둔 다음, stub_status와 각 워커의 스레드 수, 그리고 gdb로 각 워커가 쓰고 있는 연결 수(connection_n - free_connection_n)를 찍었습니다.

# 이 글을 위해 쓴 예제입니다 (nginx 의 소스가 아닙니다)
# 8080 에 TCP 연결을 N 개 열어 둔 채(요청은 보내지 않는다) 기다리다가, stdin 이 닫히면 끝낸다.
import socket, sys

n = int(sys.argv[1])
socks = []
for _ in range(n):
    s = socket.create_connection(("127.0.0.1", 8080))
    socks.append(s)
print("opened %d idle connections" % len(socks), flush=True)
sys.stdin.read()
for s in socks:
    s.close()
=== 7. 300 idle connections, still one thread per worker ===
opened 300 idle connections
Active connections: 301 
server accepts handled requests
 303 303 3 
Reading: 0 Writing: 1 Waiting: 300 
worker 46: threads=1 connections in use=126
worker 47: threads=1 connections in use=178

7. HTTP, stream, mail — 같은 코어 위의 세 층

마지막으로 폴더를 층으로 쌓아 보겠습니다.

nginx 소스의 층. 맨 아래 src/os/unix가 fork·시그널·채널·소켓·파일 I/O·sendfile을 맡고, 그 위 src/core가 cycle·설정 파서·모듈 표·풀·버퍼·연결·로그를, 그 위 src/event가 이벤트 코어를 맡는다. ngx_event_accept가 연결을 받아 ls->handler(c)를 부른다. 맨 위에 src/http(ls->handler = ngx_http_init_connection), src/stream(ls->handler = ngx_stream_init_connection), src/mail(ls->handler = ngx_mail_init_connection, 이 빌드에는 없음)이 나란히 있다. 위층은 아래층을 가져다 씁니다. 세 프로토콜이 이벤트 코어와 만나는 곳은 리스닝 소켓의 handler 함수 포인터 하나입니다.

이벤트 코어는 HTTP를 모릅니다. 연결을 받는 ngx_event_accept()는 새 연결을 만든 뒤 리스닝 소켓에 걸린 함수를 부를 뿐입니다.

        ls->handler(c);

src/event/ngx_event_accept.c 312행

이 함수 포인터를 채우는 쪽이 각 프로토콜입니다. http {} 안의 listen으로 만든 소켓에는 ls->handler = ngx_http_init_connection;(src/http/ngx_http.c 1882행), stream {} 안의 것에는 ngx_stream_init_connection(src/stream/ngx_stream.c 1002행), mail {}에는 ngx_mail_init_connection(src/mail/ngx_mail.c 326행)이 들어갑니다. 1편의 "요청 하나가 지나는 큰 길"에서 ngx_event_accept 다음이 ngx_http_init_connection이었던 것이 이 한 줄 때문입니다.

HTTP (src/http) stream (src/stream) mail (src/mail)
블록 http {} stream {} mail {}
모듈 종류 NGX_HTTP_MODULE "HTTP" NGX_STREAM_MODULE "STRM" NGX_MAIL_MODULE "MAIL"
이 빌드의 모듈 수 57 20 0 (빌드하지 않음)
문맥 main · srv · loc (+ ups, if, limit_except) main · srv (+ ups) main · srv
처리 단계 phase 11개 (9편) phase 7개(NGX_STREAM_POST_ACCEPT_PHASE … NGX_STREAM_LOG_PHASE, src/stream/ngx_stream.h 85–91행) 프로토콜별 상태 기계
프록시 upstream (11편) stream upstream (12편) —

stream은 HTTP의 구조를 거의 그대로 TCP/UDP에 옮겨 놓은 것입니다. 문맥 비트도 HTTP처럼 0x02000000부터 다시 씁니다(NGX_STREAM_MAIN_CONF 등, src/stream/ngx_stream.h 317–319행) — 5절의 "같은 비트를 종류 검사로 구별한다"가 여기서도 통합니다. 이 연재는 HTTP를 따라가고, stream은 12편에서 HTTP와 나란히 놓고 봅니다.

헷갈리기 쉬운 곳: httpd의 MPM에 해당하는 것을 찾으면 없다

httpd를 읽은 사람은 "연결을 받는 방식을 갈아 끼우는 MPM 같은 것이 nginx에도 있겠지" 하고 찾게 됩니다. nginx에는 그런 모듈이 없습니다. 프로세스 구조(마스터 하나 + 워커 N개, 워커마다 스레드 하나)는 src/os/unix/ngx_process_cycle.c에 고정되어 있고, 갈아 끼울 수 있는 것은 그 아래의 이벤트 방식(ngx_epoll_module, kqueue 등 — events {}의 use 디렉티브)뿐입니다. httpd의 event MPM이 "쉬는 keep-alive 연결은 epoll에 맡기고 일할 때만 스레드를 붙인다"로 다가간 곳에서, nginx는 처음부터 "모든 연결을 epoll에 맡기고 스레드는 하나"로 출발한 셈입니다.

이 글에서 뺀 것

지도를 한 장으로 유지하려고 뺀 것들입니다.

정리

개념 한 줄 요약
지도 A 준비(3·4·5편) → B 프로세스와 이벤트(6·7편) → C HTTP 요청 하나(8~12편)
마스터 ngx_master_process_cycle(). 워커를 fork()하고 sigsuspend()에서 잠든다. 시그널 처리기는 플래그만 세우고, reload면 ngx_init_cycle()로 새 cycle → 새 워커 → 옛 워커에 QUIT
워커 ngx_worker_process_cycle(). 스레드 하나로 ngx_process_events_and_timers()를 끝없이 부른다. 마스터의 명령은 채널(socketpair)로 받는다
모듈 ngx_module_t 하나. type(종류) · ctx(종류별 콜백 묶음) · commands(디렉티브 표)
모듈 종류 "CORE"·"CONF"·"EVNT"·"HTTP"·"STRM"·"MAIL" 네 글자의 아스키 값. 이 빌드는 89개(HTTP 57, STRM 20, CORE 9, EVNT 2, CONF 1)
블록 events·http·stream·mail 블록은 CORE 모듈의 디렉티브이고, 블록 안은 그 종류의 모듈이 맡는다
ngx_command_t 이름 · 문맥과 인자 비트(type) · set 함수 · 어느 구조체(conf)의 어느 필드(offset)
ngx_cycle_t 실행 중인 nginx의 전부. conf_ctx, modules, listening, connections. reload마다 새로 만든다
conf_ctx index로 찾는 칸 → (HTTP면) ngx_http_conf_ctx_t → ctx_index로 찾는 main·srv·loc 배열
설정 문맥 module_type + cmd_type 비트의 짝. ngx_conf_handler()가 종류와 비트를 검사하고 "not allowed here" / "unknown directive"를 낸다
이벤트 루프 타이머 찾기 → epoll_wait → ev->handler() → 타이머 만료 → posted 이벤트. 핸들러는 기다리지 않는다
프로토콜 층 이벤트 코어는 ls->handler(c)만 부른다. HTTP·stream·mail이 그 자리에 자기 함수를 꽂는다

다음에 볼 것

지도를 접고, 지도의 모든 상자가 밑에 깔고 있는 것부터 읽습니다. 다음 글은 메모리 풀과 기본 자료구조입니다. 이 글에서 cycle·설정 구조체·연결이 모두 ngx_pcalloc(pool, …)로 만들어지는 것을 보았습니다. 그 풀이 작은 할당과 큰 할당을 어떻게 나눠 받는지, 요청 하나가 끝나면 메모리가 어떻게 한꺼번에 사라지는지, 그리고 이 글에서 몇 번 지나친 ngx_str_t·ngx_array_t·ngx_buf_t·ngx_chain_t가 어떻게 생겼는지를 디버그 로그와 gdb로 확인합니다.

nginx 소스 파헤치기 3: 메모리 풀과 기본 자료구조 — ngx_pool_t, ngx_str_t, ngx_buf_t와 ngx_chain_t

앞서 끝낸 두 연재에서도 같은 자리에 전체 지도를 폈습니다. httpd 3편에서 httpd는 코어가 훅을 정의하고 MPM과 모듈이 거기에 함수를 거는 구조였고, 모듈마다 module 구조체 하나에 설정 생성·병합 함수와 디렉티브 표를 두었습니다. nginx의 ngx_module_t·ngx_command_t는 그것과 아주 닮았지만, 연결을 받는 부분은 갈아 끼우는 MPM이 아니라 고정된 마스터·워커와 이벤트 루프입니다(httpd 8편). Tomcat 2편의 지도에서는 server.xml의 요소가 자바 객체의 부모·자식 나무가 되었습니다. nginx.conf의 블록도 겹겹이 들어가지만, 객체 나무 대신 conf_ctx에서 시작하는 포인터 배열이 됩니다. Tomcat의 NIO 엔드포인트가 셀렉터 하나로 많은 연결을 지켜보다 일할 때 스레드 풀에 넘기는 것과 달리, nginx 워커는 지켜보는 것과 일하는 것을 같은 스레드에서 합니다.

다른 글