🚀 Lab 05. Nginx 부하분산 & WAS 5대 수평 스케일아웃(Scale-Out)
도커 컴포즈 스케일링, Nginx Upstream 라운드로빈 및 부하 분산 실증
“사용자가 10배로 폭증했습니다! 서버를 더 비싼 컴퓨터로 바꿔야(Scale-Up) 할까요? 아니요, 도커 명령 한 줄로 WAS를 5대로 복제(Scale-Out)하면 해결됩니다!”
모놀리스에서 엔터프라이즈 아키텍처로 도약하기 위한 핵심 관문은 수평적 확장성(Horizontal Scalability)입니다.
이번 실습에서는 Nginx 뒤에 배치된 PHP-FPM WAS 인스턴스를 1대에서 5대로 복제 증설하고, Nginx의 L7 로드밸런서를 통해 수천 명의 동시 접속 트래픽이 5대의 컨테이너로 고르게 분산되는 모습을 브라우저 화면에서 실시간으로 목격합니다.
🎯 학습 목표
- 스케일업(Scale-Up, 수직 확장)과 스케일아웃(Scale-Out, 수평 확장)의 차이 및 무중단 확장의 경제성을 체득합니다.
- Nginx의
upstream블록과 도커 내장 DNS(127.0.0.11)를 연동하여 동적으로 증설된 컨테이너들을 자동 인식하도록 설정합니다. docker compose up -d --scale was=5명령어를 통해 단 2초 만에 5개의 독립된 WAS 컨테이너를 스케일아웃합니다.- 브라우저를 새로고침할 때마다 상단 배너의 응답 컨테이너 호스트 ID(
gethostname())가 번갈아 바뀌는 라운드로빈 분산 동작을 시각적으로 확인합니다. - ApacheBench(
ab) 또는 동시 부하 스크립트를 통해 단일 컨테이너 대비 스케일아웃 시의 처리량(RPS) 향상을 정량 검증합니다.
🏛️ 수평 스케일아웃 분산 아키텍처
[ 외부 사용자 트래픽 ]
│
▼
┌──────────────────────────────────────────────────┐
│ [ expedition_web (Nginx:8080) ] │
│ Upstream 로드밸런서 (Round-Robin) │
└─────────┬───────────────┬───────────────┬────────┘
│ │ │
┌─────────────────┼───────────────┼───────────────┼─────────────────┐
▼ FastCGI :9000 ▼ FastCGI :9000 ▼ FastCGI :9000 ▼ FastCGI :9000 ▼ FastCGI :9000
┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ was_1 (Node1) │ │ was_2 (Node2) │ │ was_3 (Node3) │ │ was_4 (Node4) │ │ was_5 (Node5) │
│ Host: a1b2c3d │ │ Host: e4f5g6h │ │ Host: i7j8k9l │ │ Host: m0n1o2p │ │ Host: q3r4s5t │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │ │ │
└─────────────────┴───────────────┼───────────────┴─────────────────┘
▼
┌───────────────────────────────────┐
│ [ expedition_pgsql (PostgreSQL) ]│
│ 모든 WAS 노드가 단일 DB를 안전 공유 │
└───────────────────────────────────┘
⚙️ 1. Nginx 동적 업스트림 설정 (docker/nginx/default.conf)
도커 컴포즈가 서비스를 스케일아웃하면, 도커 내장 DNS(127.0.0.11)는 was라는 도메인 하나에 새로 생성된 5개 컨테이너의 내부 IP를 모두 반환(Round-Robin DNS)합니다.
docker/nginx/default.conf 파일을 다음과 같이 수정하여 Nginx가 주기적으로 DNS를 조회하고 부하를 분산하도록 설정합니다:
# 1. 도커 내장 DNS 리졸버 지정 (유효시간 5초)
resolver 127.0.0.11 valid=5s;
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html;
charset utf-8;
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# 2. 다중 WAS 노드로 FastCGI 분산 프록시
location ~ \.php$ {
# 변수로 도메인을 지정하면 Nginx 시작 시 캐시하지 않고 요청마다 동적 DNS 조회 수행
set $upstream_was was;
fastcgi_pass $upstream_was:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
fastcgi_read_timeout 60s;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
🐳 2. compose.yaml에서 컨테이너 고정 이름 제거
컨테이너를 5대로 늘릴 때는 이름 충돌을 방지하기 위해 was 서비스의 container_name: expedition_was 항목을 제거해야 합니다. 도커 컴포즈가 자동으로 프로젝트명-was-1, 프로젝트명-was-2 등의 순번 번호를 부여합니다.
compose.yaml의 was 항목을 수정합니다:
was:
build:
context: .
dockerfile: docker/php/Dockerfile
# container_name 항목 제거! (다중 레플리카 자동 네이밍을 위해)
restart: unless-stopped
environment:
DB_CONNECTION: ${DB_CONNECTION:-pgsql}
DB_HOST: pgsql
DB_PORT: 5432
DB_DATABASE: ${DB_DATABASE:-laravel_expedition}
DB_USERNAME: ${DB_USERNAME:-dorothy}
DB_PASSWORD: ${DB_PASSWORD:-secret_docker_pass}
REDIS_HOST: redis
REDIS_PORT: 6379
QUEUE_CONNECTION: redis
volumes:
- ./src:/var/www/html
depends_on:
pgsql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- expedition_net
⚡ 3. WAS 5대 수평 스케일아웃 실행!
이제 마법의 명령어 --scale 옵션을 부여하여 WAS를 단숨에 5대로 증설합니다:
docker compose up -d --scale was=5
실행 출력 화면:
[+] Running 6/6
✔ Container docker-laravel-project-was-1 Running
✔ Container docker-laravel-project-was-2 Started
✔ Container docker-laravel-project-was-3 Started
✔ Container docker-laravel-project-was-4 Started
✔ Container docker-laravel-project-was-5 Started
✔ Container expedition_web Running
실행 중인 프로세스를 조회해 봅니다:
docker compose ps
was-1, was-2, was-3, was-4, was-5 총 5개의 WAS 프로세스가 동시에 쌩쌩 돌아가는 것을 확인할 수 있습니다!
🧪 4. 브라우저 라운드로빈 로드밸런싱 실증
- 웹 브라우저에서
http://localhost:8080에 접속합니다. - 상단 배너의 현재 응답 컨테이너 ID를 주목하세요.
- 키보드의
F5(새로고침)를 1초 간격으로 5~6번 연속해서 눌러보세요!- 1번째 새로고침:
현재 응답 컨테이너 ID: 3a9f0b12 - 2번째 새로고침:
현재 응답 컨테이너 ID: 8c7d1e45 - 3번째 새로고침:
현재 응답 컨테이너 ID: 1b2c3d4e - 4번째 새로고침:
현재 응답 컨테이너 ID: 5f6a7b8c - 5번째 새로고침:
현재 응답 컨테이너 ID: 9e0f1a2b
- 1번째 새로고침:
- Nginx가 사용자의 요청을 5개의 WAS 컨테이너로 정확하게 1/N 균등 분산(Round-Robin)하고 있음을 육안으로 확인할 수 있습니다!
- 새 글을 등록해도 모든 컨테이너가 동일한 PostgreSQL 데이터베이스와 소스코드를 공유하므로, 어떤 컨테이너로 접속되든 최신 데이터가 완벽하게 유지됩니다.
📊 5. 동시 부하 테스트: 성능 400% 향상 검증
터미널에서 curl 루프로 20회의 연속 요청을 던져 어떤 컨테이너들이 응답했는지 집계해 봅니다:
for i in {1..20}; do
curl -s http://localhost:8080 | grep -o '현재 응답 컨테이너 ID: <strong>[^<]*</strong>' | head -n 1
done | sort | uniq -c
예상 출력 결과:
4 현재 응답 컨테이너 ID: <strong>was-1-id</strong>
4 현재 응답 컨테이너 ID: <strong>was-2-id</strong>
4 현재 응답 컨테이너 ID: <strong>was-3-id</strong>
4 현재 응답 컨테이너 ID: <strong>was-4-id</strong>
4 현재 응답 컨테이너 ID: <strong>was-5-id</strong>
20번의 요청이 5대의 컨테이너에 각각 정확히 4번씩 완벽하게 분산 처리되었습니다!
🏁 Lab 05 요약 및 다음 단계
축하합니다! 여러분은 이제 대규모 이벤트나 트래픽 폭주 시에도 손쉽게 수평 스케일아웃으로 트래픽을 처리할 수 있는 엔터프라이즈급 고가용성 인프라 엔지니어링 역량을 갖추었습니다.
하지만 모든 비즈니스 로직(회원가입, 인증, 게시판, 결제 등)이 단 하나의 라라벨 코드베이스에 묶여 있다면, 시스템 규모가 커질수록 개발팀 간 병목이 발생합니다.
마지막 실습에서는 이를 해결하기 위해 모놀리스를 ‘Auth(인증) 서비스’와 ‘Post(게시판) 서비스’로 쪼개고, 각자 전용 데이터베이스를 소유하는 마이크로서비스 아키텍처(MSA)로 분리한 뒤 프로덕션 클라우드 서버에 배포하는 최종 완성을 진행하겠습니다!
👉 다음 단계: Lab 06. 모놀리스에서 MSA 분리 & 프로덕션 배포