[Nginx] 4-1. 이벤트 루프와 처리 (1)

Park Yeongseo·2025년 4월 3일
0

Nginx

목록 보기
7/9
post-thumbnail

이전에 썼던 1. 이벤트 드리븐 서버에서 대표적인 I/O 멀티플렉싱 인터페이스와 동작 방식을 봤다. 그런데 Nginx는 리눅스만이 아니라, Windows, macOS, Solaris 등 다양한 OS와 플랫폼을 지원하고, OS에 따라 지원하는 이벤트 모델에는 차이가 있다. 계속해서 epoll만 사용할 것 같기는 하지만, 그래도 종류와 이름 정도는 간단히 슥 훑고 가자.

이벤트 모델지원 OS특징
epollLinux 2.5.44+고성능, Edge/Level Triggered 지원, 대규모 연결에 최적화
kqueueBSD 계열, macOS고성능, 다양한 이벤트 필터링
pollPOSIX 호환 OSselect보다는 낫지만 성능이 썩 좋지는 않음
selectPOSIX 호환 OS구식, FD_SET 크기 제한, 성능 낮음
IOCPWindowsWindows 전용 비동기 I/O 모델
eventportSolaris 10+최신 Solaris에서 사용

1. 이벤트 루프

워커 프로세스는 초기화 후 계속해서 루프를 돌며 이벤트를 처리한다.

// /src/os/unix/ngx_process_cycle.c
static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
	/**
		워커 프로세스 초기화
	*/

    for ( ;; ) {//이벤트 루프
		/**
			종료 중 처리
		*/

		//이벤트 처리
        ngx_process_events_and_timers(cycle);

		/**
			강제 종료 or 정상 종료 or 로그 파일 재오픈 처리
		*/
    }
}

실제로 이벤트를 대기하고 준비가 되면 처리하는 부분은 ngx_process_events_and_timers()다.

2. Thundering Herd

지금까지는 모든 워커들이 마스터로부터 상속받은 리스닝 소켓을 자신의 이벤트 모델에 등록하고, 요청이 들어오면 OS가 알아서 해당 소켓을 등록한 프로세스들 중 하나만을 깨운다고 했지만, 사실 꼭 그렇지만은 않다.

(2-1) epoll_ctl()EPOLLEXCLUSIVE 플래그

man epoll_ctl로 매뉴얼 페이지를 열어보면 epoll_event 구조체에 쓸 수 있는 플래그 목록이 주루룩 나온다. 그 중 EPOLLEXCLUSIVE 플래그는 이벤트 발생 시 동일한 타겟 이벤트를 대기 중인 것들 중 하나만을 깨우도록 하는 옵션이다. 지금까지 요청을 OS가 알아서 분배해준다고 했던 건 EPOLLEXCLUSIVE 플래그 사용을 전제로 했을 때나 그렇다.

케이스이벤트 발생 시
모두 EPOLLEXCLUSIVE로 타겟 등록정확히 하나에 이벤트 발생 알림
일부만 EPOLLEXCLUSIVE로 타겟 등록해당 플래그를 사용하지 않는 것들에는 모두 알림을 보내고, 플래그를 사용한 것들 중에는 정확히 하나만 깨움
EPOLLEXCLUSIVE 미사용모두에게 이벤트 발생 알림

실제로도 OS에서 epoll을 사용할 수 있고(Linux 2.5.44+), EPOLLEXCLUSIVE 플래그도 사용할 수 있다면(Linux 4.5+), Nginx에서는 각 워커에서 ngx_events_moduleinit_process을 호출할 때 상속받은 리스닝 소켓을 EPOLLEXCLUSIVE로 등록한다.

// /src/event/ngx_event.c
static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
	// ...

	 /* for each listening socket */
	ls = cycle->listening.elts;
    for (i = 0; i < cycle->listening.nelts; i++) {
		//...
		
#if (NGX_HAVE_EPOLLEXCLUSIVE)

        if ((ngx_event_flags & NGX_USE_EPOLL_EVENT)
            && ccf->worker_processes > 1)
        {
            ngx_use_exclusive_accept = 1;

            if (ngx_add_event(rev, NGX_READ_EVENT, NGX_EXCLUSIVE_EVENT)
                == NGX_ERROR)
            {
                return NGX_ERROR;
            }

            continue;
        }

#endif
		//...
	}
	return NGX_OK;
}

(2-2) Thundering Herd 문제

그렇다면 EPOLLEXCLUSIVE 플래그를 사용할 수 없는 경우에는 어떨까?

리스닝 소켓에 연결 요청이 들어오는 순간, 대기 중인 모든 워커 프로세스가 이벤트 준비 메시지를 받고 accept()하기 위해 경쟁하게 된다.

[클라이언트] ---- SYN ----→ [서버]
                        ↳ Worker A: accept() 성공 (연결 처리)
                        ↳ Worker B: accept() → EAGAIN (실패)
                        ↳ Worker C: accept() → EAGAIN (실패)

이때 한 워커만이 실제로 accept()에 성공하고, 나머지는 accept()를 호출하기는 하지만 곧 실패해버린다.

이렇게 실제로 작업을 처리하는 주체는 하나뿐인데 하나의 이벤트를 대기 중이던 다수의 프로세스(혹은 스레드)가 불필요하게 깨어나 자원을 낭비하게 되는 문제를 Thundering Herd 문제라 한다.

그럼 늘 이런 비효율성을 감내해야만 할까? 그렇지는 않다. Nginx의 Thundering Herd 문제 해결 전략에 대해서는 다음 글에서.

0개의 댓글

관련 채용 정보