PK의 테코톡을 들으며 제 스타일로 재정리한 글입니다! 내용도 쪼꼼 보충했고요 최고의 개발자 피케이
Nginx란?
w3techs.com에서 제공한 자료에 의하면, 21년 4월에는 Nginx가 웹서버 점유율 1위를 거머쥐었네요!
마침내 1위를 거머쥔 nginx. cloudflare server의 약진도 눈에 띄네요!
'다중 커넥션 제어에 유리하다'는 말은 많은 유저의 요청을 효율적으로 처리할 수 있다는 이야기인데요, 이는 이벤트 드리븐이라는 Nginx의 특징에서 기인합니다.
그러면 nginx는 어떤 역사를 가지고 있으며, 지금의 nginx를 있게 한 이벤트 드리븐 방식이란 무엇일까요?
태초에 apache가 있었다.
웹서버는 다양한 유저가 접속하기에 다중 유저 요청을 이상없이 처리해야 합니다. 아파치는 unix계열의 OS가 네트워크 커넥션을 생성하는 방식과 유사하게, 유저 요청이 들어오면 하나의 프로세스를 할당해 처리하게 구현하였습니다. 프로세스를 생성하는 비용은 부하가 상당하기 때문에, 프로세스를 미리 생성해서 할당하는 PREFORK 방식을 활용하였습니다.
유저 하나가 접근하면 PREFORK 과정을 통해 생성해놓은 프로세스 하나를 할당해 유저의 HTTP 요청(ex. GET /index.html)을 처리합니다. 만약 PREFORK로 만들어놓은 프로세스가 이미 누군가의 요청을 처리하고 있다면 새로운 Process를 만들어 처리합니다.
유저 요청 하나에 대해 프로세스 하나를 할당하는 이 방식을 Process Driven이라고 부르기도 합니다.
이 방식은 다양한 모듈을 만들어 유저 요청을 동적으로(동적으로 유저 요청을 처리할 수 있는 서버를 WAS라고도 부릅니다.) 처리하기 좋았습니다. 요청을 받아 응답을 내보낸다 라는 로직 중간에 모듈로서 필요한 로직(ex. 현재 시간을 구해서 index.html에 동적으로 추가해주겠어!)을 구현해두면 아파치가 해당 로직을 포함한 프로세스를 만들어 유저 요청을 처리할테니까요.
출시된 지 1년도 되지 않아 아파치는 기존 웹서버들을 밀어내고 업계 1위의 자리에 올라섭니다.
C10K 문제
시간이 흘러 1999년, PC의 보급률이 높아지기 시작합니다. 그리고 인터넷이 세간에 널리 보급되기 시작했습니다. 승승장구 하던 Apache는 새로운 도전에 직면하게 됩니다. 이전과 다르게 많아진 트래픽, 즉 클라이언트로부터의 커넥션을 처리할 프로세스를 새롭게 생성해주지 못하는 문제가 생긴겁니다. 이 문제를 두고 커넥션 10,000개 문제, 줄여서 C10K 문제라고 불렀습니다.
❗️커넥션을 10,000개까지만 유지한다고 해서 유저의 요청을 10,000개까지만 받을 수 있다는 이야기는 아닙니다. 왜냐하면 하나의 클라이언트가 동일 커넥션에서 여러 요청을 보낼 수 있기 때문입니다. 또한 서버가 응답을 보낸다고 해도 커넥션이 바로 끊기지 않습니다. HTTP의 효율을 위해 고안된 keep-alive설정 때문입니다.
CPU 성능 문제일까요? 당시 하드웨어는 1만개의 요청 정도는 처리할 수 있을 정도로 발전해있었습니다. 문제는 apache의 내부 구조 때문이었어요. apache의 장점이었던 모듈을 쉽게 얹을 수 있었던 점으로 인해 서버 프로세스가 무거워 지고, 무거운 프로세스를 유저 connetion 수만큼 생성하니 메모리가 버티지 못한 것이었죠. 또 수많은 프로세스가 끊임없이 CPU를 컨텍스트 스위칭 하는 비용도 상당했습니다. 여러모로 다중 요청을 처리하기엔 부적합한 구조였죠.
아파치는 이런 구조를 극복하기 위해 멀티 프로세스가 아닌 멀티 스레드 방식을 지원하기도 하고, prefork와 관련된 설정을 제어하여 성능을 개선할 수 있도록 발전하고 있어요.
하지만 C10K문제를 근본적인 구조를 바꾸어 해결한 소프트웨어가 등장하니 그이름은 바로..
NGINX의 새로운 구조
NGINX! (엔진 엑스로 발음합니다)
엔진엑스는 아파치 앞단에 두어 아파치가 받을 커넥션을 줄여줄 목적으로 만들어졌습니다. 어떻게 커넥션을 줄일까요?
엔진엑스는 그 자체로 웹서버이기 때문에, HTML, CSS, JS, 이미지 등의 정적파일은 직접 반환할 수 있습니다. 그리고 동적으로 처리해줘야할 요청만 apache로 보냄으로써 connection을 줄일 수 있습니다. 해당 요청은 nginx에는 keep-alive설정으로 여전히 연결되어있지만 apache로 보낼 때에는 별도 요청으로 받아옴으로써 해당 커넥션을 유지시키지 않을 수 있습니다.
그렇다면 엔진엑스는 다중 커넥션이 연결되어 있는데도 아파치처럼 프로세스 문제는 없을지요? 어떻게 그 문제를 해결했을까요?
마스터 프로세스, 워커 프로세스
비결은 요청이 많이 들어와도 프로세스를 늘리지 않는다 입니다. 참 쉽죠?
엔진엑스는 설정파일을 읽고 워커 프로세스를 생성하는 역할을 하는 마스터 프로세스와, 실제로 유저 요청을 처리하는 워커 프로세스로 이루어져 있어요. 엔진엑스가 구동되면 마스터 프로세스는 정해진 숫자만큼의 워커 프로세스를 생성하고, 리슨 소켓(워커 프로세스와 통신할 수 있는 소켓 디스크립터)을 배정해요
앞서 아파치가 요청을 처리하는 방식을 보았기 때문에, 다음과 같은 의문이 생길거에요.
: 그러면 워커 프로세스의 숫자가 정해져 있는데, 어떻게 다중 커넥션의 요청을 동시에 처리할 수 있어요???
이벤트 드리븐 Event Driven
이벤트 드리븐에선 TCP(or UDP) connection의 연결, 유저의 Http Request 처리, Connection의 종료까지의 모든 절차를 이벤트 라는 개념으로 취급하고 처리해요.
그리고 워커 프로세스에게 working queue라는 이름의 처리해야할 작업이 순차적으로 담긴 큐를 처리하도록 합니다. 이렇게 구현하면 워커 프로세스가 놀고 있는 시간 없이 끊임없이 이벤트(커넥션 연결, 종료, http request 처리...)를 처리하게 됩니다. (일해라 노예야!)
커넥션에서 아무런 요청도 들어오지 않고 대기하고 있는 keep-alive 상황에서는 하는 일 없이 메모리만 축내고 있던 apache의 프로세스 와는 대조적이죠?
오래 걸리는 작업으로 인해 이벤트들이 blocking되면 어떡하지?? → Thread Pool
위 방식도 완벽하진 않습니다. 만약 working queue의 disk I/O처럼 시간이 오래 걸리는 작업 하나가 들어온다면 어떻게 될까요? 해당 작업이 처리되는 동안, working queue의 작업들은 전부 대기해야 할겁니다. 오래 걸리는 작업 뒤에 줄 서있던, CPU 0.1초만 주면 해결할 수 있는 작업들은 억울하겠죠?
그래서 엔진엑스는 오래 걸리는 작업(Disk 읽기, 파일 전송하기)을 처리하는 Thread Pool을 따로 만들어 놓았습니다.(1.7.11버전 이후)
Thread Pool은 생성비용이 비싼 스레드를 필요할 때 편하게 늘리기 위해 스레드를 미리 만들어놓고 필요한 작업에게 할당해주는 개념입니다.
각 워커 프로세스들은 Disk I/O 처럼 오래 걸리는 작업이 감지되면 해당 작업만 처리하는 역할을 하는 Thread Pool에게 처리를 위임하고 다른 이벤트를 처리합니다. 이 Thread Pool은 32개의 default 스레드 수와 65535 default 크기를 가진 working queue를 가지고 있고요, 여러 워커 프로세스로부터의 파일 읽기 or 전송 작업을 working queue에 넣어놓고 32개의 스레드 중 놀고 있는 thread를 할당해 일을 처리한답니다. 아파치가 다중 요청을 처리하는 방식과 흡사하죠?
엔진엑스의 컨텍스트 스위치 최적화
보통 엔진엑스는 코어 개수만큼의 워커 프로세스를 만듭니다. 그리고 워커 프로세스가 하나의 코어만을 이용하도록 할당합니다. cpu에선 프로세스를 변경하는 컨텍스트 스위칭을 거치지 않아도 되고, 그만큼 cpu 부하가 감소합니다.
Nginx의 단점
하지만 위의 구조도 약점이 존재합니다. 개발자가 실수로 프로세스를 종료하게 되면 해당 프로세스가 관리하고 있던 모든 커넥션이 끊기게 됩니다. 해당 문제로 인해 개발자가 직접 모듈을 만들기가 까다롭습니다. 하지만 이런 단점 때문에 안 쓰기엔..
Nginx의 성능 향상
Nginx의 장점이 너무나 명확했습니다. 같은 자원을 가지고 처리할 수 있는 커넥션 양이 아파치에 비해 압도적으로 많았으니까요.
또 이벤트 드리븐 구조를 통해 가질수 있는 장점은 또 하나 더 있었는데요!
동적인 설정변경
바로 엔진엑스 서버가 동작하고 있을 때에도 동적으로 설정을 변경할 수 있다는 점입니다.
보통 서버 프로그램들은 설정을 하고 서버를 부팅하면 새로운 설정을 적용하기 위해 재부팅해야하는 경우가 많습니다. 그러나 엔진엑스는 이벤트 드라이븐이라는 특징 덕분에 서버가 돌고 있는 도중에도 동적으로 설정을 변경할 수 있습니다. 이것이 바로 Nginx를 다뤄보신 분들에겐 익숙한 키워드, service reload 입니다.
원리는 다음과 같습니다. service reload를 하면 마스터 프로세스는 새로운 설정파일을 읽어 새롭게 워커 노드를 생성합니다.
그리고 기존 워커 프로세스로는 새롭게 이벤트를 할당하지 않습니다. 기존 워커 프로세스(초록색)가 모든 커넥션을 처리하고 나면, 새로운 워커 프로세스(노란색)로만 이벤트를 전달하고 기존의 워커 프로세스를 종료합니다.
Nginx 전성시대
아파치는 출시 이후에도 오랜 시간 각광받지 못 하다가 2008년을 기점으로 재조명되기 시작합니다. 스마트 폰이 나오고 모바일 시대가 개막했기 때문인데요, 이전과 비교할 수 없을 정도로 네트워크 트래픽이 치솟았고 동시 커넥션 제어에 탁월한 성능을 가지고 있는 nginx가 각광받게 되었죠.
그러면 Apache는?
그렇다면 다중처리 성능이 밀리는 Apache는 사용할 이유가 없을까요?
그렇진 않아요. 아파치는 버그가 많던 웹서버 시장을 해결하기 위해 나온 솔루션인 만큼, 탁월한 안정성과 다양한 모듈을 이식할 수 있다는 장점이 있어요.
OS 안정성
Window에서도 Unix 계열에서와 동일한 성능을 보장합니다. (Nginx는 Window를 완벽하게 지원하지 않습니다)
디렉토리별 추가 구성
또한 .htaccess 파일을 통해 디렉토리별로 추가 서버 구성을 하는 것을 허용합니다. 이렇게 구성하면 메인 아키텍처는 수정하지 못하는, 특정 페이지만 관리하는 관리자 권한을 만들 수 있습니다. 각 디렉토리 별로 환경 설정을 따로 구성할 수 도 있고요.
Nginx에선 성능을 위해 추가 구성을 허용하지 않습니다. .htaccess파일을 검색하는 비용과 사용자가 만든 요구사항을 해석하지 않기 때문에 조금 더 빠르겠네요.
동적 모듈 지원
Apache는 Apache에서 지원하는 모듈에 대해 오버라이딩을 허용합니다. 이를 통해 개발자가 서버 구성을 자유롭게 커스텀할 수 있다는 유연성을 갖추고 있습니다. Nginx는 아직 개발자가 자유롭게 커스텀하기엔 제약이 있다고 하네요!
꼭 하나만 선택하라는 법은 없습니다. 커넥션 제어와 정적 컨텐츠 처리에 강점이 있는 Nginx를 리버스 프록시 서버로 활용하고, 아파치를 백엔드 서버로 두는 구성은 두 서버의 장점을 함께 활용할 수 있는 방법이 되겠네요
Nginx 기타 기능
Nginx는 위에서 이야기 한 웹서버로서의 기능 말고도 탁월한 로드밸런서(Load Balancer) 이기도 해요. 개발자는 Nginx를 이용함으로써 L7(OSI 7계층 어플리케이션 레이어)에서의 로드밸런싱을 손쉽게 구현할 수 있답니다.
로드 밸런서 이야기는 다음시간에 다뤄보도록 할게요!
그 밖에도 캐싱, SSL termination 지원, HSTS, CORS처리, TCP/UDP 커넥션 부하 분산, HTTP/2 를 지원합니다.