댓글 트리 최적화로 응답시간은 28% 개선되었지만, App CPU는 여전히 200%(2코어 상한)로 포화 상태였다.
코드 최적화만으로는 한계가 있으므로, App 인스턴스를 추가하는 수평 확장(Scale Out)을 도입했다.
| 전략 | 장점 | 단점 |
|---|---|---|
| Scale Up (CPU 증가) | 구성 변경 없음, 즉시 효과 | 단일 장애점, 비용 급증, 한계 존재 |
| Scale Out (인스턴스 추가) | 선형 확장, HA 확보 | LB 필요, 세션/캐시 공유 문제 |
이미 App CPU가 포화된 상태에서는 Scale Out이 효과적이다.
Client → App (2cpu/2GB) → MySQL (1cpu/1GB)
Client → HAProxy (LB) ─┬─ App-1 (2cpu/2GB) ─┬─ MySQL (1cpu/1GB)
└─ App-2 (2cpu/2GB) ─┘
로드밸런서 선택 시 핵심 요구사항은 Sticky Session이었다.
같은 사용자의 요청이 항상 같은 인스턴스로 가야 세션이 유지된다.
| LB | Sticky Session (오픈소스) | 비고 |
|---|---|---|
| Nginx (ip_hash) | 같은 IP만 가능 | JMeter 500 스레드가 전부 같은 IP → 한쪽 쏠림 |
| Nginx (hash $cookie) | 첫 요청 시 쿠키 없음 → 쏠림 | 첫 요청 분산 불가 |
| HAProxy (cookie insert) | 첫 요청부터 균등 분산 | 추가 모듈 불필요 |
HAProxy의 cookie SERVERID insert 방식:
SERVERID=app1 쿠키 자동 삽입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
Scale Out 시 일반적으로 Redis가 필요하다고 알려져 있다 (세션 공유, 캐시 공유).
하지만 Sticky Session을 사용하면:
| 구성요소 | Redis 필요? | 이유 |
|---|---|---|
| 세션 | 불필요 | 같은 사용자 → 항상 같은 인스턴스 → 세션 유지 |
| Caffeine 캐시 | 불필요 | 각 인스턴스별 독립 캐시 (히트율은 절반) |
| WebSocket | 불필요 | 부하테스트에 미포함 |
Sticky Session의 한계: 인스턴스 장애 시 해당 인스턴스에 붙어있던 사용자의 세션이 유실된다.
HA(고가용성)가 필요하면 Redis Session 도입이 필요하다.
| 구성요소 | Before | After |
|---|---|---|
| App | 1대 (2cpu / 2GB) | 2대 (각 2cpu / 2GB) |
| HAProxy | 없음 | 0.5cpu / 128MB |
| MySQL | 1대 (1cpu / 1GB) | 1대 (1cpu / 1GB) |
| DB 데이터 | 댓글 505만, 좋아요 735만 | 동일 |
| vusers | 500 | 500 |
| 테스트 시간 | 10분 | 10분 |
| 지표 | Before (App 1대) | After (App 2대) | 변화 |
|---|---|---|---|
| TPS | 274/s | 527/s | +92.3% |
| Avg 응답시간 | 867ms | 32ms | -96.3% |
| P95 응답시간 | 4,799ms | 93ms | -98.1% |
| P99 응답시간 | 7,511ms | 201ms | -97.3% |
| Max 응답시간 | 11,998ms | 1,338ms | -88.8% |
| 총 요청 수 | 164,601 | 316,041 | +92.0% |
| 에러율 | 0% | 0% | 동일 |

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

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

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

After (App 2대): p99가 100~350ms, p95가 50~100ms로 대폭 개선되었다. app-1, app-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 효율이 매우 높다.
App CPU 포화가 해소되면서 큐잉 지연이 사라진 것이 핵심이다.
Scale Out 후 병목이 App에서 MySQL로 이동했다. MySQL이 1cpu 100%에 도달하여, App을 더 추가해도 TPS 향상은 제한적이다.
MySQL에 병목이 확인되었으므로, Read Replica를 추가하면 추가 TPS 향상이 가능하다.
HAProxy → App-1 + App-2 → Master (Write) + Replica (Read)
| 단계 | TPS | Avg 응답시간 | 병목 |
|---|---|---|---|
| 댓글 트리 최적화 후 | 274/s | 867ms | App CPU |
| Scale Out 후 | 527/s | 32ms | MySQL CPU |
Scale Out으로 App CPU 병목을 해소하고, TPS를 92% 향상, 응답시간을 96% 감소시켰다.
병목이 DB로 이동했으므로, 다음 단계로 Read Replica 추가를 통한 DB 부하 분산을 진행할 수 있다.