[Nginx] 3-1. 워커 프로세스 초기화와 모듈 (1)

Park Yeongseo·2025년 3월 29일
0

Nginx

목록 보기
4/9
post-thumbnail

1. 리뷰

지금까지 확인한 동작은 다음과 같다.

  1. 마스터 프로세스가 리스닝 소켓을 생성, 바인드하고 논블로킹 리슨함
  2. 마스터 프로세스가 워커 프로세스들을 생성(fork())

80번(HTTP), 443번(HTTPS) 포트를 논블로킹 모드로 오픈하고 있다고 하자. fork()의 경우 부의 FD 테이블을 복사하므로 워커들도 이 포트들과 바인딩된 소켓의 FD를 가지고 있다. 그런데 실제로 요청이 들어오면 어떻게 될까? 이제 epoll과 같은 이벤트 모델이 사용된다.

각 워커는 초기화 과정에서 자기 자신의 epoll 인스턴스를 생성한 후, 부모 프로세스, 그러니까 마스터 프로세스로부터 받은 리스닝 소켓을 자신의 epoll 인스턴스에 등록하고, epoll_wait()를 호출해 등록한 소켓에서의 I/O 이벤트가 준비되기를 대기한다.

클라이언트가 서버에 HTTP 요청을 보낸다고 해보자.

  1. 80번(HTTP) 포트로 연결 요청(SYN)이 들어온다.
  2. 커널이 연결을 소켓의 백로그에 저장하고 epoll_wait()를 호출 중인 워커 중 하나를 깨운다.
  3. 선택된 워커는 연결을 accept()하고, 만들어진 연결 소켓을 다시 자신의 epoll 인스턴스에 등록해, 이후 전달될 클라이언트의 실제 요청 처리에 사용한다.

2. 소스 코드 읽기 (continue)

/src/os/unix/ngx_process_cycle.c

static void
ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker)
{
	//...

	//환경 변수 설정
    if (ngx_set_environment(cycle, NULL) == NULL) {
        /* fatal */
        exit(2);
    }

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

	/**
		워커 프로세스의 CPU 스케줄링 우선도(nice값) 지정 
    */
    if (worker >= 0 && ccf->priority != 0) {
        if (setpriority(PRIO_PROCESS, 0, ccf->priority) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "setpriority(%d) failed", ccf->priority);
        }
    }

	/**
		워커 프로세스의 최대 FD 수, 코어 덤프 크기를 nginx.conf에 적은 경우 설정
	*/

	//root 권한으로 실행되어 있음
    if (geteuid() == 0) {
	    /**
			워커 프로세스의 권한을 하강
		*/
    }

	//CPU affinity를 설정한 경우
    if (worker >= 0) {
        cpu_affinity = ngx_get_cpu_affinity(worker);

        if (cpu_affinity) {
            ngx_setaffinity(cpu_affinity, cycle->log);
        }
    }

	/**
		코어 덤프 생성 허용
		작업 디렉터리 설정
	*/

	//차단된 시그널 해제
    sigemptyset(&set);

    if (sigprocmask(SIG_SETMASK, &set, NULL) == -1) {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "sigprocmask() failed");
    }

	//난수생성기 설정
    tp = ngx_timeofday();
    srandom(((unsigned) ngx_pid << 16) ^ tp->sec ^ tp->msec);

	//모듈들 중 초기화 함수 포인터가 주어진 경우 초기화를 진행
    for (i = 0; cycle->modules[i]; i++) {
        if (cycle->modules[i]->init_process) {
            if (cycle->modules[i]->init_process(cycle) == NGX_ERROR) {
                /* fatal */
                exit(2);
            }
        }
    }

	//현재 FD 테이블에서, 자신의 것이 아닌 UDS를 닫음
    for (n = 0; n < ngx_last_process; n++) {

        if (ngx_processes[n].pid == -1) {
            continue;
        }

        if (n == ngx_process_slot) {
            continue;
        }

        if (ngx_processes[n].channel[1] == -1) {
            continue;
        }

        if (close(ngx_processes[n].channel[1]) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "close() channel failed");
        }
    }

	//자신의 FD 테이블에서 마스터의 소켓도 닫음
    if (close(ngx_processes[ngx_process_slot].channel[0]) == -1) {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "close() channel failed");
    }

	//마스터 프로세스와 통신하기 위한 채널 소켓을 이벤트 모델에 등록
    if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,
                              ngx_channel_handler)
        == NGX_ERROR)
    {
        /* fatal */
        exit(2);
    }
}

워커 프로세스의 제한 관련 설정

워커 프로세스의 최대 FD 수 제한은 nginx.conf에서 아래와 같이 명시적으로 설정할 수 있다. 한 워커 당 최대 연결 수도 마찬가지로 nignx.conf에서 정할 수 있는데, 클라이언트와의 연결을 처리하기 위해서는 해당 연결 소켓을 FD 테이블에 넣어줘야 하므로 최대 FD 수보다는 작거나 같아야 한다. 성능 튜닝이 필요한 경우 사용한다.

worker_processes  auto;
worker_rlimit_nofile 65535;  # 워커 최대 FD 수(default: 시스템 설정을 따름)

events {
    worker_connections  8192;  # 워커 당 최대 연결 수(default: 512)
}

http {
    server {
        listen 80;
        # ...
    }
}

코어 덤프 제한도 nginx.conf에서 설정할 수 있다. 100M, 1G와 사이즈를 써도 되고, 0을 써서 코어 덤프 자체를 비활성화할 수도 있다. 비정상 종료 시의 디버깅이 필요하다면 설정하자.

worker_processes  auto;
worker_rlimit_core 0;       # 코어 덤프 비활성화
worker_rlimit_nofile 65535;

모듈

아까 코드 중간에 모듈의 초기화 프로세스를 실행하는 부분이 있었다.

// /src/os/unix/ngx_process_cycle.c:ngx_worker_process_init()	
	//모듈들 중 초기화 함수 포인터가 주어진 경우 초기화를 진행
    for (i = 0; cycle->modules[i]; i++) {
        if (cycle->modules[i]->init_process) {
            if (cycle->modules[i]->init_process(cycle) == NGX_ERROR) {
                /* fatal */
                exit(2);
            }
        }
    }

Nginx 공식 문서에서 볼 수 있듯, Nginx에는 코어 이외에도 수많은 모듈들이 있다.

모듈의 종류는 크게 빌드 방식에 따라 정적(static) 모듈과 동적(dynamic) 모듈로 나눌 수 있다. 정적 모듈은 Nginx 바이너리에 내장되고, 동적 모듈은 별도의 .so 파일로 빌드되어 load_module로 로드해야 한다.

모듈을 동적으로 빌드할지, 정적으로 빌드할지, 혹은 제거할지는 /auto/configure을 실행해 Makefile을 만들 때 아래와 같이 옵션을 줘서 처리하면 된다.

Nginx에서 공식적으로 제공하는 모듈들은 아래와 같이 컴파일에 포함시키거나 제외시키기 위한 커맨드가 따로 주어져 있다.

./configure \
    --with-http_ssl_module \ # 기본적으로 포함되지 않는 모듈을 정적 모듈로 추가
    --with-http_image_filter_module=dynamic \ # 모듈을 동적으로 빌드
    --without-http_rewrite_module  # 모듈을 제외
make && sudo make install

위에서 볼 수 있듯 일부 공식 모듈의 경우 동적 빌드를 지원하기도 하는데, 바이너리로 포함시키지 않고 .so 파일로 만들어 필요할 때 로드할 수 있도록 해 유연하게 사용하기 위함이다. 단 동적으로 빌드되었으니 nginx_conf에서 load_module로 따로 명시해줘야 한다.

load_module modules/ngx_http_ssl_module.so;

공식 모듈이 아닌 서드 파티 모듈은 다음과 같이 추가할 수 있다.

./configure \
	--add-module=PATH \ # 정적 모듈로 추가
	--add-dynamic-module=PATH # 동적 모듈로 추가
make && make install

마찬가지로 동적 모듈인 경우 nginx.conf에 추가해야 런타임에 로드할 수 있다.

load_module /path/to/modules/ngx_some_dynamic_module.so;

한편 Nginx에는 --without-* 옵션으로 제거할 수 없는, 필수적인 핵심 모듈들도 있다. 이 모듈들은 말 그대로 핵심적인 역할을 하는 모듈들로 가장 먼저 초기화되어 이외 다른 모듈들을 위한 기반을 마련한다.

핵심 모듈명(소스 코드)역할
ngx_core_module전역 설정 (worker_processes, pid 파일 경로 등) 관리
ngx_events_module이벤트 처리 모델 (epoll, kqueue 등) 초기화
ngx_http_moduleHTTP 서비스의 기반 기능 제공
ngx_mail_module메일 프록시 (SMTP, IMAP, POP3) 지원
ngx_stream_moduleTCP/UDP 스트림 프록시 (Layer 4 로드 밸런싱) 지원

NGX_HTTP_MODULE이나 NGX_EVENT_MODULE 타입도 있기는 하지만, 이 타입은 구체적인 구현체들이 가지는 타입이다.

여기서 중요한 사실. nginx.conf 사실 파일은 이 코어 모듈들을 기반으로 하는 모듈들의 설정을 계층적으로 구성하는 파일이었다!

# [1] core 모듈에 대한 설정 (ngx_core_module)
user  nginx;
worker_processes  auto;

# [2] events 모듈 블록 (ngx_events_module)
events {
	use epoll;
    worker_connections  1024;
}

# [3] http 모듈 블록 (ngx_http_module)
http {
    # [4] http 모듈 하위 계층의 서버 모듈 블록 (ngx_http_core_module)
    server {
        listen       80;
        server_name  example.com;
    }
}

2-1. Nginx의 구조 (1)에서는 마스터 & 워커 프로세스의 생성만 보면서 넘어갔지만, 사실 /src/core/nginx.c:main()을 보면 다음과 같이 모듈 초기화를 위한 사전 작업을 진행한다.

// /src/core/nginx.c
int ngx_cdecl
main(int argc, char *const *argv)
{
{
	//...
	
	//모듈 초기화를 위한 사전 작업
	if (ngx_preinit_modules() != NGX_OK) {
        return 1;
    }

	//nginx.conf의 설정에 따른 초기화 작업. src/core/ngx_cycle.c에 있다.
    cycle = ngx_init_cycle(&init_cycle);

	//...
}

아래에 ngx_modules 배열에 인덱스와 이름을 매핑하는 부분이 있다. 현재ngx_modulesngx_module_names에는 각각 컴파일된 모든 정적 모듈(이 런타임에 올라간 메모리의 주소)과 그 이름이 들어가있다.

// /src/core/ngx_module.c
ngx_int_t
ngx_preinit_modules(void)
{
    ngx_uint_t  i;

	//ngx_modules 배열은 NULL로 끝을 표시한다
    for (i = 0; ngx_modules[i]; i++) {
        ngx_modules[i]->index = i;
        ngx_modules[i]->name = ngx_module_names[i];
    }

    ngx_modules_n = i;
    ngx_max_module = ngx_modules_n + NGX_MAX_DYNAMIC_MODULES;

    return NGX_OK;
}

Q. 왜 이렇게 인덱스와 모듈명을 따로 주입해주는 걸까? 각 정적 모듈에 이름을 하드 코딩하면 안 될까?

이전에 봤던 생명 주기 초기화 함수 /src/core/ngx_cycle.c:ngx_init_cycle()에서도 모듈과 관련해서 빠진 부분이 있다.

// /src/core/ngx_cycle.c
ngx_cycle t* 
ngx_init_cycle(ngx_cycle_t *old_cycle)
{
	// ...

	//사이클 설정 컨텍스트에 모듈을 담기 위한 메모리 할당
    cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));
    if (cycle->conf_ctx == NULL) {
        ngx_destroy_pool(pool);
        return NULL;
    }

	// ...

	//이 사이클에서 정적 모듈을 사용하기 위해 복사하는 함수
	if (ngx_cycle_modules(cycle) != NGX_OK) {
        ngx_destroy_pool(pool);
        return NULL;
    }

	//복사해온 정적 모듈 중 전역 코어, 이벤트 모듈에 대한 설정 생성
    for (i = 0; cycle->modules[i]; i++) {
        if (cycle->modules[i]->type != NGX_CORE_MODULE) {
            continue;
        }

	  	/**
			각 모듈 구조체의 ctx 필드에는 해당 모듈에 특화된
			 설정 생성/초기화 함수 포인터 & 모듈별 고유 데이터가 있다.
        */
        module = cycle->modules[i]->ctx;

		//설정 생성 함수 포인터 create_conf가 NULL아 아닌 경우
        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;
        }
    }

	//...

	//커맨드 라인의 옵션으로 전달된 파라미터를 파싱
    if (ngx_conf_param(&conf) != NGX_CONF_OK) {
        environ = senv;
        ngx_destroy_cycle_pools(&conf);
        return NULL;
    }

	/**
		nginx.conf의 파일을 파싱
		nginx.conf에 load_module이 있는 경우 
		/src/core/nginx.c:ngx_load_module()을 실행해 동적 모듈을 로드함
    */
    if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {
        environ = senv;
        ngx_destroy_cycle_pools(&conf);
        return NULL;
    }

    if (ngx_test_config && !ngx_quiet_mode) {
        ngx_log_stderr(0, "the configuration file %s syntax is ok",
                       cycle->conf_file.data);
    }

	//NGX_CORE_MODULE 타입 모듈에 대한 최종 초기화
    for (i = 0; cycle->modules[i]; i++) {
        if (cycle->modules[i]->type != NGX_CORE_MODULE) {
            continue;
        }

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

        if (module->init_conf) {
            if (module->init_conf(cycle,
                                  cycle->conf_ctx[cycle->modules[i]->index])
                == NGX_CONF_ERROR)
            {
                environ = senv;
                ngx_destroy_cycle_pools(&conf);
                return NULL;
            }
        }
    }

	// ...

	// 저번에 봤던 부분
	if (ngx_open_listening_sockets(cycle) != NGX_OK) {
        goto failed;
    }

    if (!ngx_test_config) {
        ngx_configure_listening_sockets(cycle);
    }


	// ...

	// 각 모듈들에 대한 초기화. 필요한 실제 리소스를 할당함.
    if (ngx_init_modules(cycle) != NGX_OK) {
        /* fatal */
        exit(1);
    }

	//...
}

/src/core/ngx_cycle.c

ngx_int_t
ngx_cycle_modules(ngx_cycle_t *cycle)
{
    /*
     * create a list of modules to be used for this cycle,
     * copy static modules to it
     */

    cycle->modules = ngx_pcalloc(cycle->pool, (ngx_max_module + 1)
                                              * sizeof(ngx_module_t *));
    if (cycle->modules == NULL) {
        return NGX_ERROR;
    }

    ngx_memcpy(cycle->modules, ngx_modules,
               ngx_modules_n * sizeof(ngx_module_t *));

    cycle->modules_n = ngx_modules_n;

    return NGX_OK;
}

ngx_int_t
ngx_init_modules(ngx_cycle_t *cycle)
{
    ngx_uint_t  i;

	//각 모듈들에 대해 초기화 함수 포인터가 있으면 실행
    for (i = 0; cycle->modules[i]; i++) {
        if (cycle->modules[i]->init_module) {
            if (cycle->modules[i]->init_module(cycle) != NGX_OK) {
                return NGX_ERROR;
            }
        }
    }

    return NGX_OK;
}

조금 곁가지로 빠지긴 했지만, 왜 nginx.conf가 저런 모습을 띄게 된 건지 이해하고, Nginx의 핵심 철학인 모듈화에 대한 냄새를 맡아볼 수 있는 기회가 됐다.

0개의 댓글

관련 채용 정보