(0) MAU 1500만 트래픽 실험을 시작하며 — 왜, 어떤 조건에서
(0) MAU 1500만 트래픽 실험을 시작하며 — 왜, 어떤 조건에서
- 이 시리즈는 MAU 1500만 규모의 서비스가 받는 부하를 로컬 환경에서 재현하고, 그 아래에서 시스템이 어디서 어떻게 무너지는지를 직접 관찰한 기록이다.
- 포스트 하나가 가설 하나를 다룬다. 각 포스트는 가설 → 측정 → 원인 → 조치 → 전후 비교 순서로 쓴다.
- 이 글은 0번으로, 실험의 이유와 전제 조건, 용량 산정을 먼저 고정한다. 이후 포스트는 모두 이 글의 숫자를 기준으로 삼는다.
❐ 1. 왜 이 실험을 하는가
경력에 없는 경험
- 지금까지 네 회사를 거치며 PHP→Java 전환, 외부기관 연동, 데이터 정합성 복구 같은 일을 해왔다.
- 하지만 대규모 트래픽을 받는 운영 환경에서 일해본 적은 없다. 수천 RPS 아래에서 커넥션 풀이 마르고, 캐시가 동시에 만료되고, p99가 튀는 상황을 직접 본 적이 없다.
- 이 경험은 책으로 읽는 것과 직접 보는 것의 차이가 크다. 그래서 만들어서 보기로 했다.
개인적인 궁금증
- 특정 회사에 가기 위한 준비라기보다, “1500만이면 도대체 어느 정도인가”라는 궁금증이 출발점이다.
- 숫자를 정의하고, 그 숫자가 시스템의 어느 부분을 먼저 부수는지 확인하고 싶다.
이 실험이 대체하지 못하는 것
- 이 실험은 운영 경험을 대체하지 않는다. 새벽의 온콜, 롤백 판단, 여러 팀이 얽힌 장애 커뮤니케이션, 몇 달치 데이터가 쌓인 DB의 마이그레이션은 재현할 수 없다.
- 로컬에서 측정한 절대 수치는 신뢰할 수 없다. 이 시리즈에서 의미 있는 숫자는 조치 전후의 상대 비교뿐이다.
- 따라서 이 기록은 “MAU 1500만 서비스를 만들었다”가 아니라 “MAU 1500만을 가정했을 때 발생하는 병목을 재현하고 해결한 기록”이다.
❐ 2. 1500만은 어느 정도인가
단위에 따라 완전히 다른 숫자
- “1500만 트래픽”은 단위를 정하지 않으면 의미가 없다. 같은 숫자라도 단위에 따라 난이도가 극과 극이다.
| 해석 | 환산 | 국내 기준 위치 |
|---|---|---|
| 하루 1500만 요청 | 평균 약 174 RPS, 피크 1~2천 RPS | 하. 서버 두세 대로 감당 |
| MAU 1500만 | 피크 수만 RPS, 동시 접속 수만 | 상. 쿠팡, 배민, 당근, 토스, 멜론 급 |
| DAU 1500만 | 피크 수십만 RPS | 최상. 카카오톡, 네이버, 유튜브 |
| 동시 접속 1500만 | 소켓 수만으로 OS 한계 초과 | 국내에서는 사실상 없음 |
- 이 시리즈의 기준은 MAU 1500만이다. 국내 인구의 약 30%가 한 달에 한 번은 쓰는 서비스이며, DB 샤딩과 다중 캐시 계층, 전담 SRE 팀이 있는 규모다.
❐ 3. 용량 산정
- 용량 산정은 가정을 명시하고 곱셈으로 내려가는 것이 전부다. 가정값은 서비스 성격에 따라 바뀌므로, 각 값을 왜 골랐는지 함께 적는다.
- 도메인은 콘텐츠 소비형 B2C 앱(곡 목록, 곡 상세, 인기 차트, 재생 기록, 좋아요)으로 잡았다. 도메인이 바뀌면 이 표의 가정값만 갈아끼우면 된다.
3-1. MAU → DAU
| 항목 | 값 | 근거 |
|---|---|---|
| MAU | 15,000,000 | 목표 |
| DAU/MAU 비율 | 25% | 콘텐츠 앱 20~30%, 메신저 50% 이상, 커머스 10~15% |
| DAU | 3,750,000 |
3-2. DAU → 하루 요청 수
| 항목 | 값 | 근거 |
|---|---|---|
| 하루 세션 수 / 사용자 | 3회 | 출근길, 점심, 저녁 |
| API 호출 수 / 세션 | 20회 | 홈 피드, 상세, 검색, 재생 로그. 정적 파일은 CDN이라 제외 |
| 하루 API 요청 수 | 225,000,000 | 3.75M × 3 × 20 |
| 평균 RPS | 약 2,600 | 225M ÷ 86,400초 |
3-3. 평균 → 피크
서버는 평균이 아니라 피크에 맞춰 설계한다
| 항목 | 값 | 근거 |
|---|---|---|
| 일간 피크 배수 | 3배 | 저녁 9~11시에 하루 트래픽의 30%가 몰림 |
| 피크 RPS | 약 8,000 | |
| 이벤트 스파이크 배수 | 2배 | 푸시 발송, 신곡 발매, 마케팅 |
| 설계 목표 RPS | 약 16,000 | 이 시리즈의 목표치 |
3-4. 요청을 자원별로 분해
| 항목 | 값 | 근거 |
|---|---|---|
| 읽기:쓰기 비율 | 9:1 | 콘텐츠 앱 표준. 커머스 결제 시간대는 7:3까지 |
| 피크 쓰기 RPS | 1,600 | DB 프라이머리 한 대의 상한 근처 |
| API당 DB 쿼리 수 | 3개 | N+1이 없다는 가정. 실험에서 이 가정을 깨뜨려 볼 것 |
| 캐시 히트율 | 80% | Redis 앞단 가정 |
| 피크 DB QPS | 약 9,600 | 16,000 × 3 × 0.2 |
| 피크 Redis QPS | 약 38,000 | 16,000 × 3 × 0.8 |
| 응답 평균 크기 | 5KB | JSON 목록 응답 기준 |
| 피크 대역폭 | 약 640Mbps | 16,000 × 5KB × 8bit |
3-5. 인스턴스 수 산정
| 항목 | 값 | 근거 |
|---|---|---|
| Spring Boot MVC 인스턴스당 처리량 | 1,000~1,500 RPS | DB I/O가 있는 실제 API 기준. 톰캣 스레드 200개, 평균 응답 100ms면 이론상 2,000 |
| 필요 앱 인스턴스 | 12~16대 | 피크 16,000 기준 |
| MySQL | 프라이머리 1 + 리드 레플리카 2~3 | 프라이머리 QPS 상한을 5,000~10,000으로 보면 읽기 분산 필수 |
| Redis | 1~2 노드 | 38K QPS는 단일 노드로 가능. 문제는 핫 키 |
3-6. 동시 접속과 데이터 증가
| 항목 | 값 | 근거 |
|---|---|---|
| 피크 시간대 활성 사용자 | 약 500,000 | DAU의 약 13%가 피크 1시간에 활동 |
| 실제 동시 접속 (열린 세션) | 40,000~60,000 | 평균 세션 5분 기준 |
| 하루 이벤트 로그 | 225M 행, 약 10~20GB | 행당 50~100바이트. 한 달이면 500GB, 파티셔닝 필요 |
이 표에서 나오는 결론
- 실험 목표는 피크 16,000 RPS, 그중 쓰기 1,600 RPS다. 병목은 앱이 아니라 DB에서 먼저 온다.
- 첫 병목은 DB 프라이머리의 쓰기와 캐시 히트율이다. 히트율이 80%에서 50%로 떨어지면 DB QPS가 2.5배가 되어 바로 넘어간다. 캐시 스탬피드가 첫 실험이어야 하는 이유다.
- 대역폭 640Mbps는 집 와이파이로 안 된다. 머신 간 유선 연결이 필수다.
❐ 4. 실험 환경
하드웨어
- 개인 맥북 두 대를 쓴다. 회사 맥북(M4)은 방화벽과 보안 에이전트가 측정에 섞이고 회사 자산 정책 문제도 있어 제외했다. 클라우드는 절대 규모가 필요해질 때 마지막에 쓴다.
| 머신 | 역할 | 이유 |
|---|---|---|
| MacBook Pro M1 Pro (16GB, 성능 8 + 효율 2코어) | 앱 서버 + MySQL + Redis + 관측 | 두 대 중 빠른 쪽. 1단계 자원 배치 8GB를 감당 |
| MacBook Air M1 (8GB) | 부하 생성기 (k6) | k6는 CPU만 필요. 1만 RPS 안팎 생성 가능. 서버 상한보다 크므로 생성기가 병목이 되지 않음 |
- M1 Pro 한 대에 앱 3대와 DB, Redis가 같이 있으면 현실적 상한은 5,000~8,000 RPS로 예상한다. 설계 목표 16,000 RPS에 미치지 못한다.
- 그래도 괜찮은 이유: 가설 9개 중 8개는 절대 RPS가 아니라 상대적 과부하 조건에서 재현된다. 풀 크기를 줄이고 슬로우 쿼리를 넣으면 500 RPS에서도 터진다.
- 16,000 RPS 도달과 수평 확장 실험은 4단계 클라우드로 보낸다. 로컬에서는 M1 Pro의 포화 지점을 기록하는 것이 가설 1의 결과다.
단계별 확장
- 1단계: M1 Pro 한 대에 전부 띄운다. 부하 생성기가 같은 머신에 있어 3,000~5,000 RPS쯤에서 수치가 흔들리기 시작할 것이다. 그 지점을 기록하는 것 자체가 첫 관찰이다.
- 2단계: 부하 생성기를 Air로 분리한다. 측정 왜곡의 대부분이 여기서 사라진다. 여기서 M1 Pro의 포화 지점을 확정한다.
- 3단계: 두 머신을 Thunderbolt 브리지(10Gbps)로 직결한다. 와이파이 대역폭이 측정에 섞이는 것을 막는다.
- 4단계: 클라우드 스팟 인스턴스. 앱을 2대→4대→8대로 늘리며 처리량이 선형으로 늘어나는지, 안 늘어난다면 무엇이 막는지를 본다. 16,000 RPS 도달은 여기서 노린다.
소프트웨어
| 구성 요소 | 선택 | 이유 |
|---|---|---|
| 앱 | Java 21, Spring Boot MVC | 주력 스택. 실험 대상이 도구가 아니라 시스템이어야 하므로 익숙한 것을 쓴다 |
| DB | MySQL 8 | 주력 스택 |
| 캐시 | Redis | |
| 부하 생성 | k6 | curl은 요청마다 프로세스를 띄워 수백 RPS에서 막힌다. k6는 단일 머신에서 수만 RPS |
| 관측 | Prometheus + Grafana, Micrometer | p50/p95/p99, 커넥션 풀, GC, 캐시 히트율을 한 화면에 |
| 컨테이너 | Docker Compose (OrbStack) | 16GB 머신이면 Docker Desktop보다 메모리 여유가 있음 |
1단계 자원 배치 (M1 Pro 한 대)
| 구성 요소 | 개수 / 크기 | 메모리 |
|---|---|---|
| Spring Boot 앱 | 3 인스턴스, 힙 512MB | 약 2.5GB |
| MySQL 8 | buffer pool 2GB | 약 3GB |
| Redis | 1대 | 약 500MB |
| Prometheus + Grafana | 각 1대 | 약 1GB |
| k6 | VU 500 | 약 1GB |
| 합계 | 약 8GB. 16GB 중 OS와 여유분을 빼면 빠듯하지만 가능 |
❐ 5. 실험 대상 서비스
API 다섯 개
- 도메인은 음원 서비스다. 읽기 4개, 쓰기 2개로 9:1 비율에 가깝게 트래픽 믹스를 조정한다.
| API | 종류 | 트래픽 비중 | 실험에서의 역할 |
|---|---|---|---|
| 곡 목록 조회 | 읽기 | 30% | 페이징, N+1 |
| 곡 상세 조회 | 읽기 | 25% | 캐시 히트율, 핫 키 |
| 인기 차트 조회 | 읽기 | 30% | 캐시 스탬피드 (전 사용자가 같은 키를 봄) |
| 재생 기록 저장 | 쓰기 | 10% | 쓰기 처리량, 큐 백프레셔 |
| 좋아요 | 쓰기 | 5% | 핫 로우 락 경합 |
❐ 6. 검증할 가설 목록
- 포스트 하나가 가설 하나를 다룬다. 각 포스트의 형식은 가설 → 측정 → 원인 → 조치 → 전후 비교로 고정한다.
- 순서는 용량 산정에서 병목이 예상되는 순서다. 실험 중에 새 가설이 생기면 목록에 추가한다.
| # | 가설 | 재현 방법 | 기대 관찰 |
|---|---|---|---|
| 1 | 베이스라인: 단일 머신에서 부하 생성기와 서버가 CPU를 나눠 쓰면 몇 RPS부터 수치가 왜곡되는가 | VU 200 → 1,000 단계 상승 | 특정 RPS 이후 p99가 서버가 아닌 생성기 때문에 튐 |
| 2 | 캐시 스탬피드: 인기 차트 키 TTL이 만료되는 순간 DB로 요청이 몰려 DB가 먼저 넘어간다 | TTL 60초, 차트 API에 4,800 RPS | TTL 만료 시점마다 DB QPS 스파이크와 p99 급등 |
| 3 | 커넥션 풀 고갈: 느린 쿼리 하나가 풀을 점유하면 무관한 API까지 함께 멈춘다 | HikariCP 풀 10, 곡 상세에 300ms 쿼리 주입 | 곡 목록 API까지 타임아웃 |
| 4 | N+1: API당 쿼리 3개 가정이 깨지면 DB QPS가 목표의 몇 배가 되는가 | 곡 목록에 아티스트 지연 로딩 | DB QPS가 페이지 크기만큼 배수로 증가 |
| 5 | 핫 로우: 특정 곡 하나에 좋아요가 몰리면 락 대기가 전체 쓰기 처리량을 깎는다 | 좋아요 트래픽의 50%를 곡 1개에 집중 | 락 대기 시간 증가, 쓰기 RPS 상한 하락 |
| 6 | 쓰기 병목: 재생 기록 1,600 RPS를 동기 INSERT로 받으면 프라이머리가 버티는가 | 재생 기록 API 단독 부하 | 커밋 지연, 이후 큐로 분리 |
| 7 | GC와 p99: 평균은 멀쩡한데 p99만 튀는 원인은 GC 정지인가 | 힙 512MB, 대용량 응답 | GC 로그와 p99 스파이크 시각 일치 |
| 8 | 인스턴스 장애: 3대 중 1대를 죽였을 때 에러율과 복구 시간은 얼마인가 | 부하 중 컨테이너 강제 종료 | 에러율 스파이크 폭과 지속 시간 |
| 9 | 무중단 배포: 롤링 배포 중에 5xx가 나는가 | 부하 중 순차 재시작 | graceful shutdown 유무에 따른 차이 |
| 10 | 수평 확장: 앱 인스턴스를 2배로 늘리면 처리량도 2배가 되는가 | 4단계 클라우드에서 2→4→8대 | 어느 시점부터 DB가 막아 선형 증가가 깨짐 |
- 로드밸런서는 용량을 만들지 않고 나누기만 한다. 한 머신 안에서 nginx로 앱 3대에 분배해도 CPU는 같은 8개다. 지금 구성의 nginx는 처리량이 아니라 가설 8·9(장애 재분배, 무중단 배포)를 위한 것이다.
- 수평 확장의 진짜 질문은 “머신을 늘렸는데 왜 처리량이 그만큼 안 느는가”이고, 이건 서버 머신이 2대 이상일 때만 볼 수 있다. 그래서 가설 10은 클라우드 단계다.
❐ 7. 기록 원칙
- 가정을 먼저 쓴다. 숫자보다 그 숫자를 만든 가정이 중요하다. 가정이 틀렸으면 틀렸다고 쓴다.
- 절대 수치를 주장하지 않는다. 로컬 측정치는 상대 비교용이다.
- 실패도 기록한다. 재현이 안 되거나 예상과 다르면 그것도 결과다.
- 타임라인을 남긴다. 각 포스트 상단에 실험 날짜와 당시 환경 단계(1~4단계)를 적는다.
다음 글
- (1) 베이스라인: 단일 머신에서 부하 생성기가 측정을 얼마나 왜곡하는가
이 기사는 저작권자의
CC BY 4.0
라이센스를 따릅니다.