AWS EC2에서 Nginx를 통한 Load Balancing 및 High Availability

zdpk·2024년 7월 3일

AWS

목록 보기
4/4
post-thumbnail

https://velog.io/@mainfn/nginx1

지난 장에서 nginx를 EC2에 셋업하고 SSH 연결 및 health check를 해봤다.

이번에도 동일한 환경으로 EC2 Instance를 하나 띄우면서 시작할 것이다.

Nginx 혹은 EC2 세팅 및 SSH 연결 방법을 잘 모른다면 위 링크를 참조하도록 하자.


환경 설정 및 nginx 설치

EC2 Instance를 하나 만들 것이다.

Instance 명은 nginx-vm, Machine Type은 t2-micro, 원하는 KeyPair를 만들고, 마지막으로 Inbound Rules에서 8080 port를 허용하도록 하자.

# local

# ssh key agent에 등록
$ ssh-add ./secret-key

$ ssh ec2-user@43.202.67.226
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'

SSH 접속이 완료되었다.

이제부터는 EC2에서 실행되는 명령어라고 생각하자.

# EC2

# nginx 설치
$ sudo yum install nginx -y

# nginx 실행
$ sudo systemctl start nginx

# nginx 상태 확인
● nginx.service - The nginx HTTP and reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; preset: disabled)
     Active: active (running) since Wed 2024-07-03 07:29:36 UTC; 2s ago
    Process: 25581 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)
    Process: 25582 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
    Process: 25583 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
   Main PID: 25584 (nginx)
      Tasks: 2 (limit: 1114)
     Memory: 2.2M
        CPU: 54ms
     CGroup: /system.slice/nginx.service
             ├─25584 "nginx: master process /usr/sbin/nginx"
             └─25585 "nginx: worker process"

nginx 설치 및 세팅이 완료되었다.

$ curl localhost:80 -I

HTTP/1.1 200 OK
Server: nginx/1.24.0
Date: Wed, 03 Jul 2024 07:30:10 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Fri, 13 Oct 2023 13:33:26 GMT
Connection: keep-alive
ETag: "65294726-267"
Accept-Ranges: bytes

기본 80 port에서 실행되므로, 세팅이 잘 되었다면 위와 같은 응답을 받게 된다.


L7 Load Balancer, Reverse Proxy

저번에는 Nginx의 static serving에 대해서만 다뤘지만, 이번에는 nginx를 간단한 Load Balancer로서 사용 해보도록 할 것이다.

이런 그림을 생각하면 된다.

local에서 43.202.67.226으로 요청을 보내면 nginx가 이를 받아서 뒤에 있는 backend 중 하나에게 실제 처리를 위임한다.

backend는 처리가 끝나면 nginx에게 응답을 생성하여 넘겨주고, 이를 다시 local에 넘겨준다.

즉, nginx는 응답 및 요청에 대한 '위임'만 담당하게 되는 것이다.

그래서 nginxReverse Proxy라고 부르기도 한다.

local을 택배 발송자, nginx를 택배기사, backend를 택배 수신자라고 생각하면,

택배 발송자로부터 직접 물건을 받는 것이 아니라, 택배기사가 중간에 물건을 전달해준다.

택배 발송자 입장에서는 택배 기사가 proxy, 택배 수신자 입장에서는 택배 기사를 reverse proxy라고 볼 수 있겠다.

직접 전송이 아닌 중개자를 끼는 것이 proxy이며

보낼 때, 받을 때 입장에 따라 proxy, reverse proxy가 되는 것이다.

Reverse Proxy를 앞에 두고 뒤에 N개의 Backend를 두는 구조를 가져가면, 실제 로직 처리는 Backend에서 일어나기 때문에 Reverse Proxy는 그저 전달만 하는 용도로 크게 부하가 걸리지 않는다.

만약 요청이 많이 들어오면 Backend를 뒤에 추가하여 단일 IP 진입점을 구성하고 실제 로직 처리 서버는 원하는 만큼 둘 수 있다.

이름 그대로 부하 분산(Load Balancing)을 담당하며, 덕분에 Backend 중 하나가 멈춰도 나머지 Backend가 돌아가고 있기 때문에 서비스에 장애가 생기지 않은 것처럼 보이게 할 수 있다.

또한 Backend 서버가 업데이트 되어 재배포 될 때도 Backend Instance 4개가 있는 경우, 4개를 한 번에 재배포 하는 것이 아니라, 1개씩 순차적으로 배포 하는 식으로 무중단 배포를 할 수도 있다.

이런 다양한 장점 덕분에 Reverse Proxy, L7 LB는 대부분의 서비스에서 아키텍처를 구축할 때 거의 필수적으로 사용된다.


Simple Express Backend

nodejs로 간단한 express 서버를 만들어서 실제 로직을 처리하도록 구동시킬 것이다.

JS를 몰라도 백엔드 개념만 알고 있다면 전혀 상관 없다.

우선 EC2에서 nodejs를 설치한다.

# EC2

# nodejs 설치
$ sudo yum install nodejs -y

홈 아래 app 디렉토리를 만들고 해당 디렉토리를 npm init -y로 초기화 한다.

$ mkdir app && cd app

$ npm init -y

npmyum으로 nodejs를 설치할 때 자동으로 설치되었다.

nodejs용 패키지 매니저다.

$ npm i express

웹서버 프레임워크인 express를 설치한다.

app.js라는 파일을 만들고 간단한 API 서버를 만들 것이다.

$ vi app.js
// app.js
const app = require('express')();

// 환경 변수 PORT
const { PORT } = process.env;

// root로 요청 보내면 현재 서버 구동중인 PORT 응답으로 보냄
app.get('/', (req, res) => {
        res.end(`Response from ${PORT}`);
});

// 입력 받은 PORT에서 서버 listen
app.listen(PORT, () => {
		// 실행될 때 출력
        console.log(`app is running on ${PORT}`);
});

실행 방법 또한 간단한데, 한가지 주의할 점은 PORT라는 환경 변수를 주입해야 한다.

# 환경 변수 PORT 주입 및 서버 실행
$ PORT=8081 node app.js

app is running on 8081

그런데 이렇게 실행하면 터미널이 blocking 되어 요청을 보낼 수 없게 된다.

이를 방지하기 위해 백그라운드에서 실행해야 하는데, 명령어 맨 뒤에 &만 붙이면 된다.

# 백그라운드에서 서버 실행
$ PORT=8081 node app.js &
[1] 27635
[ec2-user@ip-172-31-13-220 app]$ app is running on 8081

그러면 [1] 27635라는 메세지가 나오고 터미널이 blocking 되지 않아서 계속 입력이 가능하다.

백그라운드 실행 시 나오는 27635는 방금 실행한 express 서버의 process id라고 생각하면 된다.

ps -ef 명령어로 pid가 27635인 프로세스를 찾아보면 잘 출력되는 것을 할 수 있다.

$ ps -ef  | grep 27635

ec2-user   27635    2340  0 08:26 pts/0    00:00:00 node app.js
ec2-user   27698    2340  0 08:28 pts/0    00:00:00 grep --color=auto 27635

프로세스가 잘 실행되고 있는 것을 확인했으니 요청을 보내보자

$ curl localhost:8081

Response from 8081

우리가 입력한대로 응답이 잘 온다.

이제 3개의 서버를 더 띄우도록 하겠다.

$ PORT=8082 node app.js &

$ PORT=8083 node app.js &

$ PORT=8084 node app.js &

8081, 8082, 8083, 8084 4개의 port에서 동일한 서버가 실행되고 있다.

PORT를 각자 다르게 주입했기 때문에 응답 결과에서 PORT 번호 만큼은 다를 것이다.

$ curl localhost:8082
Response from 8082

$ curl localhost:8083
Response from 8083

$ curl localhost:8084
Response from 8084

모든 서버에서 응답이 잘 오는 것을 확인했다면 성공이다.

이제 nginx의 뒤에 방금 작성한 4개의 서버에 처리를 위임하면 LB로서 사용하게 된다.


Nginx as Load Balancer

nginx의 기본 설정 파일인 /etc/nginx/nginx.conf의 내용을 모두 지우도록 하자.

$ sudo vi /etc/nginx/nginx.conf

내용을 다음과 같이 수정할 것이다.

http {
   
  upstream backend {
	server 127.0.0.1:8081;
	server 127.0.0.1:8082;
	server 127.0.0.1:8083;
	server 127.0.0.1:8084;
  }

	server {
		listen 8080;

		location / {
			proxy_pass http://backend;
		}
	}
}

events {}

우선 events 블록은 없으면 오류가 나기 때문에 넣었다.

지난 시간에 이미 봤던 부분부터 살펴보면,

port 8080에서 nginx를 실행 하겠다는 뜻이며,

location 블록이 하나이며 root(/)기 때문에 어떤 경로로 들어오더라도 location / 블록에 매칭된다.

	server {
		listen 8080;

		location / {
			proxy_pass http://backend;
		}
	}

proxy_pass라는 것이 들어 있는데, http://backend로 요청을 위임 하겠다는 소리다.

원래는 도메인이나 ip 주소를 넣어줘야 하지만, backendupstream에 등록되어 있기 때문에 잘 작동한다.

upstream 쪽을 자세히 보면

  upstream backend {
	server 127.0.0.1:8081;
	server 127.0.0.1:8082;
	server 127.0.0.1:8083;
	server 127.0.0.1:8084;
  }

서버 4개가 있고, 127.0.0.1:PORT 형태로 등록되어 있다.

덕분에 proxy_passhttp://backend에서 backend

실제로 backend에 등록된 127.0.0.1:8081 등으로 치환된다.

즉, 다음 4가지 경우가 가능해진다.

http://backend -> http://127.0.0.1:8081
http://backend -> http://127.0.0.1:8082
http://backend -> http://127.0.0.1:8083
http://backend -> http://127.0.0.1:8084

기본적으로 Round Robin 방식으로 목적지 주소가 치환되는데,

Round Robin이 그냥 순서를 번갈아가면서 선택 하겠다는 소리다.

첫 요청은 http://backendhttp://127.0.0.1:8081이 되고,
두번째는 http://127.0.0.1:8082이 되고...

이를 계속해서 반복한다.

덕분에 4대의 backend 서버가 균일한 트래픽을 분담하게 된다.

가장 기본적인 Load Balancing 전략이다.

설정을 저장하고 잘 동작하는지 확인해보겠다.

# 변경된 설정 nginx에 반영
$ sudo nginx -s reload
$ curl localhost:8080
Response from 8081

$ curl localhost:8080
Response from 8082

$ curl localhost:8080
Response from 8083

$ curl localhost:8080
Response from 8084

$ curl localhost:8080
Response from 8081

...

nginx 서버로 요청을 보낼 때마다 다른 port에서 실행중인 backend가 번갈아가면서 실제 요청을 처리하는 것을 볼 수 있다.


HA(High Availability)

Cloud에서 HA(고가용성)라는 단어가 상당히 자주 사용된다.

서비스의 일부에 장애가 발생하더라도, 전체 시스템은 중단되지 않고 정상 동작하는 능력, 트래픽이 높아지면 이에 대응하여 Scale out 할 수 있는 능력 등을 일컫는다.

이 중에서 장애 대응 능력을 테스트 해보도록 하겠다.

현재 nginx 뒤에서 4개의 express server가 실행 중인데, 만약 이 중 1개에 장애가 발생하여 종료된다면 어떻게 될지 시뮬레이션 해보자.

4개 중 1개의 process를 강제 종료할 것인데, 다음 명령어로 node로 실행한 서버의 process id를 찾을 수 있다.

$ ps -ef | grep node

root         982       2  0 07:26 ?        00:00:00 [xfs-inodegc/xvd]
ec2-user   27635    2340  0 08:26 pts/0    00:00:00 node app.js
ec2-user   28430    2340  0 08:52 pts/0    00:00:00 node app.js
ec2-user   28443    2340  0 08:53 pts/0    00:00:00 node app.js
ec2-user   28453    2340  0 08:53 pts/0    00:00:00 node app.js
ec2-user   29414    2340  0 09:29 pts/0    00:00:00 grep --color=auto node

process id가 28453인 서버를 강제로 종료하도록 하겠다.

kill 명령어는 특정 pid를 가진 process에게 signal을 보낸다.

기본적으로 SIGTERM이 보내지기는데, '종료 요청'을 뜻한다.

$ kill 28453

다시 확인해보면 서버 하나가 사라져있다.

$ ps -ef | grep node

root         982       2  0 07:26 ?        00:00:00 [xfs-inodegc/xvd]
ec2-user   27635    2340  0 08:26 pts/0    00:00:00 node app.js
ec2-user   28430    2340  0 08:52 pts/0    00:00:00 node app.js
ec2-user   28443    2340  0 08:53 pts/0    00:00:00 node app.js
ec2-user   29514    2340  0 09:33 pts/0    00:00:00 grep --color=auto node

정상 동작한 것이다.

$ curl localhost:8080
Response from 8081

$ curl localhost:8080
Response from 8082

$ curl localhost:8080
Response from 8083

$ curl localhost:8080
Response from 8081

...

port 8084에서 실행되던 서버가 사라졌다.

kill 명령어에 의해 정상적으로 종료된 것이다.

그러나 nginx 뒤에 아직 돌아가고 있는 3개의 서버가 존재하기 때문에 전체 서비스 작동에는 문제가 되지 않는다.

실제로 이런 식으로 일부 인스턴스에 문제가 발생하더라도 나머지가 커버를 해주기 때문에 LB로 HA를 달성할 수 있다.

반대로 트래픽이 많아진 상황이 된다면, 인스턴스를 늘려야 할 것이다.

nginx 설정에서 backend를 3개 추가하겠다.

$ sudo vi /etc/nginx/nginx.conf
http {
        upstream backend {
                server 127.0.0.1:8081;
                server 127.0.0.1:8082;
                server 127.0.0.1:8083;
                server 127.0.0.1:8084;
                server 127.0.0.1:8085;
                server 127.0.0.1:8086;
                server 127.0.0.1:8087;
        }
        
        
        ...

생각해보니 upstream에 대해 설명을 안한 것 같은데 간단히 설명하고 넘어가겠다.

강의 상류, 하류를 생각하면 이해하기 쉽다.
실제 데이터를 내려주는 곳이 상류, 내려 받는 곳이 하류.
서버로 업로드, 클라이언트가 다운로드 하는 것과 같은 이치다.
nginx 입장에서는 실제 데이터가 내려오는 express 서버들이 upstream이 된다.

express server 4개를 추가로 실행하겠다.

$ PORT=8084 node ./app.js &

$ PORT=8085 node ./app.js &

$ PORT=8086 node ./app.js &

$ PORT=8087 node ./app.js &

ps -ef로 확인하면 잘 돌아가는 것을 확인할 수 있다.

$ ps -ef | grep node

root         982       2  0 07:26 ?        00:00:00 [xfs-inodegc/xvd]
ec2-user   27635    2340  0 08:26 pts/0    00:00:00 node app.js
ec2-user   28430    2340  0 08:52 pts/0    00:00:00 node app.js
ec2-user   28443    2340  0 08:53 pts/0    00:00:00 node app.js
ec2-user   29987    2340  1 09:50 pts/0    00:00:00 node ./app.js
ec2-user   29997    2340  1 09:50 pts/0    00:00:00 node ./app.js
ec2-user   30007    2340  1 09:50 pts/0    00:00:00 node ./app.js
ec2-user   30017    2340  3 09:50 pts/0    00:00:00 node ./app.js
ec2-user   30028    2340  0 09:50 pts/0    00:00:00 grep --color=auto node

사실 작동중인 웹서버를 확인하는 좀 더 좋은 명령어가 있는데,

netstat -ntl로 실행중인 웹서버 + port까지 확인할 수 있다.

$ netstat -ntl

Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN
tcp6       0      0 :::8086                 :::*                    LISTEN
tcp6       0      0 :::8087                 :::*                    LISTEN
tcp6       0      0 :::8084                 :::*                    LISTEN
tcp6       0      0 :::8085                 :::*                    LISTEN
tcp6       0      0 :::8082                 :::*                    LISTEN
tcp6       0      0 :::8083                 :::*                    LISTEN
tcp6       0      0 :::8081                 :::*                    LISTEN
tcp6       0      0 :::22                   :::*                    LISTEN

유용하게 사용할 수 있을 것이다.

:::8086 형태로 나오는 것은 IPv6 표기법이라 그렇다.

nginx 설정을 리로드 하는 것도 잊지 말자

$ sudo nginx -s reload

이제 nginx에 요청을 보내서 새로 추가된 인스턴스에 요청이 잘 전달되는지 확인하자.

$ curl localhost:8080
Response from 8081

$ curl localhost:8080
Response from 8082

$ curl localhost:8080
Response from 8083

$ curl localhost:8080
Response from 8084

$ curl localhost:8080
Response from 8085

$ curl localhost:8080
Response from 8086

$ curl localhost:8080
Response from 8087

$ curl localhost:8080
Response from 8081

...

이렇게 수동 Scale Out 및 Load Balancing이 완료되었다.

대규모 Application의 경우, 최근에는 이런식의 수동 Scale Out 및 Load Balancing을 하지 않는다.

이런 것을 AWS의 Auto Scaling Group이나 Kubernetes가 자동으로 관리 해주기 때문이다.

트래픽이 늘어나면 자동으로 동일한 서버 Instance가 추가되고, 트래픽이 Load Balancing 된다.

지금처럼 backend Instance가 몇 개가 아니라, 수 십, 수 백, 수 천개로 늘어난다면 Auto Scale Out을 사용하는 것이 훨씬 합리적일 것이다.

0개의 댓글