포스트

(0) MAU 1500만 트래픽 실험을 시작하며

왜, 어떤 조건에서

MAU 1500만 트래픽 실험1 / 1편 · 목차
  1. (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 라이센스를 따릅니다.