포스트

(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 라이센스를 따릅니다.