컨텍스트를 이해하며 알아보는 Nginx 내부구조
웹서버 기능에 더해 로드밸런싱, 캐싱, 접근과 대역폭 컨트롤, 다양한 앱을 효율적으로 통합할 수 있도록하는 Nginx는 2004년 러시아 소프트웨어 개발자 Igor Sysoev에 의해 탄생하였습니다.
물론 그 이전에도 널리 쓰이는 웹서버인 Apache가 존재하였습니다. 그러나 통계에 따르면 현재(2024년) 웹서버의 34.1%가 Nginx를 사용해 운영된다고 합니다 [3].
그렇다면 후발주자인 Nginx는 어떤 이점을 제공했기에 이렇게 널리 사용될 수 있게 된 걸까요?
그것을 가능하게하는 구조는 어떻게 이뤄져 있을까요?
Nginx가 선택받은 이유
Nginx가 탄생하던 시점 전후를 살펴보면, 웹서버에 대한 높은 concurrency가 점점 요구되던 시기였습니다. 예로, 1999년 10,000 커넥션 이상이 하나의 서비스들이 점점 발생하며 웹서버에 더 높은 성능을 요구하는 C10K Problem [4]이 이슈화되었습니다.
그렇기에 Nginx는 빠르게 높아져가는 웹서버 성능에 대한 니즈를 아래 2가지 특징을 중심으로 만족시켰습니다:
- C10K Problem에서와 같이 제안된 매 요청에 새로운 프로세스나 쓰레드가 생성되어 처리하는 것이 아니라, 이벤트 기반으로 처리하여 요청량과 한정된 자원의 linear한 관계를 끊음 (즉, 한 가지 예로 요청량이 10배 늘어난다고, 메모리가 10배 필요하지 않게됨)
- Modular 구조를 통해 각 모듈은 인터페이스로 커뮤니케이션하여 개별 모듈의 최적화가 가능하거나 뛰어난 대체재가 발견되면 대체할 수 있게됨 (즉, 코딩을 잘해서 성능최적화에 유연하게 대응할 수 있게됨)
Nginx가 하는일
Nginx가 하는일의 핵심은 촘촘하게 run-loop를 유지하고, 요청 처리의 각 단계에서 해당되는 섹션의 '모듈 코드'를 실행하는 것에 있습니다. 그러한 모듈들은 OSI 모델의 애플리케이션, 프리젠테이션 레이어의 대부분을 구성하고 있습니다.
모듈은 네트워크나 스토리지로(부터) 읽거나 쓰고, 컨텐츠를 변형하고, 아웃바운드 필터링을 수행하고, 서버 사이드 액션을 적용하고, 프록시가 액티브할 경우 업스트림 서버로 요청을 전달합니다.
Nginx의 아키텍쳐
Nginx의 구조의 특징은 위에서 언급한 '선택받은 이유'와 깊게 관련이 있습니다. 10K 문제를 해결하기 위해 개발된 Nginx는 요청마다 새로운 프로세스나 쓰레드를 생성하는 모델이 아닌, 당시 개발중이던 운영체제의 이벤트 기반 메커니즘을 사용하였습니다.
또 다른 한 가지 '선택받은 이유'였던 모듈화라는 점도 더해졌습니다. 결국, Nginx는 모듈화, 이벤트 기반, 비동기, 단일 쓰레드, 논블로킹의 특징을 지니며 구현되었습니다.
실제 네트워크 커넥션은 worker라고 불리는 제한된 갯수의 단일 쓰레드 프로세스들 안의 효율적인 run-loop를 통해 처리됩니다.
코드 구조
빌드 및 실행
Nginx는 빌드 시 설정을 통해 모듈의 포함여부가 결정됩니다. 그렇기에 configure라는 커맨드를 통해 --with-select_module, --without-poll_module 등 [5]의 다양한 설정이 존재하는 것을 볼 수 있습니다. configure는 최종적으로 Makefile을 만들며, 이후 make 커맨드를 실행해 nginx 코드를 빌드할 수 있습니다.
엔트리포인트 Main과 master 프로세스
nginx의 entrypoint인 main 함수 중간에서 cycle이라는 이름으로 관리되는 구조체가 ngx_init_cycle을 통해 생성됩니다 (추후 자세히 살펴볼 예정). 여러 셋업이 진행되고 ngx_master_process_cycle을 실행하는 것을 볼 수 있습니다. ngx_master_process_cycle의 내부에서 아래 이미지에서 보이는 master와 worker 모델이 각각 어떤 역할을 하며 구성되어 있는지 살펴볼 수 있습니다.
Master, Worker 및 기타 프로세스의 실행
먼저 master 프로세스는 아래와 같은 역할을 담당합니다:
- configuration 읽기 및 검증
- 소켓 생성, 바인딩, 클로징
- 설정된 갯수의 worker 프로세스 시작, 종료, 유지
- 서비스 중단 없이 reconfigure
- 역시 중단 없이 새로운 binary 실행
worker 프로세스는 클라이언트로부터의 커넥션을 처리하고(accept, handle, process), 프록싱과 필터링 기능을 제공하는 등 nginx의 핵심 기능에 해당되는 작업들을 수행합니다. 그렇기에 nginx를 모니터링할 때에는 주로 worker 프로세스를 모니터링하게 됩니다.
master, worker 외에도 위 이미지에서 보이는 cache loader, cache manager 프로세스가 존재합니다.
cache loader는 디스크 상의 파일을 체크하고 worker가 사용할 수 있도록 nginx의 공유메모리에 캐시를 업데이트합니다. 작업이 마무리되면 프로세스는 종료하게 됩니다.
cache manager는 캐시의 만료와 검증을 담당합니다. 일반적인 상황에서 메모리에 상주하며 캐시를 체크합니다.
(본격적인) Worker 프로세스 내부의 요청 처리 과정
Nginx 관점에서 커넥션의 시작은 socket에서 커넥션을 accept [8]를 하는 부분에 존재합니다. 디폴트 Nginx 설정에서의 socket - accept queue - worker의 흐름과 구조는 아래와 같습니다:
그러한 socket - accept queue의 연결은 위에서 잠깐 언급한 ngx_cycle에서 ngx_open_listening_sockets을 호출하면서 시작됩니다. 기본적으로 SO_REUSEPORT [7]와 같은 아규먼트를 반영하여 소켓을 바인딩하는 것을 알 수 있습니다.
OS단에서 핸들링되는 socket - accept queue 이후 worker에서 이벤트 모듈, 페이즈 핸들러, 아웃풋 필터, variable 핸들러, 프로토콜, 업스트림, 로드밸런서와 같은 기능적 모듈을 통해 처리됩니다. 이중 accept queue -> worker의 진입점이 되는 이벤트 모듈은 kqueue나 epoll과 같은 특정 OS 의존 이벤트 알림 메커니즘을 제공합니다.
이 부분에서 일단 멈춰서 다음과 같은 2가지를 확인하고, 각 모듈을 좀 더 상세히 살펴보는게 좋겠습니다.
1. 먼저, worker는 어떻게 위와 같은 모듈들의 흐름을 관리하는지에 대한 부분입니다. worker 코드에서 핵심이며 복잡한 run-loop (event loop)를 살펴보며 이 부분을 알아볼 수 있습니다.
2. 각 모듈의 구조는 1.의 run-loop을 통해 실행되기위한 일정한 컨벤션을 가지고 있습니다 [2]. 이 부분을 알아보면, 우리는 모듈의 일반적인 패턴을 이해하여 각 모듈을 이해한 패턴에 기반해 살펴볼 수 있습니다.
worker process cycle
worker process의 시작은 master로부터 호출되는 ngx_worker_process_cycle에서 시작됩니다. 해당 함수의 내부에서는 ngx_worker_process_init과 같은 함수로 설정 작업을 마친 후, 무한 루프 안에서 ngx_process_events_and_timers를 호출하는 것을 볼 수 있습니다. 이후 내부에서 function pointer로 연결되는 ngx_process_events를 호출하며, epoll 혹은 kqueue 등과 같이 대응되는 이벤트 모듈을 호출하게 됩니다. 이어서 event process 함수 내부에서는 ngx_event_t의 handler를 호출하면서, 설정되었던 handler로 흐름을 넘깁니다.
ngx_process_events_and_timers 부분에서는, 특히 Nginx docs의 event loop 하위의 I/O event, Timer event, Posted event의 내용을 보며 코드를 이해하면 어떤 우선순위로 처리되는지 확인할 수 있습니다.
module definition
[2]
Nginx에 관한 질문들
- reverse proxy 형태로 Nginx를 사용하면, client - nginx, nginx - backend 간의 커넥션이 따로 관리되는 것인가? 커넥션은 따로 생성됨 [12]
- proxy 형태로 nginx를 사용하면, data stream이 중간에 버퍼링 되는 것인가? 옵션으로 지정 가능한 것으로 보임 [13, 14]
Reference
[1] https://aosabook.org/en/v2/nginx.html
[2] https://www.evanmiller.org/nginx-modules-guide.html
[3] https://w3techs.com/technologies/details/ws-nginx
[4] http://www.kegel.com/c10k.html
[5] https://nginx.org/en/docs/configure.html
[6] https://blog.cloudflare.com/the-sad-state-of-linux-socket-balancing
[7] https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/
[8] https://man7.org/linux/man-pages/man2/accept.2.html
[9] https://nginx.org/en/docs/dev/development_guide.html
[10] https://github.com/dedok/nginx-tutorials
[11] https://nginxguts.com/2011/01/phases.html
[12] https://stackoverflow.com/questions/33624047/hows-tcp-connection-established-with-reverse-proxy
[14] https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_request_buffering