Scale Out — HAProxy와 App 수평 확장

조용현·2026년 5월 17일

문제 해결

목록 보기
11/15

배경

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

Scale Up vs Scale Out

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

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

이 글에서 만든 구조 — HAProxy + App 2대, DB는 아직 한 대

이 글에서 만든 구조 — HAProxy + App 2대, DB는 아직 한 대

아키텍처 설계

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분
측정 구간마지막 120초마지막 120초

측정 구간을 마지막 120초로 잡은 이유

10분 전 구간의 평균을 쓰면 안 된다. JVM의 JIT 컴파일이 끝나기 전까지 처리량이 계속 올라가기 때문이다.
특히 App CPU가 상한에 붙어 있으면 C2 컴파일러 스레드가 요청 처리 스레드와 CPU를 두고 경쟁해서
워밍업이 10분 가까이 늘어진다. 실제로 Before 런의 60초 단위 처리량은
160 → 219 → 235 → 253 → 263 → 281 → 306 → 327 → 339/s끝까지 우상향했다.

전 구간 평균을 쓰면 Before가 워밍업까지 포함한 274/s로 잡혀 실제보다 나쁘게 나오고,
개선 폭이 부풀려진다. 그래서 양쪽 모두 워밍업이 끝난 마지막 120초 구간으로 통일해 비교했다.

결과 비교

지표Before (App 1대)After (App 2대)변화
TPS350/s547/s+56.3%
Avg 응답시간543ms42ms-92.3%
P95 응답시간2,691ms131ms-95.1%
P99 응답시간4,182ms316ms-92.4%
Max 응답시간8,075ms1,338ms-83.4%
구간 요청 수 (120초)41,96965,581+56.3%
에러율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 포화로 요청이 큐에 대기하며 응답시간이 급증한다. 그래프는 10분 전 구간이라 워밍업이 포함되어 있다 — 위 표의 수치는 그래프 오른쪽 끝(마지막 120초) 기준이다.

Response Time After

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

분석

TPS가 1.5배 이상 오른 이유

App을 2대로 늘려 TPS가 350 → 547/s로 약 1.56배 증가했다.

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

Before에서 App CPU가 포화 상태였기 때문에, App을 추가하자 처리량이 크게 증가했다. 이론상 2배 대비 78% 달성이다.
선형(2배)에 못 미치는 이유는 병목이 완전히 사라진 게 아니라 MySQL로 옮겨갔기 때문이다(아래 참고).

한 가지 덧붙이면, Before는 마지막 120초 구간에서도 처리량이 아직 완만하게 오르는 중이었다.
즉 Before의 진짜 정상상태 처리량은 350/s보다 높을 수 있고, 그렇다면 실제 개선 폭은 +56%보다 작아진다.
여기서의 +56%는 개선 폭의 상한으로 읽는 것이 정확하다.

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

  • Before: App CPU 200% 포화 → 요청이 큐에 대기 → p99 4.2초, 평균 543ms
  • After: App CPU 여유 → 대기 없이 즉시 처리 → p99 316ms, 평균 42ms

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 응답시간병목
댓글 트리 최적화 후350/s543msApp CPU
Scale Out 후547/s42msMySQL CPU

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

profile
백엔드 개발자

2개의 댓글

comment-user-thumbnail
2026년 8월 19일

App을 1대에서 2대로 늘렸는데 TPS가 2배가 아니라 약 1.56배만 증가했습니다. 왜 선형적으로 증가하지 않았다고 보시나요?

답글 달기
comment-user-thumbnail
2026년 8월 19일

Sticky Session으로 Redis 없이 세션을 유지하도록 설계하셨는데, 운영 환경에서 특정 App 인스턴스가 장애 나면 어떤 문제가 생기고 어떻게 개선하시겠습니까?

답글 달기