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

Park Yeongseo·2025년 4월 2일
0

Nginx

목록 보기
6/9
post-thumbnail

지난 글에서는 모듈의 설정을 생성하고 초기화하는 과정을 봤다. 간단히 정리하면 다음과 같다.

  • create_conf: 설정 구조체를 메모리에 생성하고 필드를 NGX_CONF_UNSET 등으로 초기화
  • ngx_conf_parse() 등: 설정 파일을 불러와 파싱하고, 설정이 있는 필드를 설정값으로 적용
  • init_conf: 설정이 없어 NGX_CONF_UNSET으로 남아있는 필드에 기본값을 적용

이렇게 설정 구조체를 만들고 나면 다음으로는 이 구조체를 바탕으로 실제로 모듈을 초기화하는 과정이 이어지는데, 간단히 어떤 것이 언제 어떤 일을 하는지 정도만 보고 넘어가자.

  • init_module: ngx_init_cycle()에서 1회 호출되어, 공유 메모리 등 모든 워커 프로세스가 공유해야 하는 정보들에 대한 전역 초기화
// /src/core/ngx_cycle.c
ngx_cycle t* 
ngx_init_cycle(ngx_cycle_t *old_cycle)
{
	// ...
	// 각 모듈들에 대한 초기화. 필요한 실제 리소스를 할당함.
    if (ngx_init_modules(cycle) != NGX_OK) {
        /* fatal */
        exit(1);
    }
}
  • init_process: 각 워커를 생성하고 난 후, ngx_worker_process_init()에서 호출해 워커프로세스 별로 필요한 초기화를 진행
// /src/os/unix/ngx_process_cycle.c
static void
ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker)
{
	//...
	
	//모듈들 중 초기화 함수 포인터가 주어진 경우 초기화를 진행
    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 테이블에서 사용하지 않는 것들을 정리한다.

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

	/**
		자신의 FD 테이블을 정리. 쓰이지 않는 것들을 제거하는 과정
	*/

	// 현재 FD 테이블에서, 자신의 것이 아닌 워커용 소켓을 닫음
    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");
        }
    }

	//마스터의 소켓도 닫음
    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);
    }

현재 만들어진 워커 프로세스의 인덱스를 i라 하자. 위와 같은 정리 과정 후 이 워커 프로세스의 FD 테이블에는 마스터로부터 상속 받은 리스닝 소켓들, channel[1][0], ..., channel[i -1][0], channel[i][1]만이 남게 된다. 이후 마스터가 남은 다른 워커들을 생성하고 ngx_pass_open_channel()을 실행하면, 이후 생성되는 워커들의 channel[j][0]도 전달받게 되긴 한다.

이제 지금까지의 초기화 과정을 정리해보고, 이렇게 만들어진 프로세스들이 어떻게 서로 메시지를 교환하는지 살펴보자. 간단히 아래와 같이 가정하자.

  1. 2개의 워크프로세스를 사용
  2. 80번(HTTP), 443번(HTTPS) 포트를 사용한다. 단 REUSEPORT 소켓을 사용하지는 않는다고 가정한다.
worker_processes: 2;

http {
    server {
        listen 80;
        listen 443 ssl;

		# 실제로는 여러 기타 설정있어야 함
    }
}

1. Nginx 아키텍처 구성 과정 정리

Nginx의 아키텍처가 구성되는 과정을 간단히 정리하면 다음과 같이 정리할 수 있다.

  1. 프로세스 시작을 위한 초기 작업 수행
  2. 설정 초기화. 리스닝 소켓 오픈. 모듈 전역 초기화
  3. 마스터 프로세스 생명 주기 시작

  1. 마스터-워커 간의 통신을 위한 UDS 페어(ch[1][0], ch[1][1]) 생성 및 설정
  2. fork()worker[1]을 생성
  3. worker[1] 초기화.
    • 기타 필수 초기화 및 워커 별 모듈 초기화
    • 마스터로부터 복사해온 FD 테이블에서 사용하지 않는 소켓을 닫음
      - 마스터가 자신에게 메시지를 보낼 때 사용하는 ch[1][0]은 닫음
    • ch[1][1] 소켓은 이벤트 모델에 등록(마스터와의 메시지 교환)
  4. 초기화 이후 worker[1]은 이벤트 루프를 돌기 시작.

  1. 마스터-워커 간의 통신을 위한 UDS 페어(ch[2][0], ch[2][1]) 생성 및 설정
  2. fork()worker[2]을 생성
  3. worker[2] 초기화.
    • 기타 필수 초기화 및 워커 별 모듈 초기화
    • 마스터로부터 복사해온 FD 테이블에서 사용하지 않는 소켓을 닫음
      - 마스터가 자신에게 메시지를 보낼 때 사용하는 ch[2][0]은 닫음
      - worker[1]이 마스터와의 통신을 위해 사용하는 ch[1][1]도 닫음
    • ch[2][1]을 이벤트 모델에 등록(마스터와의 메시지 교환)
  4. 마스터 프로세스는 ch[2][0]소켓을 worker[1]에게 전달한다. worker[1]은 이를 통해 마스터 프로세스를 거치지 않고 worker[2]로 메시지를 보낼 수 있게 된다.
  5. 초기화 이후 worker[2]은 이벤트 루프를 돌기 시작.

2. 프로세스 간 메시지 교환

(1) 마스터에서 워커로 메시지 보내기

마스터에서 worker[1]로 메시지를 보내기 위해서는 ch[1][0]에 쓰기만 하면 된다. ch[1][0]에 쓰면 곧 ch[1][1]이 읽기 준비 상태가 되고, 커널이 ch[1][1]epoll 인스턴스에 등록한 프로세스를 찾아 메시지를 전달해주고, worker[1]이 이를 읽을 수 있게 된다.

(2) 워커에서 마스터로 메시지 보내기

worker[1]에서 마스터로 메시지를 보낼 때에는 그냥 ch[1][1]에 쓰기만 하면 된다. 이벤트 모델을 통해서가 아니라, ch[1][1]과 연결된 ch[1][0]으로 곧바로 메시지를 보낼 수 있게 된다( 따지자면 커널을 거치기는 하지만).

(3) 워커에서 워커로 메시지 보내기

worker[1]ngx_pass_open_channel()을 통해 마스터 프로세스로부터 ch[2][0]을 전달받은 상태다. worker[1]에서 worker[2]로 메시지를 보내려면 단순히 ch[2][0]에 쓰기만 하면 된다. ch[2][0]에 쓰기가 완료되면 이와 연결된 ch[2][1]이 읽기 준비 상태가 되고, worker[2]는 이벤트 모델을 통해 이 메시지를 받아올 수 있게 된다.

그런데!) 사실 이 UDS 채널은 마스터(ch[i][0])에서 워커(ch[i][1])로의 단방향 통신을 위해서만 사용되며, 워커에서 마스터로, 혹은 워커에서 워커로 메시지를 보내기 위해 사용되는 경우는 없다고 한다.

그렇다면 전역 상태 변화는?) 워커 프로세스가 직접 마스터 프로세스로 메시지를 보내기 보다는, 공유 메모리를 이용한 간접적, 중앙 집중적 IPC만을 사용한다고 한다.

Q) 왜 마스터에서는 사용하지 않는 ch[i][1]을 닫지 않을까?
Q) 왜 사용하는 일이 없는데도 굳이 다른 프로세스의 ch[i][0]을 공유하는 걸까?

이제 남은 주제들과 순서는 다음과 같다.

  1. 이벤트 루프 심화: Nginx에서의 이벤트 처리는 어떻게 이뤄질까?
  2. 요청 처리 파이프라인: HTTP 요청은 실제로 어떻게 처리될까?
  3. 업스트림 로드 밸런싱: 단일 서버를 넘어, 분산 처리는 어떻게 될까?
  4. 캐시 관리: 성능 최적화를 위한 캐시 관리 전략은?
  5. HTTPS: 보안 레이어를 추가한다면?
  6. HTTP3/QUIC: 최신 기술에 대한 처리는?

열심히 써야지

0개의 댓글

관련 채용 정보