지금까지 확인한 동작은 다음과 같다.
fork()
)80번(HTTP), 443번(HTTPS) 포트를 논블로킹 모드로 오픈하고 있다고 하자. fork()
의 경우 부의 FD 테이블을 복사하므로 워커들도 이 포트들과 바인딩된 소켓의 FD를 가지고 있다. 그런데 실제로 요청이 들어오면 어떻게 될까? 이제 epoll
과 같은 이벤트 모델이 사용된다.
각 워커는 초기화 과정에서 자기 자신의 epoll
인스턴스를 생성한 후, 부모 프로세스, 그러니까 마스터 프로세스로부터 받은 리스닝 소켓을 자신의 epoll
인스턴스에 등록하고, epoll_wait()
를 호출해 등록한 소켓에서의 I/O 이벤트가 준비되기를 대기한다.
클라이언트가 서버에 HTTP 요청을 보낸다고 해보자.
SYN
)이 들어온다.epoll_wait()
를 호출 중인 워커 중 하나를 깨운다.accept()
하고, 만들어진 연결 소켓을 다시 자신의 epoll
인스턴스에 등록해, 이후 전달될 클라이언트의 실제 요청 처리에 사용한다./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_module | HTTP 서비스의 기반 기능 제공 |
ngx_mail_module | 메일 프록시 (SMTP, IMAP, POP3) 지원 |
ngx_stream_module | TCP/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_modules
과 ngx_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의 핵심 철학인 모듈화에 대한 냄새를 맡아볼 수 있는 기회가 됐다.