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

Park Yeongseo·2025년 3월 30일
0

Nginx

목록 보기
5/9
post-thumbnail

nginx.conf의 각 블록 안을 컨텍스트라 하고, 어떠한 블록에도 속하지 않는 부분을 main 컨텍스트라 한다. nginx.conf를 작성할 때에는 이 블록들의 순서도 중요한데, main 컨텍스트의 전역 설정과 events 블록은 항상 설정 파일의 최상단에 위치해야 한다.

# main context : nginx_core_module
events {
	# events context
}

http {
	# http context
}

#...

이 설정들은 OS와 직접 상호작용하는 모듈(과 그 하위 모듈)들에 대한 것으로, OS와 어떤 연관이 있는지를 간단히 보자면,

  • ngx_core_module (main 컨텍스트)
    + 프로세스 관리 및 시스템 리소스 제어
    + 워커 프로세스 수, PID 파일 경로, 프로세스 실행 권한, 파일 디스크립터 수 제한 등
  • nginx_events_module (events 컨텍스트)
    + 이벤트 모델 선택
    + OS가 저마다 제공하는 I/O 멀티플렉싱 기술을 활용
    + 연결 관리
    + 동시 연결 수 제한, accept 방식 제어 등

정도가 있다.

이 모듈들은 다른 모듈들에 인프라를 제공하는 역할을 맡기에, 가장 먼저 설정을 끝내야 한다. 이때 설정 파일의 파싱이 앞에서부터 순서대로 이루어지기 때문에, 해당 모듈들에 대한 설정 또한 파일의 최상단에 위치해야 한다. 나머지 http, mail, stream의 경우에는 유연하게 설정해도 좋다.

오늘은 ngx_events_module의 초기화 코드를 보며, 모듈이 어떻게 초기화되는지 알아보자.

1. Nginx 모듈의 객체 지향성

두 구조체 ngx_module_tngx_core_module_t를 보자. 모든 Nginx 모듈은ngx_module_t 타입으로 정의되고, 아래와 같은 공통 인터페이스를 가진다.

// /src/core/ngx_module.h
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;
    uintptr_t             spare_hook1;
    uintptr_t             spare_hook2;
    uintptr_t             spare_hook3;
    uintptr_t             spare_hook4;
    uintptr_t             spare_hook5;
    uintptr_t             spare_hook6;
    uintptr_t             spare_hook7;
};

typedef struct {
    ngx_str_t             name;
    void               *(*create_conf)(ngx_cycle_t *cycle);
    char               *(*init_conf)(ngx_cycle_t *cycle, void *conf);
} ngx_core_module_t;

// /src/core/ngx_core.h
typedef struct ngx_module_s ngx_module_t;

여기서 우리는 class가 없는 C의 언어적 한계를 뛰어 넘는, Nginx의 객체 지향적 설계 철학을 엿볼 수 있다.

  1. 클래스 메서드는 함수 포인터로 표현
  2. 상속 및 확장은 ctx 필드에 하위 구조체를 포함시켜 구현
  3. 다형성은 런타임에 함수 포인터 교체로 달성

만약 Java 느낌으로 클래스를 통해 나타낸다면 다음과 같은 모습이 될 것 같다.

//별도의 class 파일로 나뉘어져 있다고 가정 
public abstract class NgxModule {
	/**
		필드
	*/

    public abstract int initMaster(NgxLog log);
    public abstract int initModule(NgxCycle cycle);
    public abstract int initProcess(NgxCycle cycle);
    public abstract int initThread(NgxCycle cycle);
    public abstract void exitThread(NgxCycle cycle);
    public abstract void exitProcess(NgxCycle cycle);
    public abstract void exitMaster(NgxCycle cycle);

	//...
};

public abstract class NgxCoreModule extends NgxModule {
	//필드

	public abstract void createConf(NgxCycle cycle);
	public abstract char initConf(NgxCycle cycle);
}

public class NgxEventsModule extends NgxCoreModule {
	//...

	public char initConf(NgxCycle cycle){
		//구체적인 구현
	}

	//...
}

2. 모듈 초기화 과정

상-하위 모듈 사이의 의존성을 관리하기 위해 Nginx의 모듈의 초기화는 다음과 같은 순서로 이루어진다.

  1. 상위 모듈 설정 생성: 하위 모듈들이 사용할 공간을 준비
  2. 하위 모듈 설정 생성/초기화: 하위 모듈이 자신의 설정 구조체를 생성하고 상위 모듈의 배열에 등록
  3. 상위 모듈 설정 초기화: 모든 하위 모듈의 설정 이후, 상위 모듈이 설정 트리의 무결성 확인
static ngx_core_module_t  ngx_events_module_ctx = {
    ngx_string("events"),                  /* name */
    NULL,                                  /* create_conf */
    ngx_event_init_conf                    /* init_conf */
};

ngx_module_t  ngx_events_module = {
    NGX_MODULE_V1,
    &ngx_events_module_ctx,                /* module context */
    ngx_events_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
};

ngx_events_module은 위와 같이 생겼다. 눈여겨 볼 점은 create_confNULL이라는 점이다. 그 이유는 이 모듈이 이벤트 관련 설정들을 위한 컨테이너 역할만을 하는 모듈이기 때문이다.

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

	/**
		NGX_CORE_MODULE 타입 모듈들에 대한 create_conf
	*/

	/**
		nginx.conf의 파일을 파싱하고 지시어에 따른 설정을 적용
    */
    if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {
        environ = senv;
        ngx_destroy_cycle_pools(&conf);
        return NULL;
    }

	/**
		NGX_CORE_MODULE 타입 모듈들에 대한 init_conf
	*/
	// ...
}

이전의 ngx_init_cycle()에서 ngx_conf_parse()를 호출하는 부분이 있는데, /src/core/ngx_conf_file.c:ngx_conf_parse()에서는 다음과 같은 작업들이 수행된다.

  1. (최상위인 경우) nginx.conf 설정 파일을 엶.
  2. 파싱 후 현재 컨텍스트(ex. http, events)에 따른 지시어의 유효성을 검사.
  3. 지시어에 해당하는 핸들러 함수를 호출, 설정 적용.
    • 실제 해당 지시어를 가지고 있는 모듈을 초기화하고 실행하는 게 아니라, 구조화하는 역할만 하며, 실제 리소스 할당은 각 모듈 별로 init_module()이 호출될 때 수행된다.
    • http { server { location {} } }과 같이 계층을 이루는 경우, 핸들러 함수(ex. ngx_http_block())에서 재귀적으로 ngx_conf_parse()를 호출해 하위 모듈 블럭을 계층적으로 처리한다.

각 모듈에는 아래와 같은, 설정 시 사용할 수 있는 커맨드(ngx_command_t)들의 목록이 있다. ngx_conf_parse()에서는 파싱을 진행할 때, 지시어와 커맨드 명이 같으면 그에 맞는 set() 함수를 호출해 초기 설정을 위한 준비를 한다.

// ngx_conf_file.h
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;
};

#define ngx_null_command  { ngx_null_string, 0, NULL, 0, 0, NULL }

// /src/core/ngx_core.h
typedef struct ngx_command_s         ngx_command_t;

예를 들어 nginx.conf에 아래와 같이 쓰여있다고 해보자.

events {
	use epoll;
	worker_connections 1024;
}

ngx_events_module의 경우 이벤트 블록임을 나타내기 위한 events 지시어가 있다.

// /src/event/ngx_event.c
static ngx_command_t  ngx_events_commands[] = {

    { ngx_string("events"),
      NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
      ngx_events_block,
      0,
      0,
      NULL },

      ngx_null_command
};

파싱 중 이 지시어를 찾게 되면 ngx_events_block()이라는 함수가 호출되고, 아래의 하위 모듈들을 위한 설정 초기화 작업이 수행된다.

static char *
ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
	/**
		events {} 블록이 있는지 확인하고 
		로드된 모듈들 중 NGX_EVENT_TYPE의 모듈들을 찾아 인덱스를 설정하고,
		해당 모듈들에 대한 설정 초기화 작업(create_conf 및 init_conf)도 수행한다.
	*/
}

ngx_events_block()에서 찾은 NGX_EVENT_TYPE 모듈 구조체는 이벤트와 관련된 실제 처리들을 하는 구현체다. 마찬가지의 방식으로 파싱을 통해 지시어에 맞는 핸들러를 실행해 적절하게 처리한다.

static ngx_str_t  event_core_name = ngx_string("event_core");

static ngx_command_t  ngx_event_core_commands[] = {

    { ngx_string("worker_connections"),
      NGX_EVENT_CONF|NGX_CONF_TAKE1,
      ngx_event_connections,
      0,
      0,
      NULL },

    { ngx_string("use"),
      NGX_EVENT_CONF|NGX_CONF_TAKE1,
      ngx_event_use,
      0,
      0,
      NULL },

    { ngx_string("multi_accept"),
      NGX_EVENT_CONF|NGX_CONF_FLAG,
      ngx_conf_set_flag_slot,
      0,
      offsetof(ngx_event_conf_t, multi_accept),
      NULL },

    { ngx_string("accept_mutex"),
      NGX_EVENT_CONF|NGX_CONF_FLAG,
      ngx_conf_set_flag_slot,
      0,
      offsetof(ngx_event_conf_t, accept_mutex),
      NULL },

    { ngx_string("accept_mutex_delay"),
      NGX_EVENT_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_msec_slot,
      0,
      offsetof(ngx_event_conf_t, accept_mutex_delay),
      NULL },

    { ngx_string("debug_connection"),
      NGX_EVENT_CONF|NGX_CONF_TAKE1,
      ngx_event_debug_connection,
      0,
      0,
      NULL },

      ngx_null_command
};


static ngx_event_module_t  ngx_event_core_module_ctx = {
    &event_core_name,
    ngx_event_core_create_conf,            /* create configuration */
    ngx_event_core_init_conf,              /* init configuration */

    { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};


ngx_module_t  ngx_event_core_module = {
    NGX_MODULE_V1,
    &ngx_event_core_module_ctx,            /* module context */
    ngx_event_core_commands,               /* module directives */
    NGX_EVENT_MODULE,                      /* module type */
    NULL,                                  /* init master */
    ngx_event_module_init,                 /* init module */
    ngx_event_process_init,                /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};

하위 모듈(여기서는 모듈 구현체)에서 필요한 작업이 모두 끝난 후에는 init_conf()가 있는지 확인하고 실행해, 하위 모듈을 검증하고 기타 마지막 사전 설정들을 끝낸다.

static char *
ngx_event_init_conf(ngx_cycle_t *cycle, void *conf)
{
	/**
		ngx_events_module의 init_conf
		- 이벤트 관련 필수 설정 검증
		- REUSEPORT를 사용하는 경우 소켓 클로닝을 위한 준비
		  - REUSEPORT 동일한 포트를 여러 소켓이 리스닝할 수 있게 됨
		  - 워커에 같은 포트-다른 소켓을 배정함으로써 성능 최적화
		  - 여기서 소켓이 생성되는 건 아니고, 나중에 소켓 생성을 하기 위한 사전 설정만. 
	*/
}

0개의 댓글

관련 채용 정보