Scale Out — HAProxy와 App 수평 확장

조용현·2026년 5월 17일

문제 해결

목록 보기
11/13

배경

댓글 트리 최적화로 응답시간은 28% 개선되었지만, App CPU는 여전히 200%(2코어 상한)로 포화 상태였다.
코드 최적화만으로는 한계가 있으므로, App 인스턴스를 추가하는 수평 확장(Scale Out)을 도입했다.

Scale Up vs Scale Out

전략장점단점
Scale Up (CPU 증가)구성 변경 없음, 즉시 효과단일 장애점, 비용 급증, 한계 존재
Scale Out (인스턴스 추가)선형 확장, HA 확보LB 필요, 세션/캐시 공유 문제

이미 App CPU가 포화된 상태에서는 Scale Out이 효과적이다.

아키텍처 설계

Before (단일 인스턴스)

Client → App (2cpu/2GB) → MySQL (1cpu/1GB)

After (Scale Out)

Client → HAProxy (LB) ─┬─ App-1 (2cpu/2GB) ─┬─ MySQL (1cpu/1GB)
                        └─ App-2 (2cpu/2GB) ─┘

왜 HAProxy인가?

로드밸런서 선택 시 핵심 요구사항은 Sticky Session이었다.
같은 사용자의 요청이 항상 같은 인스턴스로 가야 세션이 유지된다.

LBSticky Session (오픈소스)비고
Nginx (ip_hash)같은 IP만 가능JMeter 500 스레드가 전부 같은 IP → 한쪽 쏠림
Nginx (hash $cookie)첫 요청 시 쿠키 없음 → 쏠림첫 요청 분산 불가
HAProxy (cookie insert)첫 요청부터 균등 분산추가 모듈 불필요

HAProxy의 cookie SERVERID insert 방식:

  1. 첫 요청: 쿠키 없음 → round-robin 분산 → 응답에 SERVERID=app1 쿠키 자동 삽입
  2. 이후 요청: 쿠키 기반으로 동일 인스턴스 고정

HAProxy 설정

frontend http_front
    bind *:9091
    default_backend app_servers

backend app_servers
    balance roundrobin
    cookie SERVERID insert indirect nocache
    server app1 popping-app-1:9091 check cookie app1
    server app2 popping-app-2:9091 check cookie app2

Redis 없이 가능한 이유

Scale Out 시 일반적으로 Redis가 필요하다고 알려져 있다 (세션 공유, 캐시 공유).
하지만 Sticky Session을 사용하면:

구성요소Redis 필요?이유
세션불필요같은 사용자 → 항상 같은 인스턴스 → 세션 유지
Caffeine 캐시불필요각 인스턴스별 독립 캐시 (히트율은 절반)
WebSocket불필요부하테스트에 미포함

Sticky Session의 한계: 인스턴스 장애 시 해당 인스턴스에 붙어있던 사용자의 세션이 유실된다.
HA(고가용성)가 필요하면 Redis Session 도입이 필요하다.

부하테스트 결과

테스트 환경

구성요소BeforeAfter
App1대 (2cpu / 2GB)2대 (각 2cpu / 2GB)
HAProxy없음0.5cpu / 128MB
MySQL1대 (1cpu / 1GB)1대 (1cpu / 1GB)
DB 데이터댓글 505만, 좋아요 735만동일
vusers500500
테스트 시간10분10분

결과 비교

지표Before (App 1대)After (App 2대)변화
TPS274/s527/s+92.3%
Avg 응답시간867ms32ms-96.3%
P95 응답시간4,799ms93ms-98.1%
P99 응답시간7,511ms201ms-97.3%
Max 응답시간11,998ms1,338ms-88.8%
총 요청 수164,601316,041+92.0%
에러율0%0%동일

모니터링 메트릭

Container CPU Usage

Container CPU Before

Before (App 1대): popping-app이 약 200%로 CPU 상한(2코어)에 도달해 있다. MySQL은 50~60% 수준.

Container CPU After

After (App 2대): popping-app-1, app-2가 각각 120~150%로 CPU 여유가 생겼다. 병목이 App에서 MySQL로 이동하여 MySQL이 100%에 도달한 것이 확인된다.

Response Time (p50 / p95 / p99)

Response Time Before

Before (App 1대): p99 약 10초, p95 5초대. App CPU 포화로 요청이 큐에 대기하며 응답시간이 급증한다.

Response Time After

After (App 2대): p99가 100~350ms, p95가 50~100ms로 대폭 개선되었다. app-1, app-2가 균등하게 부하를 분담하고 있다.

분석

TPS가 거의 2배 달성된 이유

App을 2대로 늘려 TPS가 274 → 527/s로 약 1.92배 증가했다.

Before: App (200% CPU 포화) → MySQL (50~60%)
After:  App-1 + App-2 (각 120~150%) → MySQL (100% CPU 포화)

Before에서 App CPU가 포화 상태였기 때문에, App을 추가하자 처리량이 선형에 가깝게 증가했다. 이론상 2배 대비 96% 달성으로, Scale Out 효율이 매우 높다.

응답시간이 96% 감소한 이유

  • Before: App CPU 200% 포화 → 요청이 큐에 대기 → p99 10초, 평균 867ms
  • After: App CPU 여유 → 대기 없이 즉시 처리 → p99 201ms, 평균 32ms

App CPU 포화가 해소되면서 큐잉 지연이 사라진 것이 핵심이다.

병목 이동

Scale Out 후 병목이 App에서 MySQL로 이동했다. MySQL이 1cpu 100%에 도달하여, App을 더 추가해도 TPS 향상은 제한적이다.

다음 단계

MySQL에 병목이 확인되었으므로, Read Replica를 추가하면 추가 TPS 향상이 가능하다.

HAProxy → App-1 + App-2 → Master (Write) + Replica (Read)

정리

단계TPSAvg 응답시간병목
댓글 트리 최적화 후274/s867msApp CPU
Scale Out 후527/s32msMySQL CPU

Scale Out으로 App CPU 병목을 해소하고, TPS를 92% 향상, 응답시간을 96% 감소시켰다.
병목이 DB로 이동했으므로, 다음 단계로 Read Replica 추가를 통한 DB 부하 분산을 진행할 수 있다.

profile
백엔드 개발자

0개의 댓글