서버 개발자자면 웹서버(WS) 의 세계 1,2위를 차지하고 있는 Nginx 와 Apache 를 꼭 한번씩 사용해보셨을 겁니다. 그런데 많은 분들은 왜 Nginx를 써야하는지를 잘 모르실겁니다. 저희는 그저 대중적으로 Nginx 를 많이 쓰니까 그냥 써야할까요?
심지어 왜 Apache 와 Nginx 의 등장배경과 내부동작 구조를 잘 모르신다면, 오해하고, 잘못된 사실을 제대로 알고있다고 착각하고 웹서버를 사용할일이 허다할겁니다. 예를들어 Apache 와 Nginx 가 서로 우호적인 관계이면서 동시에 상호작용을 할 수 있는 관계에 있음에도 불구하고, 적대관계에 있다고 오해하고 계신분들도 정말 많습니다.
이번 포스팅에서는 1995년부터 시작하는 아파치 서버의 등장과 한계점, 그리고 Nginx 의 등장과 내부 메커니즘을 깊게 이해하는데 초점을 두었습니다.
많은 분들이 제 포스팅 내용을 보시고, Nginx 에 대해 다시 깊게 학습하시는 계기가 되었으면 합니다 😉
여러분들은 이번 제 포스팅을 통해 다음과 같은 내용을 얻어갈 수 있습니다.
1995년부터 시작한 아파치 서버의 등장과, 2023년 현재에 이르기까지 왜 아파치와 Nginx 서버가 대중적으로 널리 사용하는 것인지 "근본적인 이유를" 깊게 이해할 수 있습니다!
Nginx 의 중점을 두었지만, Apache 서버의 등장배경과 내부 메커니즘 또한 동시에 소개합니다. 이 둘을 비교하면서 왜 Nginx 가 동시성 처리에 유리한 것인지를 알려드립니다.
로드밸런싱, C10K, 쓰레드풀, Nginx의 Process 구조, SSL 터미네이션, 부하와 분산처리 등 다양한 문제를 해결할 수 있는 Nginx 의 내부 메커니즘을 이해할 수 있습니다!
기초적인 CS 지식을 잘 모르시는 분들이 보기엔 다소 어려운 포스팅일 수 있습니다. 일단 포스팅을 읽어보신 후, 키워드들을 추가적으로 학습하시면 CS 학습에도 도움이 되실겁니다.
최초의 웹서서 NSCA 와 HTTPd 에서 발생하는 버그에 대한 불편함으로, 대안으로 등장한 것이 Apache 서버이다.
아파치 서버가 처음열린 1995년으로 가봅시다. 그 당시에는 유닉스 기반으로 만들어진 최초의 웹서버(WS)인 NSCA 와 HTTPd 가 있었습니다. 그런데 이 프로그램은 버그가 굉장히 많아서 개발자들이 사용할 때 불편함을 걲였죠.
그래서 뛰어난 몇몇 개발자들이 그 버그를 수정하기 시작했습니다. 그러면서 그 구조를 변경하고, 기능을 추가해서 만든것이 바로 아파치 서버입니다. 그러면 이아파치 서버의 구조가 어떤지 간단하게 알아봅시다.
아파치 서버는 요청이 들어오면 커넥션(Connection) 을 생성하기 위해 프로세스를 생성합니다. 그래서 새로운 클라이언트의 요청이 들어올 때마다 새로운 프로세스를 만듭니다. 이는 유닉스 계열의 OS 가 네트워크 커넥션을 형성하는 모델을 그대로 적용한 것입니다.
PreFork : 새로운 클라이언트로부터 요청이 들어오면 미리 만들어놓은 프로세르를 가져다 사용하는 방식
그런데 프로세스를 만드는 것은 작업이 정말 오래걸리다보니, 요청이 들어오기전에 프로세스를 미리 만들어주는 PreFork 방식을 사용했습니다.
그래서 새로운 클라이언트로부터 요청이 들어올떄마다 미리 만들어준 프로세스를 가져다 사용하는 방식을 사용했습니다. 만일 만들어놓은 모든 프로세스가 할당된다면, 추가로 프로새스를 만들었죠.
이러한 구조를 개발하기 쉬워서, 개발자들은 다양한 모듈을 만들고 아파치 서버에 빠르게 기능을 추가할수 있습니다. 이렇게 아파치 서버는 동적 컨텐츠를 쉽게 처리할 수 있게됩니다.
또한 확장성이 뛰어납니다. 요청을 받고 응답을 처리하는 과정을 하나의 서버에서 해결하기 좋습니다.
이렇게 문제없이 사용하던중, 아파치 서버는 C10K 라는 문제를 맞딱뜨리게 됩니다. 1999년 이 시기에는 인터넷 트레픽이 계속 증가하는 상황이였습니다. 점점 컴퓨터 보급량이 많아지고 요청이 많아짐에 따라, 서버가 많은양의 커넥션을 수용하지 못하는 것입니다.
이전에는 서버가 처리할 요청양이 그 당시 기술로 감당할 수 있었는데, 이때부터 아차피 서버의 한계가 두각됩니다.
C10K 란 Connection 10000 Problem 의 약자로, 즉 커넥션 1만개에 대해 발생한 문제를 뜻합니다. 서버에 동시에 연결된 커넥션이 많아졌을때 더 이상 커넥션을 만들지 못하는 문제가 발생한 것이죠.
게다가 한 클라이언트는 하나의 커넥션을 통해 여러개의 요청을 보낼수도 있습니다. 또 커넥션 하나를 생성하는데는 오랜 시간이 걸립니다. 여러 절차가 따르기 때문이죠.
이러한 동시간대에 연결된 커넥션의 개수는 정말 많았고, 서버는 커넥션 수가 1만 단위가 넘어가는 순간 더 이상 커넥션을 생성하지 못하는 상황에 놓이게 된것입니다.
프로세스가 너무 많이 생성되어, 메모리 부족현상으로 이어졌기 때문이다!
C10K의 문재는 하드웨어는 문제가 될 것이 없었습니다. 하드웨어는 그 당시의 컨텐츠 용량을 담는데 성능이 충분히 좋았죠.
C10K 를 해결하지 못했던 원인은 아파치 서버의 구조에 있었습니다. 아까 설명드린 서버 구조를 다시 리마인딩 해봅시다. 아파치 서버의 구조는 커넥션이 생성될 때마다 한 프로세스가 할당되기 때문에, 동시에 처리하고 있는 커넥션이 많아지면 그만큼 생성되는 프로세스가 많아집니다. 결국 프로세스가 많아지면 메모리 부족현상으로 이어지게 된 것이였죠.
Apache 서버의 한계점을 정리해보자면 다음과 같습니다.
- 메모리 부족 : 프로세스가 너무 많이 생성된다. 커넥션이 1만개이면 프로세스도 1만개 생성된다.
시간이 흐르고 2004년이 되었을떄, 아파치 서버의 구조를 보완하기 위한 소프트웨어가 나옵니다. 그게 바로 Nginx 입니다.
초창기의 Nginx 는 아파치와 함께 사용하기 위해 만들어졌습니다. 웹서버이긴 하지만, 아파치 서버를 완전히 대체하기 위한 목적은 아니였습니다. 아파치 서버가 지닌 구조적 한계를 Nginx 를 사용함으로써 극복하려고 했습니다.
그렇다면 아파치 서버의 한계를 어떻게 Nginx 로 극복할 수 있을까요?
간단히 말하면, 아파치 서버에다 Nginx 를 올려두면 됩니다.
이렇게하면 기존의 아파치 서버가 감당해야했던 수많은 동시 커넥션을 Nginx 가 대신해서 유지해줍니다. 동시 커넥션을 유지못하는 어파피 서버의 부하 문제점을 Nginx 로 크게 줄일 수 있게 된것이죠.
Nginx 는 웹서버답게, 정적파일에 대한 요청을 본인이 모두 처리하고, 동적인 파일을 요청받았을 때만 뒤에 있는 아파치 서버와 커넥션을 형성하는 것입니다.
아파치 서버는 리소스를 커넥션을 유지하는데 쓰지않고, Nginx 가 처리하지 못하는 동적 파일을 처리하거나, 개발자가 설계해둔 로직 처리에만 신경쓰면 되는것입니다.
그렇다면 Nginx 는 어떤 구조이길래 많은 동시 커넥션을 유지할 수 있을까요? 비결은 바로 적게 생성되는 프로세스 수에 있습니다.
Nginx 의 핵심 구조를 뜯어보면, Master Process 와 Worker Process 라는 것이 있습니다. 각 프로세스의 역할을 요약해보면 다음과 같습니다.
- Master Process : 저희가 Nginx.conf 라는 설정파일에 작성한 내용을 읽고, 설정에 알맞게 Worker Process 를 생성하는 프로새스
쉽게말해 Master Process 로 부터 Worker Process 가 생성되고, 이 프로세스가 작업을 수행하는 것이죠.
이 Worker 프로세스가 만들어질 때 각자 지정된 Listen Socket 을 지정받습니다. 그리고 그 소켓에 새로운 클라이언트로부터 받은 요청이 들어오면 커넥션을 생성하고, 그 요청을 처리합니다.
그러고 연결된 커넥션은 HTTP 프로토콜에 지정된 keep-alive 시간만큼 끊기지않고 유지됩니다. 그런데 커넥션이 생성되었다고 해서 Worker 프로세스가 해당 커넥션 하나만 담당하지는 않습니다.
Worker 프로세스는 생성된 커넥션에 현재 아무런 요청이 없는 상태라면 새로운 커넥션이 들어오면 또 연결을 가능하게 합니다. 또는 이미 연결된 다른 커넥션이 요청을 시도한 경우, 해당 요청에 대해 처리를합니다.
이렇게 커넥션의 생성 및 제거, 그리고 요청을 처리하는 행위들을 모두 Event 라고 부릅니다.
이런 Event 들은 OS 커널이 큐(Queue) 형태로 Worker 프로세스에게 전달해주는 겁니다. 이 이벤트들은 큐에 담긴 상태에서 Worker 프로세스가 처리하기 전까지는 비동기 방식(Asynchronous) 이 되게합니다.
한 이벤트가 완전히 처리되고 난후에, 큐의 그 다음 이벤트를 꺼내서 그 다음것을 순차적으로 처리하는 방식이라는 것입니다.
쉽게말해, 한 이벤트가 처리되기 전까지 다른 이벤트들은 동시에 처리되는 것이 아니라, 계속 대기하고 있어야함!
이때 Worker 프로세스는 하나의 쓰레드로 이벤트를 순차적으로 꺼내서 처리해나가는 방식입니다.
이러면 worker 프로세스가 쉬지않고 계속해서 일을 한다는 장점이있습니다.
아파치 서버와 구조를 비교해봅시다. 아파치 서버는 요청이 없다면 방치되는 프로세스들이 가득했죠? 반면 서버 자원을 훨씬 효율적으로 쓰는 셈입니다.
아파치 서버는 한 프로세스가 커넥션을 딱 하나만 수용가능해서 낭비되는 프로세스가 많이생길 가능성이 정말 크죠. 반면 Nginx 에서는 Worker Process 라는 딱 하나의 프로세스만 생성하고 여러 커넥션을 수용 가능하며, 이 프로세스 하나로 이벤트 큐를 보내준다면 모두 처리가 가능해지는 것입니다.
다만 위 방식으로 이벤트를 하나씩 순차적으로 처리하면 아쉬운 점이 몇가지 있습니다. 그것은 바로 요청이 Blocking 될수도 있다는 점입니다.
쉽게말해, 요청을 비동기 방식으로 계속 차근차근 처리해나가면 처리 시간이 오래걸릴 수 있습니다. 문제가 발생할 수 있는 더 다양한 케이스를 나열해보자면 아래와 같습니다.
- 이벤트 큐(Event Queue) 중에서 시간이 오래 걸리는 작업을 수행해야할 이벤트가 존재할때 ex) 디스크에 읽고쓰는 작업
위와 같은 요청들을 처리하는 동안 Worker porcess는 다른 작업을 수행할 수 없으므로, 이벤트를 처리할 수 없어 성능이 매우 저하된다는 점이 발생합니다. 그래서 Nginx는 Thread pool 개념을 도입했습니다.
Worker process가 요청을 이벤트 큐에 넣으면, 각 요청들을 Thread pool에 있는 Thread들이 골구로 수행합니다.
예를들어 디스크에 읽고쓰는 작업을 해야한다면 그 뒤에 있는 이벤트는 요청을 처리하는 긴 시간동안 블로킹(blocking) 되는 상태입니다. Nginx 는 이런 상황을 방지하기 위해 그렇게 시간이 오래걸리는 작업을 따로 수행하는 쓰레드 풀을 만듭니다.
그리고 worker 프로세스는 지금 처리할 요청이 시간이 오래걸릴 것 같다 싶으면 해당 쓰레드 풀에게 그 이벤트를 위임하고, 큐 안에 있는 다른 이벤트를 처리하러갑니다.
Thread pool 개념을 도입했다고 해서 디폴트로는 적용되지 않는 상태입니다.
1.7.11 버전 이상에서부터는 별도의 설정이 필요하다는 점에 유의합시다!
(적용 방법은 공식문서에 나와있다.)
이러한 Worker 프로세스는 Nginx 가 존재하는 해당 서버의 CPU 코어 개수만큼 생성합니다. 이러면 코어가 담당하는 프로세스를 바꾸는 횟수를 대폭 줄일 수 있습니다. CPU 가 굳이 부가적인 일을 하지 않아도 되는 것이죠.
다시말해, CPU 의 Context Switching 사용을 줄이는 것입니다. 이게 바로 Nginx 가 택한 Event 기반 구조입니다. 또 이게 바로 아파치 서버와의 가장 큰 차이점입니다.
이렇게 수많은 동시 커넥션 양을 처리하는데에 있어 프로세스를 적게 만들다보니 가볍기까지 합니다. 프로세스를 적게 만드는 이 구조는 Nginx 의 설정을 동적으로 서버를 중단하지 않고도 바꾸는 것을 가능하게 합니다.
개발자가 nginx.conf 라는 설정파일에서 Nginx에 대한 설정을 변경하고, Nginx에 대한 설정을 적용하면 Master 프로세스는 그 설정에 맞는 Worker 프로세스를 따로 생성합니다.
그리고 기존에 있는 Worker 프로세스가 더 이상 커넥션을 생성하지 않도록 합니다. 그러고 시간이 지나, 기존 worker 프로세스가 감당하던 이벤트 처리가 모두 끝나면 해당 프로세스를 종료합니다.
그런데 이런 동적 설정 변경은 언제쓸까요?
아주 대표적인 경우로, Nginx 가 여러 동시 커넥션을 관리하는 도중에 뒷단에 서버가 추가되는 경우가 있습니다. 그때는 Nginx 가 로드밸런서의 역할을 담당하게 되는 것입니다. 로드 벨런서는 요청을 여러 서버로 분산하는 작업을 수행합니다.
로드밸런서에 대해 잘 모르시는 분들은 제 지난 포스팅 Proxy Server와 로드밸런싱, 수평확장(Scale Out)과 수직확장(Scale Up)에 대해 을 참고하세요!
로드밸런싱을 하기위해선, 설정정보 파일 nginx.conf 로 들어가서 분산작업을 처리해줄 뒷단의 서버들을 추가적으로 적어줘야합니다.
그런데 만일 Nginx 는 수많은 동시 커넥션을 감당하고 있었더라면, 설정을 바꾸기 위해 Nginx 를 종료하는 상황이 좀 어려웠을 겁니다.
그런데 동시적으로 설정을 변경할 수 있다면 어떨까요? 동시 커넥션을 유지한체 그 기존 요청을 계속해서 처리하면서, 별도로 뒷단에 서버를 동 추가할 수 있습니다.
기존 요청들을 수행하던 Worker 프로세스들은 요청을 완료하면 종료되고, Master Process 는 로드밸런싱되는 설정 정보에 기반하여 Worker 프로세스들을 생성할겁니다.
Nginx 는 이런 설정변경을 초당 수십번을 해도 무리없이 커넥션을 관리하고 요청을 서버에 전달합니다. 이벤트 기반 구조라서 가능한 방식인것이겠죠?
그런데 2008년 부터 아파치 서버는 점유율이 점점 낮아지기 시작하고, Nginx 는 빠른 속도로 점유율을 치고 올라오기 시작합니다. 도대체 2008년에 무슨일이 있었을까요?
2008년에는 스마트폰이 나오고 인터넷 환경을 바꾸기 시작했습니다. 스마트폰은 사람들이 인터넷을 더 많이 사용하게 된 계기이기도 하지만, 동시 커넥션을 훨씬 더 많이 생성하는 계기가 되기도 했습니다.
사람들은 다양한 정보를 실시간으로 제공받고 싶어하는 것입니다.
그리고 웹에 담기는 컨텐츠가 다양해지고, 그 용량이 커지면서 브라우저도 리소스를 빨리 가져오기 위해 여러 TCP 커넥션을 동시에 형성하기 시작했습니다. 그리고 각각의 커넥션은 모두 keep-alive 설정으로 유지 되었고요. 결국 동시 커넥션 문제를 처리해야 할 서버가 날이 갈수록 많아졌습니다.
회사들은 빠르게 Nginx 라는 대체제에 눈을 돌리기 시작했습니다. 특히 Nginx 는 대규모 사이트를 운영하고 있는 큰 회사들이 좋아할만한 솔루션입니다.
덕분에 Nginx 가 인터넷 트래픽을 관여하는 비중은 멈추지 않고 계속 증가했고, 현재 23년도에도 꾸준히 증가하는 추세입니다.
아파치 서버도 성능 개선을 많이 하는중입니다. 그러나 동시 커넥션 관리에선 Nginx 가 크게 앞서는중입니다. Nginx 는 동시 커넥션수가 많아져도 메모리 사용률이 낮고 일정한 비율이 나오는 반면, 아파치는 많이 사용합니다.
하지만 아파치는 지금껏 오랜기간 버그가 발생되지 않도록 수많은 개선이 이루어졌기 때문에 다양한 OS 에서 안정적이라는 특징을 지닙니다.
또한 앞서 설명드렸듯이, 아파치는 모듈을 추가해서 확장하기 쉽다는 특징도 지닙니다. 모듈의 종류도 아파치 서버가 Nginx 보다 훨씬 많죠.
사실 Nginx 는 웹서버로써 동시 커넥션 문제해결과 로드밸런싱으로써의 메인 기능외에도 다양한 기능을 제공합니다. 그 중 SSL 터미네이션 기능을 수행할 수 있습니다.
SSL 터미네이션이란 Nginx가 클라이언트와는 https 통신을하고, 서버와는 http 통신을 하는것을 말합니다.
이 구조를 만들어서 서버가 복구화 과정을 감당하지 않을 수 있도록 하는것입니다. 비즈니스 로직 처리에 리소스를 사용할 수 있도록 비용을 줄여주는 것이죠.
두번째로 Nginx 는 http 프로토콜을 사용해서 전달하는 컨텐츠를 캐싱할 수 있습니다.
이와 관련한 내용은 제 지난 포스팅 Proxy Server와 로드밸런싱, 수평확장(Scale Out)과 수직확장(Scale Up)에 대해 를 참고하시면 좋을듯합니다!
지금까지 1995 ~ 현재까지 이르기까지의 Nginx 의 역사를 깊게 알아보고, 해당년도에 어떤 한계를 부딪히고 왜 이런 Nginx 웹서버가 탄생했는지를 알아봤습니다.
또한 Nginx 의 내부 메커니즘을 최대한 유지적으로 설명드렸는데, CS 지식이 다소 포함되어 있어 최대한 쉽게 설명하는데 신경을 많이 쓴것 같네요!
이번 포스팅이 Nginx 를 정확히 이해하지 못하고 사용하시는 분들, 또 깊게 학습을 원하시는 분들에게 큰 도움이 되었으리라 믿습니다. 궁금하신 점이 있다면 댓글로 알려주세요. 제 포스팅을 읽어주신 모든 분들에게 감사드립니다! 😆
도움이 많이 되었습니다 감사합니다