엔지니어라면 대표적인 웹 서버(Web Server, WS)인 Nginx와 Apache를 사용해본 경험이 있을 것이다. Nginx는 2021년을 기준으로 아파치(Apache) 서버를 제치고, 전 세계 HTTP/HTTPS 서버 점유율 1위를 기록했다.
하지만, 나는 Nginx를 사용하면서도 왜 Nginx를 선택해야 하는지 지 잘 몰랐다. 오늘은 Apache와 Nginx가 등장한 배경을 살펴보며, 왜 Nginx를 사용해야 하는지 생각해보자.
Apache 서버의 탄생
아파치(Apache)는 1995년 처음 등장했다. 당시에는 유닉스 기반의 최초 웹서버인 NCSA HTTPd가 있었으나, 여러 버그로 인해 많은 불편을 겪었다. 이를 해결하기 위해 많은 사람들이 수정하고 구조를 변경하며 만들어진 것이 바로 아파치(Apache) 서버이다.
Apache 서버의 구조
아파치 서버는 요청이 들어오면 커넥션(Connection)을 생성하기 위해 프로세스를 생성한다. 그래서 새로운 클라이언트의 요청이 들어올 때마다 새로운 프로세스를 만드는 방식이다. 이는 유닉스 계열의 OS가 네트워크 커넥션을 형성하는 모델을 그대로 적용한 것이다.
그러나 프로세스를 만드는 작업이 오래 걸리다 때문에, 요청이 들어오기 전에 프로세스를 미리 만들어놓는 PreFork
방식을 사용한다. 새로운 클라이언트의 요청이 들어올 때마다 미리 만들어둔 프로세스를 가져다 사용하는 방식을 사용하고, 만약 만들어놓은 모든 프로세스가 할당되었다면 추가로 프로세스를 생성한다.
1999년 : Apache의 C10K 처리 한계
그러나 Apache는 1999년에 C10K라는 문제를 직면하게 된다. 이 시기는 인터넷 트래픽이 증가하던 시기로, 컴퓨터 보급이 늘어나면서 요청이 많아졌다.
C10K란 “Connection 10000 Problem”의 약자로, 동시에 연결된 커넥션이 10,000개에 도달했을 때 발생하는 문제를 의미한다. 서버에 동시에 연결된 커넥션이 많아졌을 때, 더 이상 커넥션을 만들지 못하는 문제가 발생했다.
한 클라이언트는 하나의 커넥션을 통해 여러 개의 요청을 보낼 수도 있으며, 커넥션울 새로 생성하는 데는 시간이 걸린다. 이처럼 동시 연결된 커넥션의 개수는 매우 많았고, 서버는 커넥션 수가 1만 단위가 넘어가는 순간 더 이상 커넥션을 생성하지 못하는 상황에 놓이게 된 것이다.
Apache 서버는 C10K 문제를 해결하지 못했다. 이는 프로세스가 너무 많이 생성되어, 메모리 부족현상으로 이어졌기 때문이다. 앞서 살펴본 Apache 서버 구조를 다시 생각해보면, 커넥션이 생성될 때마다 한 프로세스가 할당되기 때문에, 동시에 처리하고 있는 커넥션이 많아지면 그만큼 생성되는 프로세스가 많아진다. 결국 프로세스가 많아지면 메모리 부족 현상으로 이어지게 된 것이다.
Apache 서버의 한계
- 메모리 부족 : 프로세스가 너무 많이 생성된다. Connection이 1만개이면, 프로세스도 1만개 생성된다.
- 무겁다 : 감당해야할 프로세스가 많아질 수 있고, 다양한 모듈을 설치하다보면 서버가 정말 무거워진다.
- CPU 부하 : CPU 코어 하나당 동시적으로 처리해야 할 프로세스의 양이 많아서 부하된다.
💡 요청이 많아지면 많아질 수록 많은 프로세스 & 스레드를 생성하여 메모리 & CPU 등 컴퓨팅 소모가 많아진다.
Nginx의 등장
2004년, Apache 서버의 구조를 보완하기 위한 소프트웨어가 등장하는데, 그것이 바로 Nginx이다. Nginx는 Apache 서버의 한계를 극복하기 위해 설계되었다. Apache 서버가 감당해야 했던 수많은 동시 커넥션을 Nginx가 대신 유지함으로서, Nginx는 동시 커넥션을 보다 효율적으로 처리하여 Apache의 부담을 크게 줄일 수 있었다.
Nginx는 웹서버 답게, 정적파일에 대한 요청을 본인이 모두 처리하고, 동적인 파일을 요청받았을 때만 뒤에 있는 아파치 서버와 커넥션을 형성한다. 아파치 서버는 리소스를 커넥션 유지에 사용하지 않고, Nginx가 처리하지 못하는 동적 파일이나, 개발자가 설계해둔 로직 처리에만 집중할 수 있다. (당시에는 웹서버와 WAS의 경계가 명확하지 않았고, Apache가 WAS로 많이 사용되던 시기였다.)
Nginx 내부 동작 구조
Nginx는 어떻게 많은 동시 커넥션을 유지할 수 있을까? 이러한 비결은 바로 적게 생성되는 프로세스 수에 있다.
Nginx의 핵심 구조는 Master Process와 Worker Process로 나뉜다.
각 프로세스의 역할을 다음과 같다.
- Master Process :
nginx.conf
설정 파일에 작성한 내용을 읽고, 설정에 알맞게 Worker Process를 생성하는 프로세스 - Worker Process : 실제로 요청을 처리하는 프로세스
정리하면, Master Process로 부터 Worker Process가 생성되고, 이렇게 생성된 Worker Processer가 작업을 수행한다. 워커 프로세스는 지정된 Listen Socket을 통해 새로운 클라이언트 요청을 받아 커넥션을 생성하고 요청을 처리한다.
그리고 연결된 Connection은 HTTP 프로토콜에 지정된 keep-alive 시간만큼 끊기지 않고 유지된다. 그런데 여기서 Connection이 생성되었다고 해서, Worker 프로세스가 해당 커넥션 하나만 담당하지는 않는다.
Worker Process는 생성된 커넥션에 아무런 요청이 없는 상태라면, 새로운 커넥션이 들어왔을 때 또 연결을 할 수 있다. 또는 이미 연결된 다른 커넥션이 요청을 시도한 경우, 해당 요청 요청을 처리한다. 이렇게 connection의 생성 및 제거, 그리고 요청을 처리하는 행위들을 모두 Event라고 부른다.
Asynchronous(비동기) 처리
이러한 Event들은 OS 커널이 큐(Queue) 형태로 Worker 프로세스에게 전달된다. 이벤트들은 큐에 담긴 상태에서 Worker Process가 처리하기 전까지 비동기 방식(Asynchronous)으로 대기한다. 한 이벤트가 완전히 처리되고 난 후에, 큐의 다음 이벤트를 꺼내서 순차적으로 처리하는 방식이다. 정리하면, 비동기 Event-Driven 구조로서, Event Handler에서 비동기 방식으로 먼저 처리되는 요청을 진행한다.
Apache 서버와의 비교
Apache 서버는 요청이 없을 때 방치되는 프로세스가 많다. 반면 Nginx는 자원을 훨씬 효율적으로 사용한다고 할 수 있다. 또한 Apache 서버는 한 프로세스가 커넥션 한 개만 수용할 수 있어, 낭비되는 프로세스가 많아질 가능성이 크다.
반면 Nginx에서는 Worker Process라는 단 하나의 프로세스만 생성하고, 여러 커넥션을 수용할 수 있으며, 이 프로세스 하나로 이벤트 큐를 처리할 수 있다.
Nginx 비동기 방식의 Blocking
그러나 길고 무거운 작업이 발생하면 어떻게 될까?? 이런 경우 이벤트 처리의 전체 주기는 해당 작업이 완료될 때까지 대기하게 된다. 즉, 요청이 Blocking될 수 있다는 것이다.
요청을 비동기 방식으로 차근차근 처리해나가면 처리 시간이 오래 걸리 수 있다. 우리가 계산대에서 물건을 계산할 때를 생각해보자. 앞 사람이 정말 많은 물건을 구매하면, 우리는 앞 사람의 계산이 끝날 때까지 한없이 기다려야 한다. 대기열에 있는 모든 사람은 첫 번째 사람의 주문을 기다려야 하는 것이다.
이처럼 Worker Process는 요청을 처리하는 동안 다른 작업을 수행할 수 없기 때문에, 이벤트를 처리할 수 없어 성능이 저하되는 문제가 발생한다. 한 번의 작업으로 모든 후속 작업이 상당한 시간 동안 지연될 수 있다는 것이다. 그래서 Nginx는 Thread Pool의 개념을 도입했다.
Nginx의 Thread Pool
💡Worker Process가 Blocking 연산을 Thread Pool에게 넘긴다.
Worker Process가 요청을 이벤트 큐에 넣으면, 각 요청들을 Thread pool에 있는 스레드들이 균등하게 작업을 수행한다. 그리고 수행한 결과는 다시 Worker Process에 전달된다. worker process가 잠재적으로 오랜 시간이 걸리는 작업을 처리해야 할 때, 작업을 직접 처리하는 대신에 풀의 큐에 작업을 넣어두고 여유 스레드 중 어느 것이든 가져와 처리할 수 있게 한다.
다만, Nginx는 기본적으로(Default)로 비동기 방식을 사용하고, Thread Pool 기능을 사용하려면 다음과 같이 nginx.conf
에 설정할 수 있다.
thread_pool my_pool threads=4 max_queue=2048;
여기서 my_pool
은 스레드 풀의 이름이며, threads=4
는 사용할 스레드 수를, max_queue=2048
는 요청 대기열의 최대 크기를 설정한다.
그런데 여기서 Nginx를 구성해본 사람이라면, nginx.conf
에서 다음과 같이 worker process의 수를 지정할 수 있다는 것을 알 것이다.
worker_processes 16;
그래서 나는 문득 그런 생각이 들었다. worker process를 여러개 두면, 굳이 쓰레드 풀을 활성화할 필요가 있을까??
여기서 알아야할 것은 worker process와 thread pool이 처리하는 요청이 다르다는 것이다.
Worker Process와 Thread Pool의 차이
- Worker Process
- 각 워커 프로세스는 독립적으로 요청을 처리하며, 비동기 이벤트 루프를 통해 여러 요청을 동시에 처리할 수 있다.
- 많은 워커 프로세스를 두면 높은 동시성을 제공할 수 있지만, 각 프로세스는 메모리와 CPU 리소스를 소모한다.
- Thread Pool
- 스레드 풀을 특정 작업(ex. 파일 I/O, 데이터베이스 쿼리 등)과 같은 Blocking 작업을 비동기적으로 처리하는 데 사용된다.
- 워커 프로세스가 블로킹 작업을 수행할 때 다른 요청을 처리할 수 있도록 도움을 준다.
워커 프로세스가 블로킹 작업을 수행하는 경우, 해당 프로세스는 다른 요청을 처리할 수 없다. 이때 스레드 풀을 사용하면 블로킹 작업을 스레드 풀에 위임하고, 워커 프로세스가 다른 요청을 계속 처리할 수 있다. 그리고 많은 수의 워커 프로세스를 두면 메모리 사용량이 증가할 수 있기 때문에, 스레드 풀 활용하여 메모리 사용을 최적화하고 CPU 코어 수에 비해 더 나은 리소스 관리를 할 수 있다.
즉, 상황에 따라 두 기능을 적절히 조합하여 사용하는 좋다. 예를 들어, 높은 동시성을 요구하는 환경에서는 워커 프로세스를 늘리고, 블로킹 작업이 많은 경우에는 스레드 풀을 활용하여 성능을 최적화할 수 있다. 일반적으로 CPU 코어 수에 맞추어 worker_processes
값을 설정한다.
Context Switching
앞서 Worker Process는 일반적으로 CPU 코어 개수만큼 생성한다고 했다. 그래서 다음과 같이 Nginx가 CPU 코어 수를 자동으로 감지하여 설정할 수 있도록 auto
옵션을 제공하기도 한다.
worker_processes auto;
이렇게 Worker 프로세스를 CPU 코어 개수만큼 생성하면, 코어가 담당하는 프로세스를 바꾸는 횟수를 대폭 줄일 수 있다. 다시 말해, CPU의 Context Switching 횟수를 줄이는 것이다. 이것이 바로 Nginx가 택한 Event 기반 구조이며, 아파치 서버와의 가장 큰 차이점이다.
최근에 Apache보다 Nginx를 선호하는 이유는, Nginx가 트래픽이 많은 웹 사이트에 더 적합하기 때문이다. Nginx는 대용량 트래픽을 처리하기 위해 가벼움과 높은 성능을 목표로 하는 경량 서버로서, 동시 커넥션 수당 메모리 사용에서 nginx가 우수하다. 또한 최근에는 리버스 프록시, 로드 밸런서, 메일 프록시 및 HTTP 캐싱 등 전체 범위에서 서버 작업을 처리하는 웹 서버로 발전하였다. 다양한 기능과 클라이언트의 확장으로 대용량 트래픽을 처리해야 하는 요즘 서비스는, 무겁고 대용량 트래픽을 처리하기 어려운 Apache와는 맞지 않기 때문에 Nginx가 점점 더 많이 사용되는 것이다.
다만 지금까지 살펴보았을 때, "그러면 무조건 Nginx를 사용해야하는거 아냐?" 라는 생각이 들 수 있다. 그러나 Nginx보다 Apach가 더 적절한 선택지가 되는 경우도 있다. Apache의 경우 Nginx 보다 다양한 모듈을 제공하고 있으며, 개발이 쉽다는 장점이 있다.
구분 | Apache | Nginx |
요청 당 프로세스가 처리하는 구조 | 비동기 이벤트 기반으로 처리 | |
CPU 및 자원 낭비가 심함 | CPU 및 자원 낭비가 적음 | |
모듈이 다양하다. | 모듈이 상대적으로 적다. | |
안정성, 확장성, 호환성이 좋다. | 확장성은 떨어지지만 성능이 우세하다. |
결국 Nginx와 Apache 중 어느 것을 선택할 지는 서비스의 요구사항과 환경에 따라 달라질 수 있을 것 같다. 대규모 트래픽을 수용해야하며 성능이 중요하다면 Nginx를, 확장성이 중요하고 Nginx에 없는 모듈이 필요하다면 Apache를 고려해볼 수 있다.
Reference