ecsimsw

리버스 프록시 부하분산 개념과 시연 본문

리버스 프록시 부하분산 개념과 시연

JinHwan Kim 2022. 6. 17. 12:53

개념 1. 리버스 프록시 / Web server와 Web Application Server의 분리

 

 

 

 

서버 아키텍처에서 WAS(web application server)와 web server를 분리하는 방식이 보편적으로 사용되고 있다. 위 구조처럼 사용자 요청을 처리하는 로직을 갖는 서버 애플리케이션 전면에서 web server를 둬, 사용자 요청이 해당 web server를 거쳐 WAS에 전달되고, 반대로 WAS에서 처리가 완료된 결과가 WAS를 거쳐 다시 사용자에게 응답하는 구조이다.

 

이렇게 WAS 전면에 요청을 선 처리할 수 있는 web server를 두는 형태, 또는 그 web server를 리버스 프록시(reverse proxy)라고 한다. 이런 구조가 갖는 이점은 다음과 같다.

 

 

1. 정적 요청에 대한 선처리 / 캐싱

 

페이지는 많은 정적 자원으로 이뤄져 있다. 꼭 초창기의 웹뿐 아니라, 화려하게 변하는 현대식 동적 웹 페이지도 이미지, PDF 파일, CSS, JS 파일, 심지어 애초에 페이지의 일부는 정적인 html 문서를 사용하는 경우도 상당하다. 이런 정적인 자료는 어떤 요청에도 동일하게 응답된다. 즉 처리 로직이 필요하지 않는다는 말과 같다. 

 

WAS는 요청 처리 로직을 수행한다. 요청을 해석하고, DB를 엑세스하고, 그 결과를 조합하여 다시 응답을 만든다. WAS는 처리해야 하는 일이 많고, 전체 서버의 요청 처리 과정에서 많은 비중의 시간을 사용한다. 따라서 WAS의 부담을 최소화하는 것이 좋고, 이런 로직이 불필요한 요청은 가급적 WAS로 넘기지 않는 것이 좋다. 

 

 

WAS 전면에 분리된 web server에서 정적인 자료를 캐싱하여, 전면 web server에서 빠르게 처리해주는 것으로 WAS의 부담을 줄일 수 있다. 앞서 말한 것처럼 웹 요청에서 정적 자원을 요청하는 경우는 상당히 많으므로, 로직이나 DB 액세스가 불필요한 요청을 WAS에서 처리하는 것이 아니라, WAS에 도달 전 web server에서 캐싱된 자료를 이용하는 것만으로도 상당히 큰 성능 개선이 가능하다.

 

 

2. 유연한 교체 / 무중단 배포

 

이런 구조는 웹 서버와 연결된 WAS를 여러 개 하여 WAS 종류를 달리하거나, 무중단 배포에 사용하는 등 단일 WAS가 직접 사용자의 요청을 처리하는 것보다 유연하게 서버를 구성하고, WAS를 사용할 수 있게 한다.

 

 

 

이는 꼭 web server가 요청을 여러 WAS에 분산하는 로드 밸런싱을 말하는 것이 아니다. 부하 분산이 없는, 요청이 웹 서버에서 한 WAS만으로 넘어가는 구조라 하더라도, WAS를 쉽게 변경 가능함은 WAS를 유연하게 교체할 수 있다는 큰 이점을 얻는다.

 

가령 웹 서버에 서로 다른 두 WAS가 연결된 상태에서 한쪽 프로세스에 문제가 생긴다면 다른 쪽 WAS로 요청의 방향을 바꾸는 것으로 사용자 요청 처리를 유지할 수 있다. 또는 WAS가 배포될 때 기존의 WAS를 종료하지 않은 채로 새로운 배포 버전의 WAS 프로세스를 로드하고, 준비가 완료되면 요청의 방향을 새로운 배포 버전의 WAS로 돌리는 것으로 무중단 배포가 가능하다.

 

 

3. 전면 처리 

 

앞선 WAS를 연결/대기하는 구조나 이후에 설명에 로드밸런싱의 방식으로 하나의 웹 서버에 여러 WAS가 묶이는 구조를 쉽게 상상할 수 있다. 만약 여러 WAS에서 공통된 설정이 추가되고, 수정되는 상황이 생긴다면 어떨까. 가령 TLS 설정처럼 서버 보안 키가 설정된 상황에서 갑자기 키가 변경되었다고 생각해보자. 모든 WAS에 이 설정들이 저장되어 있다면 매번 모든 WAS의 보안 키를 수정하는 작업이 필요할 것이다.

 

WAS 전면에 웹서버를 두고 TLS나 HTTPS와 같은 설정을 프록시 서버에서 맡도록 한다. 이런 경우 앞선 예시의 설정이 변하는 상황에서 모든 WAS가 아닌, 전면의 리버스 프록시의 설정만 건들면 되는 것이다. WAS가 요청을 처리하는 비즈니스 로직에 집중할 수 있는 것이다. 

 

 

4. 로드 밸런싱

 

한 대의 WAS에 많은 요청이 동시에 몰리면 어떻게 될까. BackLog나 WAS 내부의 큐에 요청이 쌓였다가 처리되어 요청 처리가 늦어지고, 이보다 더 많은 요청에 요청 수가 큐 사이즈를 넘으면 그 외 요청들은 버려지게 되고, 너무 많은 요청 처리에 하드웨어 한계를 넘어 서버가 다운될 수 도 있다. 이런 요청을 여러 WAS 또는 모듈로 요청을 분산하는 기술을 로드 밸런싱이라고 한다. 앞선 웹서버-WAS가 분리된 구조라면 웹 서버에 여러 WAS를 연결하고, 요청이 웹 서버로 들어오면 웹 서버가 미리 정의된 부하 분산 방식에 따라 요청을 선택 WAS에 전달하는 것이다. 

 

이렇게 WAS를 여러 대로 나누면 서버의 상태에 따른 대응에도 유리하다. 예를 들어 웹 서버로 부하를 적절히 분산하면서 각 WAS의 상태를 확인하여 서버가 처리 불가 상황이라면 해당 WAS에게 요청을 넘기지 않는 것이다. 이를 통해 사용자에게는 WAS가 한 개 부족하지만 응답에는 문제에 상관없이 정상적인 서버 상황을 제공하는 것이다.

 

 

개념 2. 로드 밸런싱

1. L4, L7 부하 분산

 

로드 밸런싱을 적용한 서버 구조가 꼭 웹 서버 - WAS의 구조는 아니다. OSI7 계층의 4레벨에서 이뤄지는 L4 로드 밸런싱의 경우에는 전면 스위치에서 부하를 분산하여 뒤 서버에 전달한다. L4 로드밸런싱의 경우에는 Ip와 Port를 기반으로 분산하고, 데이터 내부를 확인하지 않기에 저레벨에서 이뤄지기에 더 빠르나, 분산의 기준으로 할 정보가 적다는 단점이 있다.

 

반대로 애플리케이션 레벨에서 이뤄지는 L7 로드밸런싱의 경우에는 데이터 내부 확인이 가능하기 때문에 저수준의 Ip, Port는 물론, 쿠키나 헤더 등의 데이터 내부의 정보가 분산의 기준이 될 수 있다. 즉 속도는 L4에 비해 느리나 더 섬세한 분산 기준을 둘 수 있다는 장점이 있다.

 

 

2. 세션을 유지하는 방법 / Sticky session과 Session clustering, Session storage

 

세션은 서버에 값을 저장한다. 부하 분산으로 요청을 처리하는 서버가 여럿이라면, 그래서 매번 요청을 처리하는 서버가 달라진다면 세션을 제대로 사용하지 못할 것이다. 이런 로드밸런싱을 처리하면서 서버의 사용자 세션을 유지할 방법을 고민해야 한다.

 

 

2-1 Sticky session

 

Sticky session은 이름 그대로 사용자 당 서버를 고정하는 방식이다. 첫 요청 이후의 모든 요청을 첫 요청을 처리한 서버로 두는 것으로 세션 문제를 해결한다. 대표적인 방법으로 사용자 cookie에 이용 서버를 저장하고, 이후 요청을 읽을 때 해당 쿠키 값을 기준으로 처리할 서버를 확인한다. cookie 값을 읽어야 하기에 이는 L7 로드밸런싱에서 가능한 방법이다.

 

또는 저레벨에서도 가능하도록 Ip Hash 방식으로 서버를 고정할 수도 있다. IP를 기준으로 이후 처리할 서버를 지정하는 것으로 사용자 당 동일한 서버를 접근하게 고정하여 세션 유지를 확실시하는 것이다. 다만, 이런 Sticky session 방식은 결국 사용자가 접속해야 하는 서버가 정해져 있기 때문에 트래픽 몰림 문제에서 완전히 자유로울 수는 없고, 서버에 장애가 발생하는 경우 해당 서버를 이용하고 있던 사용자의 세션 정보를 모두 잃게 된다는 위험이 있다.

 

 

2-2 Session synchronization

 

위의 Sticky Session이 결국 근본적인 트래픽 몰림에서 자유로울 수 없다는 문제가 있다. 이를 해결하기 위해 사용자가 모든 서버에 접근하도록 하되, 서버 간 세션을 동기화하는 방식이 사용된다. 자유로운 서버 선택과 각 서버 당 동일한 세션 정보의 보장이 가능하지만 세션 정보가 Create, Update, Delete 될 때마다 모든 서버에 이를 업데이트해야 하고 이는 부하를 야기할 수 있다는 문제가 있었다. 즉 동기화의 과정이 필요하므로 성능 저하가 올 수 있고, 개별 서버 안에서도 각 서버의 세션 정보가 아닌 모든 세션 정보를 저장해야 하므로 많은 메모리를 요하게 된다.

 

 

2-3 Session clustering 

 

Session clustering 방식은 각 서버의 세션을 묶어 관리하는 것이다. 사용자가 같은 묶음으로 접속한다면 동일한 세션 데이터를 받을 수 있도록 보장할 수 있다. 한 서버에 고정되지 않아도 되므로 트래픽 쏠림 문제를 해결할 수 있고, 개별 서버가 터지더라도 세션 정보를 유지할 수 있게 된다. N개의 서버가 있을 때 Clustering이 1개이면 전부를 동기화하는 synchronization 방식과 같고, Clustering이 N개이면 사용자 당 서버가 일대일 매칭 되는 Sticky session 방식과 동일한 것이다. 관리자는 Clustering의 개수를 이 사이로 조절하여 synchronization 방식과 sticky session 방식 각각의 효과와 문제를 관리하는 것이다.

 

(캐시의 매핑 방식으로 Direct mapped cache, Fully associative cache, 그 사이에서 set 개수를 조절하여 각각의 이점을 얻는 set associative cache 방식이 떠오른다.)

 

 

2-4. Primary-sendary session 복제 방식, Session storage

 

결국 동기화와 사용자 당 서버 Cluster를 고정해야 한다는 문제가 있기에, 모든 was가 아닌 하나의 서버에만 세션을 동기화하고, 그 외 서버에는 세션 키만 들고 있도록 하여 값을 백업하는 primary-sendary session 복제 방식과 여러 서버에서 하나의 별도의 세션 저장소(session storage)를 사용하는 세션 스토리지 방식으로 이런 세션 유지 문제를 처리하고 있다.

 

 

실습 1. LoadBalancing with NginX

1. 구현 내용 

 

- Nginx를 리버스 프록시로 사용하고, Client-웹서버-WAS 구조의 서버를 구성한다.

- Nginx를 로드밸런서로 설정하여 3개의 WAS에 요청을 분산한다.

- 로드밸런싱 방식 5가지가 적용된 Nginx 설정 파일을 작성하고 동작을 확인한다.

   - 방식1 : round robin 방식으로 부하를 분산한다.

   - 방식2 : WAS 중 하나가 다운됐을 때, 해당 WAS를 제외하고 분산한다.

   - 방식3 : 세 WAS의 성능이 다름을 가정하여, 각 WAS의 분산 빈도를 10: 1: 1로 설정하여 분산한다.

   - 방식4 : ip hash 방식으로, ip에 따라 동일한 WAS를 보장하도록 분산한다.

   - 방식5 : least connection 방식으로 커넥션 수가 가장 적은 WAS로 부하가 진행되도록 분산한다.

 

 

2. 시연 방법

 

LoadBalancer(35.174.150.226:80)으로 아래의 GET 요청을 반복적으로 보내고, 각 기대 결과를 확인한다.

각 요청마다 다른 balancing method 가 적용되어 있고, 응답은 각 WAS의 IP주소를 문자열로 직접 출력한다.

셋 중 하나의 WAS는 응답에 0.5초의 지연이 적용된다.

 

GET : http://35.174.150.226/info1 

-> 3가지 WAS가 순서대로 돌아가며 ip 주소를 출력한다.

 

GET : http://35.174.150.226/info2

-> 다운된 1개의 WAS를 제외한 2가지 WAS가 순서대로 돌아가며 ip 주소를 출력한다.

 

GET : http://35.174.150.226/info3

-> 3가지 WAS가 각각 10:1:1로 돌아가며 ip 주소를 출력한다.

 

GET : http://35.174.150.226/info4

-> 요청 클라이언트의 ip 주소에 따라 was가 결정된다. 시연 클라이언트의 ip가 고정일 경우 요청에서 같은 ip주소가 출력된다.

 

GET : http://35.174.150.226/info5

-> 가장 적은 연결의 was의 ip 주소가 출력된다. 여러 클라이언트로 요청을 반복할 경우, 0.5초의 지연이 존재하는 WAS는 다른 WAS보다 비교적 적은 횟수의 연결이 이뤄진다.

 

 

3. 실습 환경 

 

AWS EC2 : t2.large, Ubuntu 22.04 LTS (35.174.150.226)

AWS EC2 : t2.medium, Ubuntu 22.04 LTS (34.227.120.47)

AWS EC2 : t2.medium, Ubuntu 22.04 LTS (18.213.32.79)

 

 

 

 

4. WAS 정보 : https://github.com/ecsimsw/multiplexing-server-with-socketAPI  

 

- Socket API, Java NIO로 구현한 multiplexing http server

- Language : Java11

- Build tool : Gradle 7.0

- Description : /info1, /info2, /info3, /info4, /info5 입력 시 WAS의 ip 주소 출력

 

 

5.  LoadBalancer Nginx.config

events {}
  
http {
  upstream app1 {
    server localhost:8080;    
    server 34.227.120.47:8080;
    server 18.213.32.79:8080;
  }

  upstream app2 {
    server localhost:8080;        
    server 34.227.120.47:8080 down;
    server 18.213.32.79:8080;
  }

  upstream app3 {
    server localhost:8080 weight=10;
    server 34.227.120.47:8080;
    server 18.213.32.79:8080;
  }

  upstream app4 {
    ip_hash;  
    server localhost:8080;
    server 34.227.120.47:8080;
    server 18.213.32.79:8080;
  }

  upstream app5 {
    least_conn;
    server localhost:8080;
    server 34.227.120.47:8080;
    server 18.213.32.79:8080;
  }

  server {
    listen 80;

    location /info1 {
      proxy_pass http://app1;
    }

    location /info2 {
      proxy_pass http://app2;
    }

     location /info3 {
      proxy_pass http://app3;
    }

    location /info4 {
      proxy_pass http://app4;
    }

    location /info5 {
      proxy_pass http://app5;
    }
  }
}

 

 

실습 2. Proxy pass로 정적 자원 선처리

 

1. 문제 사항

 

WAS의 요청 처리에 정적 자원을 반환한다고 가정해보자. 예를 들어 "/hello"라는 요청의 response에 이미지 파일이 함께 반환되는 상황을 가정한다. 로드 밸런싱을 작업하여 각 WAS가 "/hello" 요청을 처리할 수 있다고 할 때, 가장 쉬운 해결 방식은 모든 WAS의 로컬 스토리지에 해당 이미지 데이터를 각각 둬 처리할 수 있을 것이다.

 

 

각 WAS 인스턴스에 이미지를 저장, 참조하는 구조

 

 

혹은 아래 그림처럼 각 WAS가 요청 처리 과정에서 외부 스토리지 서버에 새로운 요청을 만들고, 응답 받아 필요한 이미지 파일을 얻은 후에 최종 응답하는 구조를 만들 수도 있다.

 

WAS가 요청 처리 시 또 다른 요청을 만들어, 외부 스토리지 서버로부터 응답 받는 형식

 

앞선 두가지 방식 모두 좋은 구조라고 생각되지 않는다. 첫 번째 방법의 경우 매 데이터가 모든 WAS 인스턴스에 각각 필요하기 때문에 중복된 데이터가 매번 저장될 것이다. 또는 각 WAS의 로컬 데이터가 동기화가 되지 않아, 이미지 파일이 다르다면 (또는 한쪽만 존재하지 않는다면) 같은 요청인데도 서로 다른 이미지 파일로 응답을 하거나, 몇몇 WAS만 응답을 못하는 상황이 발생한다.

 

두 번째 방식은 WAS의 큰 오버헤드가 발생한다. 외부 서버에 요청을 쏘고, 응답을 얻는 과정은 큰 부담이 있다. 이 경우 DB 액세스, 응답 파싱 등 기본적으로 WAS 자체가 해야 하는 일도 많은 데다가, 외부 서버에 요청을 쏘고, 해당 서버가 응답하는 시간까지 기다려 최종 응답을 만들게 된다.

 

 

2. 구현 내용 

 

리버스 프록시(현재 실습의 로드밸런서)에서 정적 자원에 대한 처리를 선처리해주는 구조를 만들어 보았다. 앞서 언급한 바처럼 웹 서비스에서 정적 자원에 대한 요청은 상당히 많고, 이를 분산하는 것만으로도 많은 성능 개선이 가능하다.

 

아래 구조처럼 리버스 프록시가 처리할 수 있는 정적 자원에 한해서 외부 스토리지 서버를 바로 참조하는 구조를 만든다면, 앞선 문제 사항처럼 1. 자원이 각 로컬 스토리지에 중복 저장되어야 하는 문제, 2. 자원이 각 WAS마다 동기화되어야 하는 문제, 3. WAS에 부담이 된다는 문제의 세 가지 문제를 모두 해결할 수 있는 것이다.

 

 

 

 

3. 실습 환경 

 

실습 환경은 다음과 같다. 실습에선 정적 자원의 root path를 "/static"으로 고정되어 있다고 가정하였으나, 원하는 path로 수정하여도 문제가 되지 않는다. 외부 스토리지 서버도 실습에선 자원 업로드와 참조가 간단한 AWS S3로 진행하였으나, Http 요청으로 정적 자원을 응답 받을 수 있는 스토리지 서버라면 어떤 형태든 상관없이 같은 구조를 만들 수 있다.

 

AWS EC2 : t2.large, Ubuntu 22.04 LTS (35.174.150.226)

AWS EC2 : t2.medium, Ubuntu 22.04 LTS (34.227.120.47)

AWS EC2 : t2.medium, Ubuntu 22.04 LTS (18.213.32.79)

AWS S3

 

- 각 WAS는 "/static"으로 시작하는 이미지 파일을 응답으로 반환한다.

- AWS S3를 외부 스토리지 서버로 하고, 필요한 이미지 파일을 업로드 해둔다. (실습에선 'hello'라는 이름의 이미지 파일을 사용한다.)

 

 

4.  LoadBalancer Nginx.config

 

Nginx 설정은 다음과 같다. 'location / { }' 으로 세 WAS에 부하 분산을 정의하고, 'location /static/'으로 /static으로 시작하는 요청을 외부 스토리지 서버로 패싱 한다.

 

이때 location을 '/static/'으로 잡는지, '/static'으로 잡는지가 큰 차이를 낳는데, 전자의 경우 {스토리지-서버-주소}/{요청자원}으로 요청이 패싱 되고, 후자의 경우 {스토리지-서버-주소}/static/{요청자원}으로 요청이 패싱 된다. 즉 자원의 주소만 static~으로 하고 S3 객체의 제목은 static을 추가하지 않고자 한다면 '/static/'으로 location path를 정의하여 static을 자원 요청 주소에서 제거해야 한다.

events {}

http {

  upstream hello {
     server localhost:8080;
     server 34.227.120.47:8080;
     server 18.213.32.79:8080;
  }

  server {
    listen 80;

    location / {
      proxy_pass http://hello;
    }

    location /static/ {
      proxy_pass https://{buck-name}.s3.amazonaws.com/; // 외부 스토리지 서버 주소
    }
  }
}

 

5. 결과

 

로드 밸런서의 ip주소에 "/hello"를 요청하고, 응답 받은 내용이다. WAS는 html만 파싱 하여 전달하고 실제 img src에 해당하는 '/static/sample'은 WAS 내부 파일로 존재하지 않고, 리버스 프록시에서 S3에 직접 요청하여 자원을 처리하였다.

 

 

 

실습 3. AWS의 ELB (ALB, NLB)

AWS의 Elastic Load balancing 서비스를 이용하면 L4, L7 수준의 부하 분산을 쉽게 설정하고 편하게 모니터링 할 수 있다. 앞선 웹 서버를 활용한 실습의 경우처럼 로드밸런서를 위한 EC2 인스턴스를 직접 만드는 것이 아닌 로드 밸런서의 ip와 port, 타겟이 될 서버(인스턴스, 이미지 등) 목록을 설정해주면 로드밸런서에 요청 시 타겟 서버로 요청을 분산 전달하게 된다.

 

1. EC2 -> 로드 밸런싱 -> 로드 밸런서를 생성한다.

 

2. 원하는 load balancer type을 선택한다.

 

3. VPC : 타겟이 될 인스턴스의 VPC를 선택하고, Mappings : 트래픽이 전달될 인스턴스의 가용 영역을 선택한다.

 

4. Load balancer가 Listening 할 protocol과 port를 입력한다. Default action으로 분산 타겟을 지정하는데, target이 없다면 아래 create target group으로 타켓이 될 그룹 생성 페이지로 이동한다.

 

5. 타겟 타입을 설정한다. 이미 서버 인스턴스가 존재하기에 Instances를 선택하고, 해당 서버 인스턴스가 요청을 처리하는, Load balancer의 요청을 전달받을 포트를 선택한다. (예시에선 8080)

 

6. Load balancer 생성 완료한다.

 

7.  활성 상태가 될 때까지 기다리고, 활성 상태가 된다면 DNS 이름을 ip로, port를 설정에서 설정한 port로 하여 요청을 보내면 이전에 설정한 target 그룹에 해당하는 서버로 요청이 분산되어 처리되고 있는 것을 확인할 수 있다.

 

 

8. 위 ELB 실습 예시는 ALB(L7)으로 그림을 첨부하였으나, Load balancer의 타입을 Network load balancer로, Listening 할 프로토콜 타입과 target group의 프로토콜 타입을 TCP로 하는 과정만 달리한다면 NLB(L4) 역시 다른 과정은 동일하여 쉽게 설정할 수 있었다.

 

 

 

Comments