토이 프로젝트에서 로드밸런서 설정 후 세션 동기화 하기

민씨·2024년 1월 30일
0
post-custom-banner

개요

제가 진행하는 토이 프로젝트는 AWS EC2 인스턴스에 하나의 스프링 부트 애플리케이션 실행되고 있습니다.

저는 이 프로젝트의 복잡성을 높이기 위해 마치 스케일-아웃(Scale-Out)이 된 것 처럼 두 개의 스프링 부트 애플리케이션을 실행시키고 싶었는데요.

이에 따라 클라이언트 요청을 두 서버에 균등하게 분산하기 위해 로드밸런서(Nginx)의 필요성을 느끼게 되었습니다.

이번 포스팅에서는 로드밸런서(Nginx)를 설정하는 과정을 살펴보고 서버가 2개가 되었더니 발생한 세션 동기화 문제에 대해 알아보겠습니다.

로드밸런서 설치

웹 사이트 접속 시 일반적으로 8080 포트가 아닌, HTTP 통신의 기본 포트인 80을 사용합니다. 이를 활용하여 저는 아래처럼 포트 80에서 들어오는 요청을 8080으로 포워딩하는 설정을 해놓았었습니다.

version: '3.8'

services:
  app:
    image: ghcr.io/ber01/lo-gak-gye:latest
    container_name: lo-gak-gye
    ports:
      - "80:8080"

이제 로드밸런서를 설치한 뒤, 아래의 그림처럼 개선해 보겠습니다.

기존의 스프링 부트 애플리케이션은 docker-compose를 수정하여 8080 포트를 노출하도록 변경해 주었습니다.

version: '3.8'

services:
  app:
    image: ghcr.io/ber01/lo-gak-gye:latest
    container_name: lo-gak-gye
    ports:
      - "8080:8080"

EC2 인스턴스의 운영체제는 Amazon Linux 2023 입니다.

  1. 패키지 저장소 업데이트

    sudo dnf update -y
  2. Nginx 설치

    sudo dnf install nginx -y
  3. Nginx 서비스 시작

    sudo systemctl start nginx
  4. Nginx 설치 확인

    sudo systemctl status nginx
    스크린샷 2024-01-29 오후 8 57 08
  5. 재부팅 시 Nginx 활성화

    sudo systemctl enable nginx
  6. 접속 테스트

    스크린샷 2024-01-29 오후 9 00 38

로드밸런서 설정

이제 Nginx의 nginx.conf 설정 파일을 편집하여, 클라이언트로부터 들어오는 요청을 원하는 애플리케이션으로 포워딩하는 방법을 설정해야 합니다.

Nginx의 버전마다 설정 파일의 경로가 다를 수 있겠지만, 제가 설치한 1.24.0 버전 설정 파일의 위치는 /etc/nginx/nginx.conf 입니다.

스크린샷 2024-01-29 오후 9 11 12

많은 설정 중 중요한 것은 server 블록 내의 location 속성을 통해, 80 포트로 들어오는 모든 요청(/)을 스프링 부트 애플리케이션 (localhost:8080) 으로 전달하도록 구성하는 것입니다.

server {
    listen 80;
    listen [::]:80;
    server_name _; 
    root /usr/share/nginx/html;

    location / {
        proxy_pass http://localhost:8080;
    }
}

나머지 속성들에 대해 잠깐 알아보겠습니다.

  • listen 80 : 클라이언트로부터 들어오는 HTTP 요청을 받는 포트 (IPv4)
  • listen [::]:80 : 클라이언트로부터 들어오는 HTTP 요청을 받는 포트 (Ipv6)
  • server_name : 요청 헤더의 Host와 server_name이 일치하면 server 블럭 실행
    - _ : 모든 Host 헤더를 허용
  • root : 정적 파일을 제공할 때 해당 파일들의 루트 디렉토리
    - /usr/share/nginx/html : 기본적인 정적 파일이 제공되는 디렉토리 위치

설정후 sudo nginx -t 명령어로 문법에 이상이 없는지 확인을 한 다음,

스크린샷 2024-01-29 오후 10 01 58

systemctl restart nginx 명령어로 Nginx를 재시작하여 새로운 설정을 적용합니다.

이후 실제 요청을 보냈더니 올바른 반환 값이 오는 것을 확인할 수 있었습니다.

스크린샷 2024-01-29 오후 10 03 28

애플리케이션 1개에 대한 로드밸런서 설정이 완료되었습니다.

애플리케이션 확장 및 로드밸런싱

프로젝트의 복잡성을 높이기 위해 기존의 단일 애플리케이션 구조에서 벗어나 두 개의 애플리케이션을 실행해 보겠습니다. 우선 docker-compose 파일을 아래와 같이 수정하였습니다.

version: '3.8'

services:
  app1:
    image: ghcr.io/ber01/lo-gak-gye:latest
    container_name: lo-gak-gye-app1
    ports:
      - "8081:8080"

  app2:
    image: ghcr.io/ber01/lo-gak-gye:latest
    container_name: lo-gak-gye-app2
    ports:
      - "8082:8080"

다음 단계로 Nginx 설정 파일을 수정하여 두 애플리케이션에 대한 트래픽을 균등하게 분산 시키는 로드밸런싱 구성을 추가해야 합니다. 이를 위해 upstream 지시어를 사용합니다.

upstream backend {
    server localhost:8081;
    server localhost:8082;
}

server {

    listen 80;
    listen [::]:80;
    server_name _; 
    root /usr/share/nginx/html;

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

설정 변경 후 sudo nginx -t 명령어로 문법에 이상이 없는지 확인을 한 다음, systemctl restart nginx 명령어로 Nginx를 재시작하여 새로운 설정을 적용합니다.

로드밸런서가 트래픽 분산이 잘 이루어지고 있는지 어떻게 확인할 수 있을까요?

저는 이 전(APM 도입 삽질 일기(Spring Boot Actuator + Prometheus + Grafana))에 설정한 APM 환경을 활용하였습니다.

프로메테우스가 15초마다 지표를 수집하고 있는 상황에서 시큐리티 관련 로그 확인하기 위해 활성화 해 둔 디버그 옵션(logging.level.org.springframework.security=DEBUG) 덕분에 로그를 확인할 수 있었는데요. 아래의 스크린샷을 살펴보면 15초마다 찍혀야 할 로그가 30초 단위로 찍히는 것을 볼 수 있었습니다.

스크린샷 2024-01-30 오전 12 53 51

로드밸런서의 트래픽 분산을 확인하였습니다. 하지만 이 구조에는 약간의 문제점이 있습니다.

현재 아키텍처의 문제점과 proxy_set_header

현재 구성된 아키텍처는 아래와 같이 나타낼 수 있습니다. 이 구조의 문제점이 무엇일까요?

바로 애플리케이션은 클라이언트로부터 직접 정보를 받지 못하고, 오직 로드밸런서를 통해 전달된 정보만 인식할 수 있습니다. 이로 인해 애플리케이션은 클라이언트에 대한 직접적인 정보를 알지 못하는 문제가 발생합니다.

저는 이 문제를 OAuth 2.0을 이용한 구글 로그인 테스트를 진행하면서 만나게 되었습니다. 구글 로그인 시도 중 리다이렉션 URL이 올바르지 않아 인증 과정이 실패하고 아래와 같은 오류 메시지가 나타났습니다.

구글 클라우드 플랫폼에서 리다이렉션 URL을 확인했을 때는 문제가 없었음에도 불구하고, 오류가 난게 이상하여 로그를 분석한 결과 redirect_uribackend로 설정된 것을 발견할 수 있었습니다.

이것이 로드밸런서를 거치며 클라이언트의 원래 요청 정보가 아닌 로드밸런서에서 설정한 정보가 애플리케이션으로 전달되어 발생한 문제점입니다. 즉, upstream 지시어에 정의된 값이 redirect_uri로 사용되었던 것입니다. 그림으로 나타내면 아래와 같습니다.

이 문제를 해결하기 위해, Nginx 설정에 proxy_set_header 옵션을 추가하여 클라이언트의 원본 요청 정보를 애플리케이션까지 전달할 수 있도록 조정했습니다.

server {
    listen       80;
    listen       [::]:80;
    server_name  _;
    root         /usr/share/nginx/html;

    location / {
            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
    }
}

각각의 옵션에 대해 알아보겠습니다.

  • Host $host

    • 클라이언트 요청에서 사용된 호스트명을 백엔드 서버로 전달
  • X-Real-IP $remote_addr

    • 클라이언트의 실제 IP 주소를 백엔드 서버에 전달
  • X-Forwarded-For $proxy_add_x_forwarded_for

    • 원본 요청의 IP 주소와 프록시 체인을 통해 요청이 전달된 모든 서버의 IP 주소 포함
  • X-Forwarded-Proto $scheme

    • 클라이언트가 요청을 시작할 때 사용한 프로토콜(예: http 또는 https)을 백엔드 서버로 전달

이제 로드밸런서를 통해 전달되는 클라이언트의 원본 요청 정보를 애플리케이션이 정확히 인식할 수 있게 되어, 리다이렉션 URI 문제를 해결할 수 있었습니다. 또한 로그를 확인해 본 결과, 인증 객체가 올바르게 생성되는 것을 확인할 수 있었습니다.

하지만 문제가 남았습니다. 인증 객체는 app-1 서버에서 생성을 했지만, 이후의 요청을 app-2 서버에 보냈기에 세션이 동기화 되지 않아 인증에 실패했습니다.

이 문제를 해결해야 했습니다.

세션 동기화

세션을 동기화 할 수 있는 방법은 무엇이 있을까요?

제 생각에는 대표적으로 세가지 방법이 있습니다.

  1. 스티키 세션
  2. 세션 클러스터링
  3. 세션 저장소

먼저 스티키 세션은 클라이언트의 첫 번째 요청을 처리한 서버에 후속 요청도 지속적으로 전달하는 방법입니다. 구현이 단순하고 서버간 세션을 동기화 하지 않아도 되지만 트래픽 분산을 할 수 없고 서버가 죽으면 모든 세션 정보가 사라진다는 단점이 있습니다.

두 번째 세션 클러스터링, 여러 서버가 세션을 공유하는 방식입니다. 즉 app-1app-2가 세션을 동기화 하여 같이 들고있는 형태입니다. 트래픽 분산도 쉽고 하나의 서버가 죽어도 가용성이 높다는 단점이 있지만 그만큼 메모리 낭비가 심하고, 동기화 과정에서 부하가 올 수 있습니다.

마지막 세션 저장소는 Redis, 데이터베이스 등의 외부 세션 저장소를 이용하는 방법입니다. 별도의 저장소가 있기 때문에 메모리 낭비가 없고 서버 증설에 따른 트래픽 분산이 쉽습니다. 다만 저장소 자체에 병목 현상이 올 수 있고, 외부 저장소 자체가 죽어버릴 수 있습니다.

저는 이번 프로젝트에서는 데이터베이스를 이용한 세션 저장소를 이용하기로 결정 하였습니다.

먼저 Spring-session 의존성을 추가합니다.

implementation 'org.springframework.session:spring-session-jdbc'

이제 session-jdbc가 데이터베이스에 세션 정보를 저장할 수 있도록 테이블을 생성해 주어야 하는데요.

application.properteis의 속성을 추가하면 됩니다.

spring.session.jdbc.initialize-schema=always

이제, 세션 동기화 까지 완료 됐으므로 실패했던 API를 다시 호출해 보겠습니다.

세션 정보가 서버끼리 공유되면서, 정상적으로 호출되는 것을 확인하였습니다.

마치며

이번 포스팅에서는 스프링 부트 애플리케이션을 두 개 실행하여 분산 환경을 구성하고, 로드 밸런서(Nginx)를 통해 트래픽을 분산하는 과정을 살펴보았습니다. 또한 트래픽 분산에 따른 세션 동기화 문제를 데이터베이스를 이용한 세션 저장소로 해결해 보았습니다.

이번에는 특정 옵션에 대해 파악하고 알아두는 것이 참 중요하다는 것을 알게 되었는데요.

처음에는 proxy_set_header 옵션에 대해 정리만 해둔 뒤, 프로젝트에 필요없다고 생각하여 제외 하였으나 트러블 슈팅 과정에서 이 옵션에 대해 조사한 것이 큰 도움이 되어 오류를 빠르게 파악할 수 있었습니다.

앞으로도 조금 더 찾아보고 조금 더 고민해 봐야겠습니다.

감사합니다!

profile
進取
post-custom-banner

0개의 댓글