1. 웹서버(WS)에 대하여
1-1. 웹서버(WS) 란?
NGINX, Apache 두 소프트웨어 모두 “웹서버 소프트웨어”이다.
그렇다면 WS(웹서버)란 무엇일까?
클라이언트의 HTTP 요청을 받아 정적인 컨텐츠를 제공하는 서버 혹은 프로그램이다.
이후에 다시 말하겠지만, WS는 정적인 컨텐츠를 서비스하는 데에 특화된 서버 소프트웨어이다.
- 동적인 콘텐츠를 처리 못한다는 것은 아니다.
- 참고로, Apache의 경우 자체적으로 동적인 컨텐츠 또한 처리가 가능하다.
- NGINX의 경우 불가능하다.
- 하지만, 동적 처리는 보통 WAS(Web Application Server)에서 주로 수행하므로, Web Server는 프록시 역할만 하면 충분하다.
여기서 주요한 점은 “정적인 컨텐츠”이다.
html, JS 같은 정적인 컨텐츠들 정도는 웹서버를 구축한다면 웹서버 선에서 요청을 정리할 수 있다.
과거 (2-tier Architecture)
우선 과거의 버그가 많았던 초기의 웹서버(WS) 프로그램이 수정되고 개발된게 Apache이다.
과거에 html, js, css 등과 같은 정적인 콘텐츠만을 주고 받는 구조에서는
클라이언트 → WS [Apache] (정적 및 동적 요청 모두 처리) → DB 만으로도 충분했다.
- 사실은 Apache의 경우, 정적/동적 모두 커버가 가능하기 때문에 기능적으로는 문제가 없었다.
- 하지만 다음과 같은 단점이 제기되었다.
- 동적 요청 처리가 필요할 경우, Apache가 너무 많은 부담을 가진다.
- 보안과 성능 측면에서 아쉬움이 있다.
- 그리고 이후에 다시 소개하겠다…
- 그래서 앞단에 정적 처리를 전문적으로 해주는 서버가 있었으면 좋겠다는 생각을 하게 된다.
현재 (3-tier Architecture)
정적/동적 컨텐츠에 대한 책임을 분리하고,
효율성과 확장성에 용이하기 위해 아키텍처가 발전했다.
클라이언트 → [WS (정적 요청 처리) → WAS (동적 요청 처리)] → DB
(초기 발전 과정에서는 Apache가 WAS 역할로 동적 요청을 처리하고, 앞단에 정적 요청 처리를 NGINX가 붙는 방식으로 사용되었다.)
WAS는 동적 처리에 리소스 집중할 수 있게 되었고, 트래픽 증가에 유연한 대응이 가능하게 되었다.
- 소프트웨어 측면 말고, 개발자의 측면에서 보아도 효율적이다.
- WS는 정적 파일, 보안, 로드밸런싱에만 집중하면 되고, WAS에서는 비즈니스 로직에만 집중하면 된다.
2. Apache와 NGINX
2-1. Apache 처리 방식 (커넥션 기반)
아파치는 Multi-Process Module 방식이다.
클라이언트가 요청 하나를 보내면, 하나의 process를 생성해서 연결한다.
요청당 프로세스/스레드 생성한다는 뜻이다.
이런 구조는 확장성 면에서 장점이 크다.
→ 이러한 측면에서 개발자들은 다양한 모듈을 만들어서 서버에 동적으로 빠르게 추가할 수 있는 장점이 있었다.
하지만 다음과 같은 문제가 제기되었다.
(🤔문제) 프로세스 생성이 가벼운 작업이 아니기에, 매번 생성하기에는 너무 속도가 느려진다.
(✨발전 1) 요청 이전에 몇 개의 프로세스를 미리 만들어주는 prefork() 방식을 사용했다.
(✨발전 2) 어찌됐든 매번 커넥션을 만드는 것은 비효율적이라고 판단했고, 이미 연결된 커넥션이 있다면 재활용하자는 생각을 한다.
- http 헤더 종류 중에 keep-alive가 있다.
- → Keep-Alive 옵션을 통해 일정 기간 동안 클라이언트와 Connection을 유지하는 방식
그러나 또 다른 문제가 제기되었다.
(🤔또 다른 문제) 클라이언트 수가 늘어나면서 결국 동시에 연결되어 있는 커넥션의 수도 매우 늘어나게 되었다.
- 이렇게 동시 연결된 커넥션 수가 10,000개의 동시 연결을 넘어서는 순간, 서버는 더이상 커넥션을 형성하지 못하게 되는 현상이 발생했다. (C10K 문제…)
- 그런데 이 C10K 문제에서 하드웨어적으로 문제되는 것은 없었다.
- 이 당시 웹 페이지 용량 자체가 적었기 때문에 컴퓨터의 성능은 충분했지만,
비효율적인 서버의 구조 때문에 메모리 부족 현상이 일어난 것이다. - 많은 커넥션에서 요청이 들어오기 시작하면, CPU 코어는 계속해서 프로세스를 바꿔가며 일을 해야 했다.
- 결론적으로, 수 많은 동시 커넥션을 감당하기엔 아파치 서버의 구조가 적합하지 않았다.
2-2. “NGINX”의 등장 (이벤트 기반)
사실 NGINX는 Apache 를 대체하려고 나온 소프트웨어가 아니다.
NGINX는 Apache 앞 단에 붙여서 보완하는 방식으로 사용되었다.
구조적으로 동시 커넥션을 많이 유지 못하는 아파치 서버의 부하를 nginx가 크게 줄일 수 있었다.
- 참고로,
[동시에 연결 가능한 커넥션 수(한 시점에 감당되는 동시 커넥션 수)] 와 초당 요청 처리 수는 다른 의미이다.
이때 NGINX 가 정적인 페이지 처리를 모두 하고,
Apache는 동적 파일 처리만 담당하는 (현대의 WAS 느낌) 구조로 바뀌게 되었다.
그렇다면 어떻게 NGINX는 많은 동시 커넥션을 견뎌 낼 수 있을까?
NGINX 처리 방식에 대해
Nginx는 Master-Worker 구조의 Process 방식을 채용했다.
Master Process는 설정파일에 맞게 실제 일하는 Worker Process를 만들어낸다.
그리고 Worker가 만들어질 때 각자 지정된 Listen 소켓을 배정받는다.
(수신기를 받는다.)
그리고 그 소켓에 새로운 클라이언트로부터 Request가 들어오면, 커넥션을 형성하고 요청을 처리한다.
- 마찬가지로 이 커넥션은 정해진 Keep-alive의 Timeout까지 연결을 유지하게 된다.
하지만, Apache와의 차이는
“그 커넥션이 형성되었다고 해서, Worker Process 1개가 그 커넥션 1개만을 담당하지는 않는다.”
- 형성된 커넥션에 아무런 요청이 없으면 새로운 커넥션을 형성하거나,
이미 만들어진 다른 커넥션으로부터 들어온 요청을 처리한다. (여러 커넥션을 쥐고 있는다.) - 이런 [커넥션 형성 / 커넥션 제거 / 새로운 요청 처리] 등을 모두 이벤트라고 부른다.
NGINX의 OS커널은 이러한 이벤트를 큐형식으로 줄세우고, Worker에게 전달한다.
(멀티플레싱, epoll 에 대해 이해하면 어떻게 커널이 이벤트를 제공하는지 알 수 있다.)
→ 는 커넥션 연결, O는 Request이며, 모두 “이벤트”이다.
그리고 이 이벤트들은 큐에 담긴 채 비동기 방식으로 대기하고 있다.
그리고 Worker Process는 하나의 스레드로 이벤트를 꺼내서 처리해 나간다.
이러면 Worker는 쉬지 않고 일을 계속 할 수 있다.
요청이 없다면 방치되던 Apache의 구조보다 서버 자원을 훨씬 효율적으로 사용하는 셈이다.
(🤔문제) 그런데 순서대로 꺼내서 쓰는 Queue에서 앞단에 들어오연 요청하나가 매우 시간이 오래걸리는 Disk I/O 작업이라면 어떨까..?
매우 긴 IO 작업 중 큐에 대기하는 모든 이벤트들은 블로킹 될 것이다.
(✨해결책) 오랜 기간 작업을 요하는 요청들을 위한 쓰레드 풀을 미리 만들어놓고,
Worker Process가 지금 처리하는 요청이 오래 걸린다고 판단되면,
해당 스레드 풀에 그 작업을 위임하고, 큐의 다른 이벤트를 처리하러 간다.
이러한 Worker Process 는 보통 Cpu의 코어 개수만큼 생성하고,
이러한 구조는 코어가 부담하는 Context Switch의 횟수를 대폭 줄일 수 있다.
Apache와 비교하자면,
- Apache
- CPU가 여러 프로세스 간 스위칭
- 요청당 프로세스/스레드 필요
- Nginx
- 하나의 Worker Process가 이벤트 큐로 관리
- 단일 스레드로 여러 이벤트 처리
- CPU 코어당 하나의 Worker 고정
- 이러한 Event-Driven Model이 Apache 구조와의 가장 큰 차이이다.
2-3. Apache와 NGINX
항상 어떤 기술을 사용할 때에는 “최신 기술이여서”, “많이들 써서”가 이유가 될 수는 없다.
어떤 기술, 어떤 아키텍처건 Trade-Off는 존재한다.
과연 NGINX는 장점만 있을까?
개발자가 기능(모듈) 추가를 시도했다가 돌아가고 있는 Worker Process를 종료해야 하는 상황이 생길 수 있다.
그러면 해당 Worker Process는 관리하고 있던 커넥션과 관려된 요청을 더이상 처리할 수 없다는 문제가 발생한다.
- 그래서 NGINX는 개발자가 직접 모듈을 만들기에 좀 까다롭다.
하지만, 단점보다 장점이 명확했다.
수많은 동시 커넥션을 빠르게 처리하는 데에 있어 프로세스를 적게 만들다보니
(One Worker Process per CPU core)
⇒ 동시 커넥션 양 최소 10배 증가
⇒ 동일한 커넥션 수일 때 Apache에 비해 속도 2배 향상
- NGINX의 명확한 장점
- One Worker Process per CPU core
- Event-driven, asynchronous processing
- Event Queue management
- Thread Pool for long-running tasks
그리고 적은 프로세스는 관리가 쉽고, 새 Worker 생성 시 리소스 부담이 적다.
이러한 구조는 “NGINX가 설정을 동적으로 바꾸는 것”을 가능하게 했다.
- 개발자가 설정 파일을 Runtime에 변경하고, NGINX에 적용하면,
이후부터는 Mater Process는 그 설정에 맞는 Worker Process를 찍어내게 된다. - 그리고 기존 Worker Process에는 더이상 커넥션을 형성하지 못하게 막고,
Queue에 있는 이벤트들을 마저 처리한 뒤 기존 Worker들은 종료된다.
그런데 이러한 동적 설정 변경은 언제 사용할까?
- 대표적인 예로, NGINX가 여러 동시 커넥션을 관리하는 중에 뒷단에 새로운 서버를 추가하려는 상황이다.
NGINX는 이제부터 로드밸런싱 역할도 담당해야 한다.
역할의 추가/변경이 의미하는 것은 무엇일까?
- = Master Process가 찍어내야 하는 Worker Process의 역할의 추가/변경
= 기존 Connection 및 기존 Request들의 손실 없이 역할의 변경이 필요하다. - NGINX는 동시 커넥션을 유지한 채, 기존의 요청을 마저 처리하며, 뒷단의 서버를 추가할 수 있다.
- 이벤트 기반 구조이기 때문에 이러한 설정 변경을 초당 수십번 해도 무리없이 커넥션을 관리하고 요청을 서버에 전달할 수 있다.
출처 : NetCraft Web Server Survey
그런데 위 자료를 보면 의문이 든다.
NGINX가 탄생한 지 2004년부터 2007년 까지도 압도적으로 Apache가 1위를 차지하고 있다.
이때까지만 해도 NIGNX는 소수의 문제 상황에 대한 해결책을 제시한 것 뿐이지,
대다수의 서비스를 가동하는 데에는 Apache로도 큰 문제가 없었다.
스마트폰의 탄생
출처 : NetCraft Web Server Survey
2008년부터 급격하게 상황이 역전된다. 바로 스마트폰의 등장 때문이다.
- 단순히 ‘스마트폰의 등장 = 인터넷 사용자의 증가’의 이유가 아니다.
- “동시 커넥션을 훨씬 더 많이 생성한 계기”로써 문제가 발생한다.
- PC의 경우 클라이언트가 일정 시간 사용하고 전원을 끄는 방식으로 사용되었지만,
스마트폰은 실시간으로 정보를 받고 싶어하는 사용자의 니즈에 맞게 알림 등
동시 커넥션을 계속해서 유지하는 방식으로써 유용하게 사용되었다.
이러다보니 “동시 커넥션 문제”가 현실로 다가온 것이다.
- 대규모 사이트들이 좋아할 만한 솔루션 이었고, NGINX는 급성장하게 된다.
(현재 우리가 쿠버네티스고, 뭐고 배우는 이유도 이러한 문제의 솔루션으로써 대두된다는 것을 알고 넘어가자)
그렇다면 Apache는 가만히 있었을까?
Apache도 MPM이라는 모듈을 추가해서 성능을 개선하기 시작한다.
- Multi-Processing Module
- Apache 서버를 어떤 방식으로 운영할 지 선택할 수 있는 모듈이다.
- 안정성이나 소규모 프로젝트에서는 기존의 prefork 방식을
- 성능 향상이나 대규모 프로젝트에서는 worker라는 스레드를 만들어 worker가 요청을 처리하도록 했다.
그렇다면 왜 아직도 동시 커넥션 지표에서 NGINX가 Apache를 압도적으로 앞설까?
출처: dreamhost.com
왼쪽 동시 커넥션 수당 메모리 사용률 지표를 보면,
- NGINX는 동시 커넥션이 많아져도 메모리 사용률이 낮고 일정한 반면,
Apache는 굉장히 많은 메모리 사용률을 요구한다.
오른쪽 동시 커넥션 수가 많아졌을 때 처리하는 초당 요청 수 지표를 보아도,
NGINX가 Apache에 비해 압도적으로 많다.
- NGINX가 커넥션 관리를 얼마나 잘하는 지 볼 수 있다.
사실 지금까지 동시 커넥션이라는 포인트에만 집중해서 NGINX의 장점과 현재 많이 쓰이는 이유를 알아보았다.
그렇다면 Apache의 장점은 무엇이 있을까?
2-4. Apache가 아직 쓰이는 이유와 NGINX의 단점
근본자체가 “NCSA HTTPd”라는 소프트웨어의 버그를 수정해오며 커온 Apache이기에, 호환성과 확장성의 측면에서는 장점이 있다.
아직도 오픈소스로써 활발한 수정과 개선이 이루어지고 있다. [풍부한 모듈 생태계]
또한, Apache는 여러 운영 체제(리눅스, 윈도우, 유닉스 등)에서 폭넓은 호환성과 안정성을 자랑한다.
NGINX는 그렇지 않아서, Windows에서 성능이 매우 낮게 나온다.
- NGINX는 원래 리눅스 환경에서 설계되었기 때문에, 리눅스 OS에서 최고의 성능이 나오며,
- 윈도우에서도 동작은 하지만,
윈도우의 비동기 I/O 시스템에 최적화되지 않았기 때문에 성능이 상대적으로 낮다.
현재 서비스에서 큰 성능 문제 없이 잘 돌아가고 있고, 모듈을 계속해서 추가/제거할 일이 많다면 굳이 NGINX로 옮길 이유는 없는 것이다.
- 오해하면 안되는 것이 NGINX는 설정 변경과 이에 따른 Worker Process의 로직 변경이 무중단으로 이루어진다는 것이며,
NGINX는 모듈 자체를 동적으로 로드할 수 없으므로, 새 모듈을 추가하려면 재컴파일이 필요하다. - Apache는 설정 변경 없이 실행 중 동적 모듈 로드가 가능하므로, 특정 기능을 빠르게 추가하거나 제거할 때 더 유연하다.
둘은 서로 대립관계가 아니며, 목적이 다르게 탄생한 것이다.
2-5. 그렇다면 현재 NGINX를 어떻게 이용해야 하는가?
앞서 우리는 NGINX의 WS로써, 로드밸런싱으로서의 역할을 보았다.
비동기 이벤트 기반 구조로 동시 커넥션 처리가 효율적이며, Reverse Proxy로써 서버 부하를 줄일 수 있다.
이 자체로 웹 서버 가속(성능 개선) 역할을 한다.
추가적으로 NGINX를 사용하며 고려해야할 관점이다.
1) SSL 터미네이션 역할을 한다.
NGINX가 앞단에서 SSL을 처리하여, 클라이언트와는 HTTPS로 통신하고, 뒷단 서버와는 HTTP로 통신하는 방식이다.
- 어차피, NGINX와 SERVER가 같은 네트워크 안에 있는 경우가 많기 떄문에,
이렇게 통신을 해도 보안 위험이 비교적 적다.
이 방식은 뒷단 서버가 복호화 과정에서 발생하는 부하를 줄이는 장점이 있다.
2) 캐싱 역할을 한다.
NGINX는 <span class=”post-highlight”HTTP 콘텐츠 캐싱</span>을 지원하며,
이를 통해 서버 부하를 줄이고 클라이언트 요청에 빠르게 응답할 수 있습니다.
- 한 번 서버로부터 받은 응답을 스스로 보관하고 클라이언트에게 전달하므로 효율적이다.
이러한 경우 1번과 달리 NGINX를 클라이언트와 가까운 곳(예: CDN이나 엣지 서버)에 배치하여
캐싱 효과를 극대화할 수 있다.
3)NGINX와 컨테이너화
현재 클라우드 네이티브와 컨테이너화가 대세인 환경에서, NGINX는 더욱 중요한 역할을 수행하고 있다.
특히 Kubernetes의 Ingress Controller로서, 또는 마이크로서비스 아키텍처에서의 API Gateway로서 그 가치가 두드러진다.
Kubernetes에서 Ingress Controller로 사용될 때, API Gateway로서의 설정 등등
- 이러한 설정들을 통해 NGINX는 현대적인 마이크로서비스 환경에서 트래픽 라우팅, 로드밸런싱, SSL 종료, API 게이트웨이 등 다양한 역할을 효율적으로 수행할 수 있다.
- 특히 컨테이너 환경에서는 가볍고 빠른 NGINX의 특성이 더욱 큰 장점으로 작용한다.
이 외에도 HSTS, CORS 처리, TCP/UDP 커넥션 분산 부하, HTTP/2 지원 (최근에는 /3도 지원) 등 서버를 보완하는 역할을 할 수 있다.
어떠한 기능이 있는지를 알아보고 상황에 따라 알맞는 기능을 적용하는 자세가 필요하다.