이전에 썼던 1. 이벤트 드리븐 서버에서 대표적인 I/O 멀티플렉싱 인터페이스와 동작 방식을 봤다. 그런데 Nginx는 리눅스만이 아니라, Windows, macOS, Solaris 등 다양한 OS와 플랫폼을 지원하고, OS에 따라 지원하는 이벤트 모델에는 차이가 있다. 계속해서 epoll
만 사용할 것 같기는 하지만, 그래도 종류와 이름 정도는 간단히 슥 훑고 가자.
이벤트 모델 | 지원 OS | 특징 |
---|---|---|
epoll | Linux 2.5.44+ | 고성능, Edge/Level Triggered 지원, 대규모 연결에 최적화 |
kqueue | BSD 계열, macOS | 고성능, 다양한 이벤트 필터링 |
poll | POSIX 호환 OS | select 보다는 낫지만 성능이 썩 좋지는 않음 |
select | POSIX 호환 OS | 구식, FD_SET 크기 제한, 성능 낮음 |
IOCP | Windows | Windows 전용 비동기 I/O 모델 |
eventport | Solaris 10+ | 최신 Solaris에서 사용 |
워커 프로세스는 초기화 후 계속해서 루프를 돌며 이벤트를 처리한다.
// /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()
다.
지금까지는 모든 워커들이 마스터로부터 상속받은 리스닝 소켓을 자신의 이벤트 모델에 등록하고, 요청이 들어오면 OS가 알아서 해당 소켓을 등록한 프로세스들 중 하나만을 깨운다고 했지만, 사실 꼭 그렇지만은 않다.
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_module
의 init_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;
}
그렇다면 EPOLLEXCLUSIVE
플래그를 사용할 수 없는 경우에는 어떨까?
리스닝 소켓에 연결 요청이 들어오는 순간, 대기 중인 모든 워커 프로세스가 이벤트 준비 메시지를 받고 accept()
하기 위해 경쟁하게 된다.
[클라이언트] ---- SYN ----→ [서버]
↳ Worker A: accept() 성공 (연결 처리)
↳ Worker B: accept() → EAGAIN (실패)
↳ Worker C: accept() → EAGAIN (실패)
이때 한 워커만이 실제로 accept()
에 성공하고, 나머지는 accept()
를 호출하기는 하지만 곧 실패해버린다.
이렇게 실제로 작업을 처리하는 주체는 하나뿐인데 하나의 이벤트를 대기 중이던 다수의 프로세스(혹은 스레드)가 불필요하게 깨어나 자원을 낭비하게 되는 문제를 Thundering Herd 문제라 한다.
그럼 늘 이런 비효율성을 감내해야만 할까? 그렇지는 않다. Nginx의 Thundering Herd 문제 해결 전략에 대해서는 다음 글에서.