Server

[Nginx] Nginx를 통한 무중단배포

날아 2024. 4. 24. 03:35

토이 프로젝트를 진행하던 중 github Action, aws의 Codedeploy를 통해 CI/CD 과정에서 최신 버전을 배포할 때 서버가 꺼지는 현상이 있었다.

따라서 nginx를 통해 무중단배포를 구현했다. (단 두줄로 설명되지만 약 2주가 걸렸던 험난한...과정이었다.)

사실 Nginx를 선택한 이유는 무료여서 였다.......서버를 두 대 띄울 돈이 없었다...ec2도 프리티어를 사용한 마당에....🥹

하지만 토이 프로젝트를 복기하며 공부하니 Nginx를 통해 어떻게 무중단 배포를 구현할 수 있었는지 더 깊게 알게되었고, 이를 정리해보고자 한다.

 

1. Nginx란?

Nginx란 트래픽이 많은 웹사이트의 서버(WAS)를 도와주는 비동기 이벤트 기반구조의 웹 서버 프로그램이다.
클라이언트로부터 정적 파일을 요청받았을 때 응답해주는 Web Sever로 활용되기도 하고, 
Reverse Proxy Server로 활용하여 WAS 서버의 부하를 줄일 수 있는 로드 밸런서로 활용되기도 한다. 

 

여기서 짧게 설명하자면,

  • Web Server : 단순한 정적 파일을 응답 (Nginx, Apache 등등)
  • Web Application Server : 클라이언트 요청에 대해 동적 처리가 이루어진 후 응답 

 

비동기 이벤트 기반구조를 이해하려면 먼저 Nginx가 나타나게 된 배경을 아는 것이 좋다.

 

 

Apache

Nginx가 나타나기 이전 웹 서버는 Apache가 가장 점유율이 높았다. 

 

Apache 서버는 클라이언트의 요청이 들어오면 커넥션을 형성하기 위해 프로세스를 생성한다. 

즉, 새로운 요청이 들어올 때마다 프로세스를 새로 만든다.

(이 과정에서 프로세스 생성 시간을 단축하기 위해 프로세스를 미리 생성하는 PREFORK 방식을 사용했다.)

 

새로운 클라이언트 요청이 들어오면 미리 만들어 놓은 프로세스를 가져다 사용하고, 만들어 놓은 프로세스가 모두 할당되면 추가로 프로세스를 만들었다. 

 

그러나 1999년 부터 서버 트래픽 양이 높아지며 서버에 동시 연결된 커넥션이 많아졌을 때 더 이상 커넥션을 형성하지 못하는 문제가 생겼다. (C10K 문제 - Connection 10000 Problem)

  • 메모리 부족 : 커넥션마다 프로세스를 만들기 때문에 메모리 부족을 야기 
  • CPU 부하 증가 : 많은 커넥션에서 요청이 들어오면 context Switching을 많이 하기 때문에 CPU 부하가 높아짐 

 

 

NginX의 등장

Nginx는 2004년 아파치 서버를 보완하기 위해 출시되었다. 

초창기 Nginx는 단독적으로 사용되기보다는 아파치 서버와 함께 사용하기 위해 만들어졌다. 

수많은 동시 커넥션을 Nginx가 유지하고, Nginx 또한 웹서버이기 때문에 정적 파일에 대한 요청은 스스로 처리하여 아파치 서버의 부하를 줄였다. 

 

 

NginX의 구조

그렇다면 Nginx는 어떻게 수많은 동시 커넥션을 유지할까? 

 

Nginx는 Event-Driven 구조, 즉 비동기 이벤트 기반 구조이다.

여러 개의 connection을 전부 Event Handler를 통해 비동기 방식으로 처리해 먼저 처리되는 것부터 로직이 진행되게끔 하는 것이다. 

자세히 알아보자.

 

 

  • Nginx는 하나의 Master Process와 다수의 Worker Process로 구성되어있다. 
  • Master Process는 설정 파일을 읽고 유효성을 검사하고, Worker Process는 요청을 처리한다. 
  • 워커 프로세스(Worker Process)는 생성될 때 각자 지정된 listen 소켓을 배정받고, 그 소켓에 새로운 클라이언트 요청이 들어오면 커넥션을 형성하고 처리한다. 
  • 커넥션은 정해진 Keep Alive(Http Header에서 볼 수 있는) 시간만큼 유지되는데, 이렇게 커넥션이 형성되었다고 해서 워커 프로세스가 커넥션 하나만 담당하지는 않는다. 
  • 형성된 커넥션에 아무런 요청이 없으면 새로운 커넥션을 형성하거나 이미 만들어진 다른 커넥션으로부터 들어온 요청을 처리한다. 
  • Nginx에서는 이러한 커넥션 형성과 제거, 새로운 요청을 처리하는 것을 이벤트(event)라고 부른다.

 

 

이벤트들은 OS커널이 큐(Queue) 형식으로 워커 프로세스에 전달한다. 

이벤트는 큐에 담긴 상태에서 워커 프로세스가 처리할 때까지 비동기 방식으로 대기한다.

그리고 워커 프로세스는 하나의 스레드로 이벤트를 꺼내서 처리한다.

 

만약 큐에 담긴 요청 중 하나가 시간이 오래 걸린다면, 스레드 풀(Thread Pool)을 만들어 그 요청을 따로 수행한다.

워커 프로세스는 처리할 요청이 시간이 오래 걸릴 것 같으면 스레드 풀에 이벤트를 위임하고 다른 이벤트를 처리하는 것이다. 

 

이러한 방식으로 worker process는 쉬지 않고 일을 하기 때문에, 요청이 없을 때 프로세스를 방치시키는 아파치 서버보다 훨씬 효율적으로 자원을 사용할 수 있다. 

 

 

Nginx 기능 

  • 웹 서버
  • Foward Proxy, Reverse Proxy
  • 로드 밸런서
  • SSL 터미네이션 : 클라이언트와 https 통신하고, 서버와 http 통신하는 것
  • 캐싱 : http 프로토콜을 사용하여 전달하는 콘텐츠를 캐싱할 수 있기 때문에 한 번 서버에서 응답받은 것을 스스로 보관하고 클라이언트에 전달한다. 

2. Nginx의  Reverse Proxy

Nginx는 reverse proxy로 활용할 수 있다. reverse proxy는 클라이언트와 서버 사이에서 중개자 역할을 하여 요청들을 설정에 따라 알맞은 내부 서버로 접근할 수 있도록 도와주는 서버이다. 리버스 프록시는 클라이언트의 요청을 대신 받아서 서버들에게 요청을 전달한다.

  • 이를 통해 프록시 서버에 암호화를 설정이나 여타 서버 공격들이 실제 서버가 아닌 프록시 서버를 공격하도록 함으로써 서버들을 보호할 수 있다. 
  • 또한, 프록시 서버로 오는 요청들을 분산하여 한 서버에만 부하가 되지 않도록 로드밸런싱의 기능도 수행한다. 

 

나는 이 reverse proxy 기능을 통하여 무중단 배포를 구현했다. 

 

Nginx는 reload를 통해 동적으로 설정을 변경할 수 있다. 

 

개발자가 설정 파일을 변경하고 Nginx에 적용하면 마스터 프로세스는 거기에 맞게 워커 프로세스를 새로 생성한다.

기존의 워커 프로세스가 더 이상 커넥션을 형성하지 않도록 하여 처리하는 이벤트가 없으면 해당 프로세스를 종료시킨다.

 

 

위와 같이 하나의 EC2 서버에 Nginx 1대와 스프링 부트 Jar 2대를 사용하였다. 

  • 사용자는 서비스 주소로 접속한다. (80 혹은 443 포트)
  • 엔진엑스는 사용자의 요청을 받아 현재 연결된 스프링 부트1로 요청을 전달한다. 연결되지 않은 스프링 부트2는 전달받지 못한다.
  • 1.1 버전으로 신규 배포가 필요하면 엔진엑스와 연결되지 않은 스프링 부트2로 배포된다. 엔진엑스는 스프링 부트 1을 바라보고 있으므로 배포하는 동안 서비스가 중단되지 않는다.
  • 배포 이후 스프링 부트2가 정상적으로 구동되는지 확인하고, nginx reload명령어 (0.1초 소요)를 통해 스프링 부트 2를 바라보도록 한다.

 

 

참고로 실제 코드들을 요약해보자면, 

Nginx의 /etc/nginx/nginx.conf 라는 Nginx 설정 파일로 들어가 

location / 부분을 아래와 같이 변경했다.

 

  • include /etc/nginx/conf.d/service-url.inc;
    • 배포할 스프링 부트 포트 번호가 적힌 설정 파일을 include 하겠다는 의미이다.
    • 해당 파일을 Include 하면 service-url.inc에 있는 service-url 변수를 proxy pass에 적용할 수 있다. 

해당 설정을 완료한 뒤, Nginx를 재시작하면 클라이언트가 80, 혹은 443 포트로 접속할 경우, Nginx는 리버스 프록시 기능으로 현재 배포중인 포트(8081 혹은 8082) 포트로 요청한다. 

 

 

 

참고