wiki 403 V1 한계 지점과 scale out scale up - 100-hours-a-week/KTB4-3rd-CLOUD GitHub Wiki
목차
- 문서의 결정 범위와 상위 가정
- V1에서 제공한 서비스
- V2 서비스 성장 가정
- V1 인프라의 한계
- V2 AI 기능 추가로 생기는 새 한계 가설
- V1 한계 지점과 scale-out · scale-up 결정
- 측정의 신뢰 범위
문서의 결정 범위와 상위 가정
이 문서는 위키에서 정의한 서비스 가치·사용자 행동·트래픽 가정을 시작점으로 삼아, V1 인프라의 한계를 수치로 증명하고 V2 확장 방향을 결정하는 것을 목적으로 한다.
문서 간 역할은 다음과 같다.
| 근거 | 이 문서에서 사용하는 내용 |
|---|---|
001~004 |
팀·비용 제약, 서비스 가치, 사용자 행동, V1 기준 트래픽 |
101~103 |
V1 단일 EC2·Docker Compose·Nginx·Spring·MySQL 구성 |
401 |
V2 서비스 확장과 사용자 행동 변화 |
402 |
V2 트래픽으로 V1의 한계를 검증하는 시나리오 |
403 |
실측 결과, 장애 전파 범위, 확장 결정, 다음 인프라 기준점 |
이 문서의 숫자는 다음 상태로 구분해 사용한다.
- 외부 근거: 출처에서 가져온 수치
- 내부 가정: 운영 데이터가 없어 부하테스트 초기값으로 선택한 수치
- 실측: 실험 환경에서 측정한 수치
- 파생값: 가정 또는 실측값에서 계산한 수치
V1은 FastAPI·AI 기능이 없는 AS-IS 실측 기준선이다. V2는 FastAPI, 이미지 처리 경량 모델, 외부 LLM 기반 혼잡도 판단이 추가된 TO-BE 구조다. 따라서 V1 실측값은 V2 AI 경로의 처리량을 직접 보증하지 않고, 기존 공유 자원의 한계와 V2 재측정의 기준선을 제공한다.
V1에서 제공한 서비스
V1 서비스는 모여타의 핵심 경험인 “사용자가 출퇴근이나 장거리 이동에서 대중교통 이외 택시팟을 모아서 이동하고자 하는 수요를 충족하는 서비스”을 제공하는 것에 있다.
모여타 서비스를 사용하는 사용자의 유즈 케이스는 다음과 같다.
출퇴근·심야 이동 문제 발생
→ 주변 택시팟·커뮤니티 게시글 조회
→ 위치·목적지·시간이 맞는 팟 선택
→ 참여 요청 및 승인
→ 팟별 WebSocket 채팅으로 약속 조율
→ 탑승·이동 상태 변경
→ 기본 정산
→ 평가·신고
V1에서 제공하는 다음 기능을 통해 직접 팟을 모으기 위해 겪어야 했던 불편함과 문제를 해결해준다.
| 영역 | V1 제공 기능 |
|---|---|
| 사용자 | 회원가입, 로그인 |
| 탐색 | 홈, 지도, 위치 기반 주변 팟 조회 |
| 팟 | 택시팟 생성, 상세 조회, 상태 관리 |
| 매칭 | 참여 요청·승인·거절·취소, 좌석 수 관리 |
| 채팅 | 팟별 WebSocket 채팅, 메시지 이력 저장 |
| 정산 | 참여자별 기본 정산 정보 제공 |
| 커뮤니티 | 게시글 조회 및 게시글 데이터 보존 |
| 인프라 | Nginx, Next.js, Spring Boot, MySQL, Docker Compose |
V2 서비스 성장 가정
1. DAU 1500명에서 3만6천명으로 증가
V2에서는 서비스 지역을 판교에서 수도권 주요 업무지구 6곳의 출퇴근 직장인을 대상으로 서비스 사용자가 확대된다. 주요 업무지구의 출퇴근 직장인구수는 약 115만명으로 산정해 전체 시장이 V1의 판교 직장인 시장에서 확대한다.
- 판교
- 강남
- 여의도
- 광화문·종로
- 구로·가산
- 마곡
BlaBlaCar 서비스의 벤치마킹을 기반으로 V2 MAU·DAU 산정
BlaBlaCar는 본래 자신의 여정에서 발생하는 비용을 절감해주는 장거리 카풀 매칭 플랫폼으로 다음과 같은 이유로 모여타 서비스의 DAU/MAU 활동률과 세션 빈도를 정하는 벤치마킹 자료로 선정했다.
- 출퇴근을 위해서 또는 이벤트·행사 장소에 가기 위한 이동 수단을 제안하는 서비스의 핵심 가치가 유사하다.
- 카풀 매칭이라는 제공하는 서비스 기능이 유사하다.
BlaBlaCar의 스페인 2025 공식 자료에 의하면 월평균 고유 활성 사용자가 30~40만 정도 되며, 연간 카풀 승객은 400만명이다. 이러한 연간 승객수를 일간 활동 프록시로 산출하자면 다음과 같은 활동률을 도출할 수 있다.
출처 : BlaBlaCar Spain 2025 activity report
일 평균 승객 활동 = 4,000,000 ÷ 365 = 10,959명/day
DAU/MAU = 10,959 ÷ 350,000 = 3.131%
서비스의 사용자 집단과 시장 규모가 다르기 때문에 사용자 수가 아닌 활동률을 도입하여 V2 기준의 115만명의 MAU에서 DAU를 3만 6천명으로 선정한다.
이 문서는 V2 용량 테스트 기준값으로
MAU 115만·DAU 3.6만을 채택한다.401의 기존10% 침투율·MAU 11.5만·DAU 3만가정과 동시에 사용하지 않고, 후속에401·402의 수치와 공식을 이 기준으로 동기화한다.
2. V2에서 성장하는 수치
V2 서비스로 성장에 따라 활성 사용자 수와 사용자의 행동의 변화가 발생한다.
- 일일 DAU 사용자 수: 1500명 → 36000명 으로 24배 증가
- 세션 체류 시간 증가
실시간 교통 상황에 대한 제보와 커뮤니티 글, 팟 모집글, 팟 채팅처럼 콘텐츠는 사용자 수와 비례적으로 증가한다. 따라서 사용자들이 체류하는 세션 시간이 증가하게 된다.
사용자 수와 더불어 세션 시간이 길어지면서 피크 시간대의 HTTP RPS와 쓰기 TPS도 함께 증가한다.
따라서 V1의 세션당 HTTP 3건 가정을 V2에서 세션당 HTTP 13건으로 늘린다. RPS는 세션 시작률에 이 호출 수를 곱해 계산한다.
목록·검색 4건 + 상세 4건 + 혼잡도 3건 + 쓰기 1건 + 인증·기타 1건 = 세션당 HTTP 13건 ```
행사 burst RPS
행사 종료 상황에서는 DAU의 10%가 10분 내에 유입된다고 가정한다.
- 이벤트 사용자: DAU의 10%
- 사용자당 HTTP 요청: 15건
- WebSocket 접속률: 50%
- WebSocket 유지 시간: 10분
- 접속자당 메시지: 6건
- HTTP와 메시지는 5분 동안 peak까지 상승한다고 가정
HTTP peak RPS
= 2 × 이벤트 사용자 × 15 ÷ 600초
WebSocket 연결률
= 최종 연결 수 ÷ 300초
메시지 peak rate
= 2 × 최종 연결 수 × 6 ÷ 600초
| 이벤트 사용자 | HTTP peak | 최종 WS 연결 | 연결률 | 메시지 peak |
|---|---|---|---|---|
| 3,600명 | 180.0 RPS | 1,800 | 6.00/s | 36.0/s |
3. V2에 추가되는 FastAPI·AI 기능
V2에서는 기존 V1 경로에 FastAPI 서버와 두 가지 AI 업무가 추가된다.
이미지 처리·판단
FastAPI 프로세스 내에 경량 이미지 처리·판단 모델을 상주시켜 추론을 진행한다.
사용자 이미지 전송
→ Nginx
→ Spring Boot의 인증·권한·업무 검증
→ FastAPI 이미지 전처리
→ 경량 모델 추론·판단
→ Spring Boot에 결과 반환
→ MySQL에 업무 결과 반영
이 경로는 일반 API와 달리 이미지 크기·모델 추론 시간·동시 추론 수·worker 수가 자원 점유를 결정한다.
혼잡도 판단
혼잡도는 사용자 조회가 발생할 때마다 새로 계산하지 않는다. 게시글 생성을 이벤트로 처리하고, 20분 주기로 거점별 결과를 집계한다.
[이벤트 경로]
새 게시글 생성
→ Spring Boot가 게시글·댓글 정보 조회
→ FastAPI를 통해 외부 LLM API에 판단 요청
→ 중간 혼잡도 결과를 메모리 캐시에 저장
[20분 주기 집계 경로]
거점별 캐시 결과 조회
→ FastAPI가 최종 혼잡도 판단
→ Spring Boot에 결과 전송
→ 사용자 혼잡도 조회는 최신 완료 값을 읽음
따라서 기존 402에서 사용한 "혼잡도 조회 1건당 최신값 upsert + 이력 insert" 모델은 폐기한다. V2 혼잡도의 부하는 조회 RPS가 아니라 다음 두 축으로 산정한다.
- 실시간 축: 게시글·댓글 이벤트율, 텍스트 크기, LLM 지연·오류율
- 배치 축: 거점 수, 거점별 캐시 항목 수, 20분 주기 내 처리 완료 시간
V1과 V2의 자원 경계
V1의 컨테이너 CPU 예산은 0.25 + 0.50 + 0.75 + 0.50 = 2.00 vCPU로 t3.medium의 2 vCPU를 이미 모두 배분한다. FastAPI를 같은 호스트에 추가하려면 다음 값을 실측해 V2 자원 예산을 다시 작성해야 한다.
| 항목 | 결정에 필요한 측정값 |
|---|---|
| FastAPI 기본 비용 | 무부하 RSS·CPU, worker 수 |
| 이미지 모델 | 모델 상주 메모리, worker별 복제량, 요청당 CPU-ms |
| 혼잡도 캐시 | 항목당 바이트, 최대 항목 수, TTL·퇴거 비용 |
| 외부 LLM | 지연 p95·p99, timeout·429·5xx, 동시 outbound 요청 수 |
| 호스트 여유량 | Docker·OS·SSM·CloudWatch·page cache를 위한 메모리·CPU |
FastAPI의 자원 예산과 장애 격리 방법이 결정되기 전까지 V1 테스트 결과로 V2 전체 스펙을 확정하지 않는다.
V1 인프라의 한계
기존 인프라 구성요소는 다음과 같다.
- t3.medium 1대
- Frontend·backend·MySQL·Nginx 4개 컨테이너
- Docker Compose 기반 컨테이너 운영
- S3
- ECR
- VPC public subnet
애플리케이션 서버가 동작하는 인스턴스는 t3.medium로 2 vCPU · 4 GiB 자원을 가진다. 하나의 EC2 인스턴스 안에 컨테이너 예산의 합계가 호스트 CPU를 100% 소진하도록 배분되어 있다.
| 컨테이너 | CPU | 메모리 | 기본 설정값 |
|---|---|---|---|
| nginx | 0.25 | 128 MB | worker_processes 1, worker_connections 16384, nofile 32768 |
| frontend(placeholder) | 0.50 | 512 MB | 상시 384 MB 점유 |
| backend(Spring) | 0.75 | 768 MB | -Xmx512m, nofile 16384, Tomcat threads 200 / max-connections 8192 |
| mysql | 0.50 | 768 MB | buffer pool 384 MB, max_connections 151 |
| 합계 | 2.00 | 2,176 MB |
이런 단일 인스턴스 구조에서는 V2 트래픽을 감당하면서 특정 컴포넌트 부하가 증가해도 해당 컴포넌트만 독립적으로 확장할 수 없다.
- Spring CPU가 부족해도 Next.js와 MySQL이 함께 있는 EC2를 증설해야 한다.
- MySQL Buffer Pool이 증가하면 Spring과 Next.js가 사용할 수 있는 메모리가 감소한다.
- MySQL 디스크 I/O가 증가하면 애플리케이션 로그와 Nginx 처리에도 영향을 줄 수 있다.
- WebSocket 연결이 증가하면 Spring의 소켓과 세션 메모리가 늘어나지만 REST API와 같은 컨테이너 자원을 사용한다.
반대로 특정 컴포넌트에 장애가 발생하면 단일 장애점이 된다.
한계 지점 측정 방법과 판정 기준
V2 트래픽 가정이 V1의 어느 자원을 먼저 포화시키는지 확인하기 위해 2026-09-20에 31개 시나리오를 실측했다. 기존 V1 실측값은 그대로 유지하면서 각 절을 장애 가설과 결정 기준까지 포함한 아래 9단계 흐름으로 보강한다.
① 서비스 동작 (유저플로우의 어느 구간)
→ ② 예상 트래픽 지표 (숫자)
→ ③ 부하 특성 (요청 유형·동시성·지속시간·데이터 크기)
→ ④ 점유하는 인프라 자원 (경로 + 자원 종류 + V1 배정량)
→ ⑤ 장애 가설 (어떤 자원이 먼저 포화되고 어디까지 전파되는가)
→ ⑥ 측정 목적과 관찰 지표 (SLI·원인 지표·중단 조건)
→ ⑦ 측정 설계 (시나리오·부하 단계·control/treatment·오류 주입)
→ ⑧ 측정 결과와 신뢰 범위 (최초 포화 자원·반복성·제약)
→ ⑨ 판정과 인프라 결정 (적용 조건·재검증 기준)
9단계 작성 규칙
각 절은 아래 질문에 답할 수 있어야 한다. 숫자를 적을 때는 외부 근거·내부 가정·실측·파생값 중 상태를 함께 표시한다.
| 단계 | 반드시 답할 질문 | 필수 산출물 |
|---|---|---|
| ① 서비스 동작 | 누가 어떤 상황에서 어떤 순서로 사용하는가? | 정상·재시도·예외 플로우 |
| ② 예상 트래픽 | 총량과 순간 피크를 어떤 산식으로 구했는가? | RPS·TPS·동시성·유지 시간·데이터 크기 |
| ③ 부하 특성 | 처리량, 상태 유지, 핫스팟, 배치, 외부 대기 중 무엇인가? | 독립변수와 통제변수 |
| ④ 점유 자원 | 요청이 시스템을 어떻게 통과하며 각 구간에서 무엇을 점유하는가? | 경로별 CPU·메모리·스레드·커넥션·I/O 예산 |
| ⑤ 장애 가설 | 첫 포화 자원은 무엇이고 사용자에게 어떻게 보이는가? | 원인 → 전파 → 사용자 영향 연쇄 |
| ⑥ 측정 목적·지표 | SLO 위반을 무엇으로 판정하고 원인을 무엇으로 설명할 것인가? | 사용자 SLI, 원인 지표, 정합성, 중단 조건 |
| ⑦ 측정 설계 | 어떤 변수만 바꾸어 원인을 분리할 것인가? | control·treatment, 부하 단계, 반복, warm-up·측정 시간 |
| ⑧ 결과·신뢰 범위 | 어디서 처음 깨졌고, 반복되며, 무엇은 미측정인가? | C_knee, C_safe, 표본 수, 제약, 하한·상한 |
| ⑨ 판정·재검증 | 이 결과로 무엇을 변경하며 성공을 어떻게 확인할 것인가? | 적용 조건, 우선순위, 재측정 통과 기준 |
신규 V2 시나리오는 아래 공통 실험 규칙을 적용한다.
- 각 단계는
warm-up 2분 + 측정 5분으로 시작하고,C_knee근처는 동일 조건으로 3회 반복한다. - 지속 부하와 누수는 계획 peak에서 최소 60분 soak로 따로 확인한다.
- 3회 반복의 중앙값을 대표값으로 사용하고, 지연·오류율의 최댓값과 처리량의 최솟값처럼 지표별 최악값을 함께 기록한다.
- 새 제품 SLO가 확정되기 전의 간섭 통과 기준은
핵심 API p95 악화 20% 미만,오류율 증가 0.5%p 미만으로 둔다. - 실패율 1% 초과, 컨테이너 메모리 한도의 90% 초과, OOM·정합성 오류 발생 시 해당 단계를 중단한다.
측정 구성과 판정 규칙은 다음과 같다.
- 부하는 로컬 k6에서 공개 HTTPS·WSS 주소로 발생시켜 공개 DNS, TLS, Nginx를 포함한 실제 진입 경로를 측정한다.
- 관측은 Node Exporter·cAdvisor·Micrometer·mysqld-exporter를 5초 간격으로 Prometheus가 수집한다.
- 자원 순수 비용을 재는 시나리오(연결 단가, 메시지 비용, 조회 RPS)는 REST 배경 부하 없이 실행한다. 간섭을 재는 시나리오는
control(단독)과treatment(동시 실행)를 같은 조건으로 연속 비교한다. - 측정창은 시나리오당 75~150초, 반복 1회다. 동일 조건 반복 측정(
c3↔g-c3-repeat)의 CPU 차이가 8%였으므로 지연 차이는 30% 이상일 때만 실재로 인정한다. - 지연·오류율은 k6 클라이언트 측정값을 사용한다. 자원 지표는 Prometheus 값을 사용한다.
위 30% 기준은 2026-09-20의 단발성 V1 실측 결과를 해석하기 위한 보수적 기준이다. 앞으로 수행할 V2 실험은 3회 반복을 전제로 하므로 별도의 잠정 간섭 기준인 p95 20%·오류율 0.5%p를 사용하며, 두 기준을 같은 측정 결과에 섞어 적용하지 않는다.
1. 조회 지표 증가로 컨테이너 자원 점유
① 서비스 동작
사용자가 출퇴근 경로에서 이동 수단을 찾는 구간이다.
앱 실행
→ 위치 기반 주변 팟·커뮤니티 목록 조회
→ 여러 팟의 출발지·목적지·시간·좌석·비용 비교
→ 팟 상세 조회
→ 조건을 바꿔 목록 재조회
② 예상 트래픽 지표
출퇴근 시간(피크 시간대 4시간)에 일간 세션의 70%가 집중될 때, 피크 시간대에 활동하는 사용자는 약 2만 5천명이다. 이 값은 동시 접속자 수가 아니며, RPS는 세션 시작률과 세션당 호출 수로 계산한다. WebSocket 동시 연결 수는 별도로 채팅 진입률과 연결 유지 시간을 사용해 산정한다.
현재 시퀀스의 한 세션을 HTTP 13건으로 정의하고, 피크 4시간에 일간 세션의 70%가 집중되었을 때, 순간 피크 계수 2.0을 적용한 RPS는 45.5 RPS가 나온다.
피크 세션 시작률
= DAU × (일 세션/DAU) × 70% ÷ (4 × 3,600초) × 2.0
피크 HTTP RPS
= 피크 세션 시작률 × 13건
| DAU | 피크 세션 시작률 | 계산 HTTP RPS |
|---|---|---|
| 36,000 | 3.501/s | 45.51 |
세션이 하루 2회·3회라면 RPS는 선형으로 약 91·137 RPS가 된다. 출퇴근 왕복 사용을 반영한 2회 세션, 약 91 RPS을 기준으로 둔다. 단순 조회 지표는 V1의 1.25 RPS에 비해 73배 증가한다.
③ 부하 특성
정상 상태에서 지속되는 처리량 부하다. 동시성이나 데이터 크기가 아니라 요청 1건의 비용 × 초당 건수가 자원 점유를 결정한다.
| 항목 | 값 |
|---|---|
| 요청 유형 | GET 목록·상세 (읽기 전용) |
| 동시성 | 요청당 Tomcat 스레드 1개, 응답 후 즉시 반환 |
| 지속시간 | 피크 4시간 지속 |
| 데이터 크기 | 목록 20건 단위 응답, 단건 상세 |
④ 점유하는 인프라 자원
목록·상세 조회
→ Nginx (0.25 vCPU · 128 MB) : TLS 종료, 프록시
→ Spring Boot (0.75 vCPU · 768 MB · -Xmx512m) : Tomcat 스레드 200, 역직렬화·JSON 직렬화 CPU
→ HikariCP (최대 10) : DB 조회 구간에서만 커넥션 점유
→ MySQL (0.5 vCPU · 768 MB, buffer pool 384 MB) : 인덱스 조회, buffer pool 히트
캐시된 값과 DB에서 읽어오는 값을 구분하기 위해 DB를 사용하지 않는 경로와 DB를 사용하는 경로를 같은 단계로 올려 Spring 자체 비용과 DB 경로 비용을 분리한다.
⑤ 장애 가설
조회 RPS 증가
→ Spring 직렬화 CPU 또는 MySQL 조회 시간 증가
→ Tomcat busy·HikariCP active·DB CPU 중 하나가 먼저 포화
→ 대기열과 커넥션 획득 시간 증가
→ 목록·상세 p95·p99 상승 후 timeout·5xx
light와 detail이 함께 느려지면 Spring·Nginx 계층, detail만 느려지면 HikariCP·MySQL 계층을 첫 원인으로 본다.
⑥ 측정 목적과 관찰 지표
- 사용자 SLI: 실제 RPS, p50·p95·p99, HTTP 오류율, dropped iteration
- Spring: process·container CPU, throttling, Tomcat busy·queue, heap·GC pause
- DB 경로: HikariCP active·pending·acquire, MySQL CPU, rows examined, buffer-pool hit, disk await
- 중단 조건: 오류율 1% 초과, p95 1초 초과, 목표 RPS 달성률 95% 미만
⑦ 측정 설계
조회 RPS를 단계적으로 올리며 어떤 자원이 먼저 한계에 도달하는지 측정한다. WS 연결은 0으로 고정해 이후 소켓 연결 유무에 따른 성능 차이를 비교할 기준점으로 삼는다.
| 계열 | endpoint 성격 | 부하 단계 | RUN_ID |
|---|---|---|---|
light |
DB·외부 호출 없음 (Spring 자체 비용) | 50 → 150 → 300 → 500 → 700 RPS | f-light-* |
detail |
DB 조회 포함 (풀·MySQL 비용) | 50 → 150 → 300 RPS | f-detail-* |
각 단계는 warm-up 15초 + 측정 75초이며, 측정 전 light 30 RPS로 3분간 JVM을 예열해 JIT·클래스로딩 영향을 제거했다.
⑧ 측정 결과와 신뢰 범위
| 계열·목표 RPS | 실제 RPS | p95 | p99 | 오류율 | backend CPU | CPU throttling | MySQL CPU | 호스트 CPU |
|---|---|---|---|---|---|---|---|---|
| light 50 | 45.9 | 11.5 ms | 16.9 ms | 0% | 0.029 | 0 | 0.016 | 8.4% |
| light 150 | 137.6 | 11.3 ms | 18.2 ms | 0% | 0.066 | 0 | 0.018 | 13.5% |
| light 300 | 275.0 | 11.5 ms | 21.9 ms | 0% | 0.085 | 0 | 0.020 | 17.4% |
| light 500 | 458.3 | 15.0 ms | 53.6 ms | 0% | 0.120 | 0 | 0.018 | 22.6% |
| light 700 | 641.6 | 19.4 ms | 64.1 ms | 0% | 0.148 | 0 | 0.018 | 27.8% |
| detail 50 | 45.9 | 13.2 ms | 26.4 ms | 0% | 0.047 | 0 | 0.036 | 10.5% |
| detail 150 | 137.5 | 12.3 ms | 24.8 ms | 0% | 0.086 | 0 | 0.070 | 18.0% |
| detail 300 | 274.5 | 16.7 ms | 44.1 ms | 0% | 0.125 | 0 | 0.105 | 26.0% |
측정 최대 지점에서도 오류율 0%, dropped iteration 0, CPU throttling 0, Tomcat busy 스레드 최대 1이다. SLO가 깨지는 지점(C_knee)에 도달하지 못했다.
요청 1건의 자원 단가와 컨테이너 quota 기준 이론 상한은 다음과 같다.
요청당 CPU = backend CPU(cores) × 1000 ÷ 실제 RPS
quota 이론 상한 = 750 CPU-ms/s ÷ 요청당 CPU
| 계열 | 요청당 CPU | 0.75 vCPU quota 기준 이론 상한 | 기준 91 RPS 대비 여유 |
|---|---|---|---|
light |
0.231 CPU-ms | 약 3,250 RPS | 7.0배 (실측 641 RPS 기준) |
detail |
0.455 CPU-ms | 약 1,650 RPS | 3.0배 (실측 274 RPS 기준) |
MySQL CPU는 detail 274 RPS에서 0.105 cores로 배정량 0.5의 21%만 사용했다. 행사 burst의 HTTP peak 180 RPS도 이 범위 안에 있다.
⑨ 판정과 인프라 구성 변경
- 조회 경로는 V1의 1차 병목이 아니다. 기준 91 RPS의 7배, 행사 peak 180 RPS의 3.6배까지 p95가 20 ms 이내이며 CPU throttling이 발생하지 않았다.
- 따라서 조회량을 근거로 한 scale-up(인스턴스 상향)이나 scale-out(다중 EC2)은 필요하지 않다. 조회 때문에 인스턴스를 키우면 실제 병목(공유 자원)은 그대로 남는다.
- 단, 이 결론은 읽기 전용 경로에 한정된다. 아래 3절에서 채팅 이력 저장이 같은 HikariCP 풀을 포화시키면 조회 p95가 13.6배 악화됐다. 조회 지연의 원인은 조회량이 아니라 동시에 실행되는 쓰기다.
- 후속 과제: 혼잡도 조회는 이제 최신 완료 값을 읽는 경로로 분리된다. 게시글·댓글 이벤트 처리와 20분 주기 집계는 별도의 FastAPI·LLM 부하로 측정해야 하며, 일반 조회와 동시 실행했을 때 핵심 API p95·오류율이 악화되는지 비교한다.
2. WebSocket 연결 증가가 Spring 자원 점유
① 서비스 동작
택시 팟 모집은 이전 글을 재활용하는 것이 아닌 매일 새롭게 모집하고 새로운 채팅이 발생한다. 따라서 기존의 유휴 연결이 아닌 신규 연결이 증가한다.
참여 승인
→ 팟별 채팅방 입장 (WebSocket handshake)
→ 탑승 위치·시간 조율 동안 연결 유지
→ 매칭 미성사 1시간 무활동 또는 정산 완료 시 연결 종료
② 예상 트래픽 지표
행사 burst 가정에서 최종 동시 연결 1,800개, 신규 연결률 6.00/s다.
| 항목 | V2 가정값 |
|---|---|
| 최종 동시 WebSocket 연결 | 1,800개 |
| 신규 연결률 | 6.00 conn/s |
| 연결 유지 시간 | 10분 이상 |
③ 부하 특성
두 가지 성격이 섞여 있어 분리해서 측정해야 한다.
| 구분 | 성격 | 변수 |
|---|---|---|
| 연결 유지 | 장시간 상태 보유형 부하. 메시지가 없어도 비용이 0이 아니다 | 동시 연결 수 |
| 연결 수립 | 짧고 비싼 순간 부하. 수립마다 TLS handshake 발생 | 초당 신규 연결 수 |
④ 점유하는 인프라 자원
WebSocket handshake
→ Nginx (0.25 vCPU · 128 MB, worker_processes 1, nofile 32768)
: TLS handshake CPU, 연결당 client·upstream 소켓 2개, 연결별 프록시 버퍼
→ Spring Boot (0.75 vCPU · 768 MB · -Xmx512m, nofile 16384)
: 세션 객체 heap, NIO direct buffer, Tomcat max-connections 8192
유휴 WebSocket 연결이 DB 커넥션을 계속 점유하지는 않지만, 소켓·파일 디스크립터·세션 메모리는 연결이 유지되는 동안 점유한다.
⑤ 장애 가설
동시 연결 수 또는 초당 핸드셰이크 증가
→ Nginx 프록시 버퍼·소켓·FD 또는 Spring 세션 메모리 증가
→ accept·listen queue, FD, 메모리 중 하나가 먼저 포화
→ handshake 대기가 timeout까지 증가
→ 사용자는 채팅방이 열리지 않는 상태를 경험
메모리·CPU가 여유로운데 연결만 더 이상 늘지 않으면 서버 자원 외에 발생기·네트워크 한도를 함께 의심한다.
⑥ 측정 목적과 관찰 지표
- 사용자 SLI: 성공한 동시 연결 수, handshake p95·p99, 연결 실패율, 재접속 횟수
- Nginx·OS: active connection, accepts·handled, listen overflow·drop, container CPU·RSS, process·host FD, socket state
- Spring: active WebSocket session, heap·direct buffer, GC, process FD, CPU·RSS
- 발생기: CPU·RSS·FD, dropped iteration, 소켓 오류, 업로드 대역폭
- 중단 조건: 연결 실패율 1% 초과, handshake p95 5초 초과, 발생기 CPU 80% 초과
⑦ 측정 설계
| 목적 | 설계 | RUN_ID |
|---|---|---|
| 연결 1개의 자원 단가 | 0 → 3,000 연결을 12.5분에 선형 증가시키고, 증가 구간을 lab_ws_connections 대비 자원 회귀 |
a-ramp3000 |
| 연결 시도율의 영향 | 연결 1,000개로 고정하고 초당 신규 연결만 20 → 100 → 200으로 변경 | b-rate20/100/200 |
램프 구간 자체를 독립변수로 사용하면 5초 수집 간격에서 표본 약 150점이 확보되어, 단계별 평균보다 안정적인 연결당 단가를 얻을 수 있다. 두 측정 모두 REST 부하 없이 실행했다.
⑧ 측정 결과와 신뢰 범위
연결 1개의 자원 단가
| 항목 | 실측 | 1,800 연결 환산 | Spring 배정량 대비 |
|---|---|---|---|
| 연결당 heap | 28.2 KB | 50.8 MB | -Xmx512m의 9.9% |
| 연결당 direct buffer | 0.2 KB | 0.4 MB | 무시 가능 |
| 1,000 연결당 CPU | 0.0092 cores | 0.017 cores | 0.75 vCPU의 2.2% |
heap 기준 수용 가능 연결 수는 (512 MB × 0.7 − 기저 heap 31.9 MB) ÷ 28.2 KB ≈ 11,800개다. 즉 메모리와 CPU 계산만으로는 1,800 연결을 충분히 감당한다.
실제 동시 연결 상한
| 항목 | 실측 |
|---|---|
| 지시한 목표 연결 | 3,000 |
| 실제 최대 동시 연결 | 1,734개 |
| 그 지점의 handshake p95 | 30,001 ms (30초 타임아웃) |
| WebSocket 오류율 | 57.3% |
| 그때 heap 최대 / 컨테이너 메모리 | 301 MB / 604 MB (limit 768 MB) |
같은 상한이 다른 시나리오에서도 반복됐다. 목표 1,800 연결을 지시한 네 시나리오의 실제 최대는 c3 1,708 / c4 1,633 / d1 1,628 / g 1,692개다. 반면 1,000 연결을 지시한 세 시나리오는 오류 0%로 1,000개를 모두 수립했다.
즉 현재 시험 환경에서 관측된 end-to-end 동시 연결 임계는 약 1,700개이며, 이는 V2 행사 가정치 1,800개와 거의 같은 지점이다. 초과분은 즉시 거부되지 않고 30초 대기 후 실패하므로, 사용자에게는 "채팅방이 열리지 않는 상태" 로 나타난다. 다만 Nginx·발생기·네트워크 중 원인이 확정되지 않았으므로 서버 자체의 확정 상한으로 표현하지 않는다.
연결 시도율의 영향
| 신규 연결률 | handshake p95 | WS 오류율 | 달성 연결 |
|---|---|---|---|
| 20 conn/s | 33.5 ms | 0% | 1,000 |
| 100 conn/s | 156.6 ms | 0% | 1,000 |
| 200 conn/s | 3,568.8 ms | 0% | 1,000 |
시도율이 10배(20 → 200) 되면 handshake 지연은 107배가 된다. 선형이 아니다. 다만 오류율은 0%이므로 이 구간은 실패가 아니라 대기다.
⑨ 판정과 인프라 구성 변경
- 신규 연결률 6.00/s는 문제가 아니다. 가정치의 3배인 20 conn/s에서도 handshake p95가 33.5 ms였다.
- 동시 연결 1,800개는 V1의 한계선과 겹친다. 실측 상한이 약 1,700개이므로 행사 가정치에 여유가 없다.
- 이 상한은 scale-up으로 해결된다고 단정할 수 없다. heap은 11,800개까지 여유가 있고 CPU는 2.2%만 사용했으므로, 상한의 원인이 메모리·CPU가 아니다. 후보는 Nginx 128 MB 안의 연결별 프록시 버퍼, Nginx 단일 worker의 연결 처리, 발생기·네트워크 측 소켓 한도다.
- 따라서 이 항목의 다음 작업은 확장 결정이 아니라 원인 규명이다. 연결을 1,500 → 1,800까지 100개 단위로 올리며 Nginx 컨테이너 메모리·CPU와 listen queue overflow를 함께 관측하고, Nginx를 우회해 Spring에 직접 연결해 동일 상한이 재현되는지 확인한다.
- 확보된 연결당 단가(28.2 KB, 1,000개당 0.0092 cores)는 WebSocket을 별도 프로세스로 분리할 때의 메모리·CPU 산정 근거로 사용한다.
3. 채팅 메시지 처리와 이력 저장
① 서비스 동작
채팅방에서 탑승 위치와 시간을 조율하는 구간이다. 메시지는 전달 전에 이력으로 저장된다.
② 예상 트래픽 지표
행사 burst 가정에서 메시지 peak는 36.0/s다. 4인 팟에서 발신자를 제외한 3명에게 전달되면 전달량은 최대 약 108건/s다.
| 항목 | V2 가정값 |
|---|---|
| inbound 메시지 peak | 36.0 msg/s |
| 메시지 유형 | 짧은 대화 + 긴 안내·정산 내역이 혼재 |
③ 부하 특성
두 축이 섞인 부하다. 메시지 건수와 payload 바이트가 각각 다른 비용을 만든다. 저장이 포함되면 여기에 응답 경로가 DB 트랜잭션에 묶이는 성격이 더해진다.
| 항목 | 값 |
|---|---|
| 요청 유형 | WebSocket 프레임 수신 → 저장 → ACK |
| 동시성 | 연결 수만큼 동시 수신 가능, 저장은 커넥션 1개 점유 |
| 지속시간 | 저장 트랜잭션이 커넥션을 점유하는 시간이 처리량 상한을 결정 |
| 데이터 크기 | 256 B ~ 8 KB |
④ 점유하는 인프라 자원
메시지 수신
→ Spring Boot : 프레임 처리·역직렬화 CPU(건수), 버퍼·직렬화(바이트), heap allocation과 GC
→ HikariCP (최대 10, 획득 타임아웃 1,000 ms) : 저장 트랜잭션 동안 커넥션 1개 점유
→ MySQL (0.5 vCPU) : insert, redo log, fsync, EBS I/O
이 풀 10개는 목록·상세 조회, 좌석 승인, 채팅 저장과 공유한다. V2 혼잡도 중간 결과는 FastAPI 메모리 캐시에 저장하므로 조회당 DB 쓰기로 계산하지 않는다. 다만 20분 최종 결과를 Spring이 영속화한다면 그 짧은 쓰기 구간은 동일 풀을 사용할 수 있다.
⑤ 장애 가설
채팅 메시지율·저장 트랜잭션 시간 증가
→ HikariCP 커넥션 10개가 저장 대기에 장시간 점유
→ 커넥션 획득 대기·timeout
→ 채팅 저장 실패와 목록·상세 조회 지연
→ 사용자 재전송·새로고침이 부하를 추가 증폭
메시지 건수만 늘어 CPU가 포화되는 경로와, DB 저장으로 풀이 포화되는 경로를 분리한다.
⑥ 측정 목적과 관찰 지표
- 사용자 SLI: inbound·ACK·fan-out msg/s, ACK p95·p99, 저장 실패율, 순서·중복·유실
- Spring·JVM: CPU, allocation rate, heap·GC pause, WebSocket send queue
- DB 경로: HikariCP active·pending·acquire, transaction·commit latency, MySQL CPU, redo·fsync·disk await
- 장애 전파: 동시 조회 p95·p99·오류율
- 중단 조건: 저장 오류율 1% 초과, HikariCP pending 지속 증가, 메시지 유실·순서 위반 1건 이상
⑦ 측정 설계
| 목적 | 설계 | RUN_ID |
|---|---|---|
| 건수 비용과 바이트 비용 분리 | 연결 200·1,800 × payload 256 B·8,000 B 의 2×2, 저장 없는 echo 경로 | c1~c4 |
| 저장의 비용 | 저장 없음(c3) → 저장만 → 저장 + 조회 50 RPS |
d1, d2 |
| 저장이 조회에 주는 영향 | control(조회 단독) ↔ treatment(조회 + 저장) | d0-rest-control, d2-rest-treatment |
⑧ 측정 결과와 신뢰 범위
메시지 건수 비용과 바이트 비용
연결 200개 두 시나리오는 메시지 건수가 16,684건으로 정확히 같아 바이트 비용만 분리된다.
| 시나리오 | 연결 | payload | 메시지 | 실제 msg/s | backend CPU |
|---|---|---|---|---|---|
c1 |
200 | 256 B | 16,684 | 97.5 | 0.0378 |
c2 |
200 | 8,000 B | 16,684 | 94.8 | 0.0511 |
c3 |
1,708 | 256 B | 10,628 | 44.9 | 0.0388 |
c4 |
1,633 | 8,000 B | 9,928 | 42.4 | 0.0494 |
메시지 1건 기본 비용 = 0.0378 cores ÷ 97.5 msg/s ≈ 0.39 CPU-ms/건
payload 바이트 비용 = (0.0511 − 0.0378) ÷ (7.56 KB × 94.8/s) ≈ 0.02 CPU-ms/KB
8,000 B 메시지 1건 = 0.39 + 0.15 ≈ 0.54 CPU-ms → 건수 비용이 72%
연결 1,708개 시나리오(c3)는 메시지가 44.9 msg/s로 절반인데 CPU는 c1과 같다. 즉 연결 1,508개 추가가 메시지 52.6건/s와 맞먹는 비용이다.
이력 저장의 비용
| 항목 | c3 저장 없음 |
d1 저장만 |
d2 저장 + 조회 |
|---|---|---|---|
| 실제 msg/s | 44.9 | 47.4 | 37.6 |
| 커밋 p95 | — | 494 ms | 109~466 ms |
| HikariCP active (최대 10) | 0 | 10 / 10 | 7 |
| HikariCP 대기 | 0 | 53 | 0 |
| 커넥션 획득 p95 | 0.97 ms | 445 ms | 47~214 ms |
| 채팅 저장 오류/s | 0 | 1.02 | 3.03 |
| backend CPU | 0.0388 | 0.0896 | 0.0734 |
| MySQL CPU | 0.018 | 0.072 | 0.070 |
저장만 켜는 것으로 풀이 포화(active 10/10)되고 대기가 53건까지 쌓였다. MySQL CPU는 배정량 0.5의 14%만 사용했으므로 한계는 DB 처리 능력이 아니라 커넥션 점유시간이다.
저장이 조회에 주는 영향
| 구간 | 조회 p95 | 조회 p99 | 조회 실패율 |
|---|---|---|---|
control d0-rest-control (저장 없음) |
12.1 ms | 28.5 ms | 0% |
treatment d2-rest-treatment (저장 동시 실행) |
164.8 ms | 663.6 ms | 1.53% |
| 변화 | 13.6배 | 23배 | — |
treatment 구간의 실제 메시지율은 37.6 msg/s로, V2 행사 가정치 36.0 msg/s와 거의 같다. 즉 V2가 가정한 메시지 peak 수준에서 이미 일반 조회 API가 무너진다.
⑨ 판정과 인프라 구성 변경
| 관측 증거 | 판정 |
|---|---|
| 저장만으로 풀 active 10/10, 대기 53, 획득 p95 445 ms | 커넥션 점유시간이 병목 → 채팅 저장 비동기화(큐 + 배치 insert) |
| MySQL CPU가 배정량의 14% | DB 처리 능력이 아니라 풀이 한계 → 풀 증설·DB scale-up보다 점유시간 단축이 먼저 |
| 36 msg/s 수준에서 조회 p95 13.6배·실패 1.53% | 공용 풀이 장애 전파 경로 → 읽기·쓰기 풀 분리 |
저장 없음(c3)에서는 풀 지표 전부 0 |
실시간 전달 경로와 영속 경로 분리가 V2 필수 항목 |
| 메시지 건수 비용이 바이트 비용의 2.6배 | payload 압축·상한보다 건당 처리 비용 절감(ACK 경량화, 배칭)이 우선 |
이 결과는 긴 시간 DB 커넥션을 점유하는 어떤 쓰기 경로도 공용 풀을 통해 조회에 장애를 전파할 수 있음을 보여준다. V2 혼잡도 경로는 조회당 DB 쓰기가 아니므로 이 결과를 그대로 대입하지 않고, 20분 최종 결과 영속화가 있는 경우에만 동일한 control/treatment 방법으로 재검증한다.
4. WebSocket과 REST API의 자원 경합
① 서비스 동작
채팅 연결을 유지한 사용자가 동시에 목록·상세를 조회하는 구간이다. V1에서는 REST API와 WebSocket이 같은 Spring Boot 컨테이너에서 동작한다.
② 예상 트래픽 지표
행사 burst에서 동시 WebSocket 연결 1,800개와 HTTP 180 RPS가 같은 시간대에 발생한다.
③ 부하 특성
자원 공유형 간섭이다. 두 부하가 각각은 감당 가능해도, 같은 프로세스 안에서 heap·GC·스레드를 공유하면서 서로를 악화시킨다.
④ 점유하는 인프라 자원
REST 요청 ─┐
├─→ 같은 Spring Boot 컨테이너 (0.75 vCPU · 768 MB · -Xmx512m)
WS 연결 ───┘ : 같은 heap, 같은 GC, 같은 Tomcat 스레드 풀, 같은 HikariCP 10개
Next.js(0.5·512MB) / MySQL(0.5·768MB) : 별도 quota → 직접 영향 없음
⑤ 장애 가설
WebSocket 세션 메모리 증가
→ 같은 JVM의 heap 사용량·allocation·GC pause 증가
→ REST 스레드가 GC pause에 함께 멈춤
→ DB가 여유로워도 조회 p95·p99·5xx 악화
HikariCP·MySQL 지표는 그대인데 heap·GC와 REST 지연만 함께 증가하면 JVM 내부 자원 공유를 원인으로 판정한다.
⑥ 측정 목적과 관찰 지표
- 사용자 SLI: 조회 p95·p99·오류율, dropped iteration, WebSocket 유지·재접속
- JVM: heap used, allocation rate, young·old GC count, GC pause p95·max, thread count
- 컨테이너·호스트: Spring CPU·RSS·throttling, host memory·swap·CPU credit
- 대조 지표: HikariCP·MySQL·Next.js CPU를 같이 관찰해 DB·호스트 전체 원인을 배제
- 간섭 실패: control 대비 조회 p95 20% 이상 악화 또는 오류율 0.5%p 이상 증가
⑦ 측정 설계
control : WS 0개 상태에서 조회 50 RPS
treatment : WS 2,100개를 지시해 유지한 상태에서 같은 조회 50 RPS
두 실행을 연속 배치해 CPU credit 조건을 맞췄다(f1c-rest-control, f1c-ws2100, f1c-rest-treatment).
⑧ 측정 결과와 신뢰 범위
| 항목 | control (WS 0) | treatment (WS 1,642개 유지) | 변화 |
|---|---|---|---|
| 조회 p95 | 11.0 ms | 167.2 ms | 15배 |
| 조회 p99 | 33.4 ms | 914.8 ms | 27배 |
| 조회 실패율 | 0% | 4.54% | — |
| dropped iterations | 0 | 24 | — |
| backend CPU | 0.032 | 0.061 | 1.9배 |
| heap 최대 | 123 MB | 277 MB | 2.3배 |
| GC pause p95 | 3.1 ms | 257 ms | 82배 |
| 컨테이너 메모리 | 601 MB | 632 MB (limit 768) | +5% |
| HikariCP 대기 | 0 | 0 | 변화 없음 |
| MySQL CPU | 0.017 | 0.018 | 변화 없음 |
| Next.js CPU | 변화 없음 | 변화 없음 | — |
악화와 함께 움직인 지표는 **GC pause(3.1 → 257 ms)와 heap(123 → 277 MB)**이다. HikariCP와 MySQL은 변화가 없으므로 원인은 DB 경로가 아니라 같은 JVM의 메모리·GC다. Next.js와 MySQL 컨테이너는 영향을 받지 않았으므로 컨테이너 quota 격리는 정상 작동했고, 경합은 Spring 컨테이너 내부에서만 발생했다.
⑨ 판정과 인프라 구성 변경
- 조회 p95가 control 대비 15배, p99가 27배 악화되고 오류율 4.54%가 발생했다. 판정 기준(20% 악화)을 크게 초과한다.
- WebSocket 연결 자체는 CPU·heap 단가가 작지만(2절), 연결이 만드는 heap 증가가 GC를 통해 REST 응답 시간에 전이된다.
- 이 경합은 인스턴스를 키우는 scale-up으로는 근본 해결되지 않는다. 같은 JVM에 두는 한 연결 수가 늘 때마다 같은 전이가 반복되며, 한쪽 부하가 다른 쪽의 SLO를 결정하게 된다.
- 따라서 REST와 WebSocket의 프로세스 분리가 V2 설계의 1순위다. 분리 후 이 절의 control/treatment를 동일 조건으로 재실행해 조회 p95 악화가 20% 미만인지 검증한다.
5. 인기 팟 좌석 경쟁
① 서비스 동작
V2에서는 노선과 거점이 늘어나면서 사용자가 여러 팟을 비교한다.
팟 목록 조회
→ 출발 시간 비교
→ 목적지와 경로 비교
→ 남은 좌석 확인
→ 예상 비용 비교
→ 팟 상세 조회
→ 참여 요청
원하는 팟이 마감되거나 참여 요청이 거절되면 다음 행동이 이어진다.
좌석 마감
→ 목록 새로고침
→ 다른 출발 시간 검색
→ 검색 반경 확대
→ 다른 거점 탐색
→ 대체 팟 참여
→ 직접 팟 생성
② 예상 트래픽 지표
| 요청 종류 | 계산 | RPS |
|---|---|---|
| 팟 참여 요청 | 36,000 ÷ 14,400 | 약 2.5 |
| 참여 승인·거절 | 36,000 ÷ 14,400 | 약 2.5 |
| 인기 팟 집중(승인의 50%) | 2.5 × 0.5 | 약 1.25 |
이 값은 평균 RPS다. 아래 ⑥에서 확인되듯 좌석 경쟁의 실제 위험은 평균 RPS가 아니라 좌석 공개·마감 순간에 같은 행으로 동시에 도착하는 요청 수다.
③ 부하 특성
직렬화되는 경합 부하다. 동일 행을 SELECT ... FOR UPDATE로 잠그기 때문에 요청이 순차 처리되며, 대기 중에도 커넥션을 점유해 자원을 이중으로 소비한다.
| 항목 | 값 |
|---|---|
| 요청 유형 | POST 승인 (읽기 후 조건부 갱신) |
| 동시성 | 같은 pod_id 행에 동시 도착 N건 |
| 지속시간 | 트랜잭션 시간 = lock 보유 시간 |
| 데이터 크기 | 작음 (행 1~2개) |
④ 점유하는 인프라 자원
승인 요청
→ Spring 스레드
→ HikariCP 커넥션 1개 점유 (최대 10, 획득 타임아웃 1,000 ms)
→ MySQL 동일 row X-lock (innodb_lock_wait_timeout 3,000 ms)
→ 대기 요청은 lock 대기와 커넥션 점유를 동시에 유지
임계 조건이 두 개이며 서로 다르다.
풀 고갈 : 동시 요청 > 10 → 커넥션 획득 1,000 ms 초과 → 503
lock 초과 : 동시 요청 × 트랜잭션 시간 > 3,000 ms → 503
⑤ 장애 가설
동일 팟에 동시 승인 요청 집중
→ 하나의 row lock 뒤에 요청이 직렬화
→ 대기 요청이 HikariCP 커넥션을 점유
→ 풀 10개 고갈·획득 timeout
→ 승인 실패와 무관한 조회·채팅·정산의 꼬리 지연
동일 동시성을 여러 팟으로 분산했을 때는 정상이고 하나의 pod_id에서만 실패하면 row hot-spot을 원인으로 판정한다.
⑥ 측정 목적과 관찰 지표
- 사용자 SLI: 승인 p95·p99·오류율, 오류 코드별 건수, 재시도·중복 요청
- 정합성: 잔여 좌석 음수 0건, 정원 초과 0건, 동일 요청 중복 승인 0건
- 풀·잠금: HikariCP active·pending·acquire, row-lock wait p95·max, deadlock, transaction duration
- 장애 전파: 동시 조회 p95·p99·오류율
- 중단 조건: 정합성 오류 1건 이상, 승인 실패율 1% 초과, HikariCP acquire p95 1,000 ms 이상
⑦ 측정 설계
RPS가 아니라 동시 도착 수를 제어한다.
| 항목 | 설계 | RUN_ID |
|---|---|---|
| 동일 팟 경합 | 같은 팟에 동시 5 · 20 · 50 · 100건을 3라운드 발행 (lock 보유 100 ms) | e-same-c5/20/50/100 |
| 대조군 | 서로 다른 팟에 같은 동시성 | e-dist-c50 |
| 다른 API로의 전파 | control(조회 단독) ↔ treatment(조회 + 동시 승인 50건) | e0-rest-control, e-mix-rest |
⑧ 측정 결과와 신뢰 범위
동시 도착 수별 결과
| 동시성 | p95 | p99 | 실패율 | HikariCP 대기 | 획득 p95 | 좌석 lock p95 | 서버 5xx/s |
|---|---|---|---|---|---|---|---|
| 5 | 542 ms | 546 ms | 0% | 0 | 2.3 ms | 425 ms | 0 |
| 20 | 1,961 ms | 2,033 ms | 4.76% | 10 | 934 ms | 971 ms | 0.05 |
| 50 | 1,872 ms | 2,106 ms | 59.5% | 32 | 1,061 ms | 976 ms | 1.14 |
| 100 | 1,627 ms | 2,042 ms | 79.9% | 84 | 1,067 ms | 1,046 ms | 5.73 |
- 동시 20건이 분기점이다. 풀 크기 10을 넘는 순간 대기가 발생하고 획득 p95가 934 ms로 타임아웃(1,000 ms) 직전까지 올라간다.
- 동시 50건 이상에서는 획득 p95가 1,060 ms로 타임아웃에 붙어 실패율이 59.5% → 79.9%로 뛴다. 즉 실패의 주된 원인은 lock 자체가 아니라 lock 대기가 커넥션을 붙잡아 발생한 풀 고갈이다.
- 정합성 위반(잔여 좌석 음수, 정원 초과, 중복 승인)은 관측되지 않았다.
다른 API로의 전파 (조회 50 RPS 병행)
| 구간 | 조회 p95 | 조회 p99 | 조회 실패율 |
|---|---|---|---|
control e0-rest-control |
10.7 ms | 17.5 ms | 0% |
treatment e-mix-rest |
26.4 ms | 844.5 ms | 0.38% |
| 변화 | 2.5배 | 48배 | — |
조회 p95는 2.5배지만 p99가 48배다. 좌석 경합이 공용 풀을 통해 조회 API의 꼬리 지연을 만든다.
⑨ 판정과 인프라 구성 변경
- 트래픽 가정의 단위를 바꿔야 한다. 평균 1.25 RPS에 트랜잭션 시간 0.5초를 적용하면 평균 동시성은 0.6으로 V1이 통과한다. 그러나 실제 한계는 동시 20건이므로, 좌석 공개·마감 순간의 동시 승인 요청 수로 가정을 재산정해야 한다.
- 변경 우선순위
- 좌석 차감을 DB row lock 밖으로 옮긴다(원자 카운터 + 멱등키). lock 보유 시간이 사라지면 커넥션 점유도 함께 사라진다.
- 쓰기 경로의 커넥션 풀을 조회와 분리해 승인 API가 풀 전체를 잠식하지 못하게 한다.
- 풀 크기 증설은 후순위다. 10 → 20은 임계를 동시 20 → 40으로 미룰 뿐이며, MySQL 동시 실행 수만 늘린다.
- 인스턴스·DB scale-up으로 트랜잭션 시간이 짧아지면 임계점을 늦출 수는 있지만, 동일 row의 직렬화와 공용 풀을 제거하지는 못한다. 따라서 scale-up을 근본 해결책으로 판정하지 않는다.
6. 단일 장애점(SPOF)
V2 트래픽의 성장을 t3.medium 1대에 4개의 컨테이너가 감당한다. 만일 Spring Boot 컨테이너가 오류나 OOM Kill 등 다양한 원인으로 중단이 된다면 곧 단일 장애점으로 이어져 서버 장애가 나타난다.
측정에서 확인된 관련 사실은 다음과 같다.
- Spring Boot 컨테이너 메모리는 측정 전반에서 574~652 MB를 사용했다(limit 768 MB, 75~85%). WebSocket 연결이 유지되는 구간에서 상한에 더 가까워졌다.
- 4절에서 WebSocket 연결이 heap을 123 MB → 277 MB로 올렸다.
-Xmx512m과 컨테이너 한도 768 MB 사이의 여유는 약 256 MB이며, 이 구간에서 Metaspace·스레드 스택·NIO direct buffer가 함께 증가한다. - 즉 heap OOM보다 컨테이너 OOM Kill이 먼저 발생할 수 있는 구조다. 이번 창에서 OOM Kill은 발생하지 않았으나 여유율이 15~25%에 불과하다.
- 컨테이너 quota 격리는 작동했다(4절: Spring 포화 시 Next.js·MySQL 영향 없음). 그러나 격리는 다른 컨테이너를 보호할 뿐, Spring 자체의 중단을 막지 못한다.
단일 EC2·단일 MySQL 구조에서는 호스트 장애 중 무중단을 제공할 수 없다. 이 항목은 성능 측정으로 해결되지 않으며 다중 EC2와 로드밸런서, 외부 DB(RDS) 라는 구성 변경으로만 해소된다.
V2 AI 기능 추가로 생기는 새 한계 가설
이 장은 V1 실측 결과가 아니라 V2에서 새로 검증해야 할 가설과 측정 설계를 정의한다. 측정 전에는 FastAPI의 수용 용량과 확장 방식을 확정하지 않는다.
7. 이미지 처리 경량 모델
① 서비스 동작과 사용자 행동
정상 경로는 다음과 같다.
사용자가 정산·확인 화면에서 이미지 첨부
→ Nginx가 크기 제한을 검사하고 Spring Boot로 전달
→ Spring Boot가 인증·권한·업무 상태를 검증
→ FastAPI에 이미지와 request_id를 전달
→ FastAPI가 디코딩·리사이즈·정규화를 수행
→ 경량 모델이 판단 결과와 confidence를 반환
→ Spring Boot가 request_id 멱등성을 확인하고 업무 결과를 저장
→ 사용자에게 결과 또는 수동 입력 화면을 제공
예외 흐름은 아래처럼 처리한다.
- 지원하지 않는 포맷·손상 파일·크기 초과: 추론 전 4xx로 종료한다.
- confidence가 품질 기준보다 낮음: 결과를 자동 반영하지 않고 수동 입력으로 전환한다.
- FastAPI timeout·5xx: 서버 자동 재시도는 최대 1회로 제한하고, 그 후에는 수동 입력으로 전환한다.
- 중복 요청:
request_id를 멱등키로 사용해 결과를 중복 저장하지 않는다.
② 예상 트래픽 지표
운영 데이터가 없으므로 예상 요청률과 용량 탐색을 분리한다.
예상 이미지 RPS
= DAU × 해당 기능 진입률 × 사용자당 이미지 수 × 재시도 계수
× 피크 집중률 ÷ 피크 시간(초)
필요 동시 추론 수
≈ 예상 이미지 RPS × end-to-end 평균 체류시간(초)
기능 진입률·이미지 수·재시도 계수는 아직 미측정 내부 가정이다. 첫 실험은 예상 RPS를 성과로 상정하지 않고 아래 축으로 용량을 탐색한다.
| 축 | 초기 시험값 | 의미 |
|---|---|---|
| 동시 추론 | 1 → 2 → 5 → 10 → 20, 필요 시 20% 단위 추가 |
C_knee 탐색 |
| worker | 1 → 2 → 4 |
처리량 증가와 모델 복제 메모리 비교 |
| 파일 | 0.5 MB·2 MB·5 MB | 업로드 제한 확정 전 탐색용 픽스쳐 |
| 해상도 | 1,024·2,048·4,032 px 긴 변 | 디코딩·전처리 비용 분리 |
| 포맷 | JPEG·PNG·손상 파일 | 정상·예외 경로 비교 |
이 값은 제품 제한이 아니라 초기 부하테스트 픽스쳐다. 실제 업로드 정책이 확정되면 그 상한값으로 교체한다.
③ 부하 특성
이 기능은 다음 세 가지 부하가 연속된다.
- 네트워크·버퍼 부하: 파일 바이트와 동시 업로드 수에 비례한다.
- CPU·메모리 부하: 디코딩·리사이즈·모델 추론 시간에 좌우된다.
- 상태 점유 부하: 모델은 worker에 상주하고 대기 요청은 queue에 머물러 요청 종료 전까지 메모리를 점유한다.
독립변수는 worker 수·동시 추론·이미지 크기·cold/warm이다. 대조 실험에서는 모델 버전, CPU 스레드 수, 이미지 포맷, 호스트 스펙을 고정한다.
④ 점유하는 인프라 자원
이미지 업로드
→ Nginx: request body buffer·network·임시 파일
→ Spring: 인증·권한 스레드, FastAPI outbound connection, timeout 타이머
→ FastAPI: request buffer, 이미지 디코딩 메모리, worker queue
→ 모델: worker별 상주 RSS, 추론 CPU, 임시 tensor·image buffer
→ Spring·MySQL: 결과 멱등성 확인·저장
자원 한도를 변경하며 측정하기 위해 FastAPI 컨테이너에 0.5·1·2 vCPU와 1·2·4 GiB의 실험용 제한을 적용한다. 이 값은 운영 스펙이 아니며, 모델 상주 메모리와 CPU 민감도를 찾기 위한 treatment다.
⑤ 장애 가설
| 가설 | 원인 지표 | 사용자 영향 |
|---|---|---|
| worker CPU 포화 | FastAPI CPU·run queue·queue wait 증가 | 판단 지연·timeout |
| worker별 모델 복제로 OOM | worker 수에 비례한 RSS, container OOM | AI 요청 5xx·재시도 |
| 동기 추론이 event loop 차단 | event-loop lag·active request 증가 | 가벼운 health·다른 AI 요청까지 지연 |
| Spring이 FastAPI를 동기 대기 | outbound pending·Tomcat busy 증가 | 팟 조회·참여 API로 장애 전파 |
| timeout 후 중복 재시도 | 동일 request_id의 여러 실행 | 중복 결과·불필요한 부하 증폭 |
FastAPI가 실패해도 이미지 판단만 수동 입력으로 축소되어야 하며, 팟 조회·참여·채팅을 함께 중단시켜서는 안 된다.
⑥ 측정 목적과 관찰 지표
| 구분 | 지표 | 통과·중단 기준 |
|---|---|---|
| 사용자 SLI | 성공률, end-to-end p95·p99, timeout·fallback·재시도율 | 실패율 1% 초과 시 해당 단계 중단 |
| FastAPI | active worker, queue depth·wait, CPU, RSS, event-loop lag, OOM | RSS가 limit 90% 초과하거나 OOM 시 중단 |
| 모델 | cold-start, 전처리·추론 p95, 요청당 CPU-ms, worker별 RSS | 처리량과 자원 단가 산출 |
| Spring 간섭 | outbound active·pending, Tomcat busy, 핵심 API p95·오류율 | control 대비 p95 +20% 미만, 오류율 +0.5%p 미만 |
| 정합성 | request_id별 모델 실행·DB 저장 횟수 | 중복 저장 0건 |
모델 정확도·confidence threshold는 용량 판정과 분리한 품질 검증에서 측정한다.
⑦ 측정 설계
| 단계 | 목적 | 조건 | 기록할 결과 |
|---|---|---|---|
I0 |
기준선 | 모델 로드 전·후 무부하 5분 | FastAPI 기본 RSS·CPU, 모델 상주 메모리 |
I1 |
cold-start | FastAPI를 재시작한 후 첫 요청을 5회 반복 | 로드 시간, 첫 요청 p50·max |
I2 |
이미지 단가 | worker 1·동시성 1, 파일·해상도 축만 변경 | 전처리·추론·전체 시간, peak RSS |
I3 |
C_knee |
worker 1·warm, 동시성 1→2→5→10→20 | 성공률, queue wait, CPU·RSS, p95·p99 |
I4 |
worker 비교 | I3 임계 근처에서 worker 1·2·4 |
안전 처리량 증가율, worker별 RSS |
I5 |
간섭 | 조회 91 RPS 또는 행사 180 RPS + C_safe 이미지 부하 |
핵심 API p95·오류율 변화 |
I6 |
예외·복구 | 크기 초과·손상 파일·FastAPI kill·timeout·중복 request_id | 4xx 즉시 거부, fallback, RTO, 중복 저장 |
I7 |
soak | 예상 peak 또는 C_safe로 60분 |
RSS 증가 기울기, queue 회복, 열화 여부 |
I3·I4·I5의 임계 근처는 각각 3회 반복한다. 이미지 데이터는 매 요청마다 다른 파일을 사용해 OS·라이브러리 캐시가 성능을 과도하게 높이지 않게 한다.
⑧ 측정 결과와 신뢰 범위
현재는 미측정 상태다. 측정 후 아래 표를 채우고, 원본 결과 JSON·Prometheus 측정창·모델 버전을 함께 남긴다.
| 조건 | 실제 처리률 | p95·p99 | 실패율 | queue wait | FastAPI CPU·RSS | 핵심 API 영향 | 판정 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| worker 1, C_knee | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | — |
| 선택 worker, C_safe | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | — |
| C_safe + 91 RPS | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | — |
| C_safe + 180 RPS | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | — |
C_knee는 SLI·실패율·메모리 중 하나가 처음 깨지는 동시성이다. C_safe는 3회 반복으로 확인한 C_knee × 0.7로 시작하되, soak에서 누수·queue 미회복이 보이면 더 낮춘다.
⑨ 판정과 인프라 결정
| 관측 결과 | 결정 |
|---|---|
CPU가 먼저 포화되고 vCPU 증가 시 C_knee가 비례 증가 |
FastAPI 전용 자원 scale-up 후 재측정 |
| worker 증가에 따라 RSS만 선형 증가하고 처리량 향상이 적음 | worker 증설 중단, 모델 공유·실행 방식 최적화 |
| replica 증가에 따라 안전 처리량이 선형 증가 | FastAPI scale-out과 LB 도입 검토 |
| Spring outbound·Tomcat 대기로 핵심 API가 20% 이상 악화 | queue 기반 비동기화, 짧은 timeout, 수동 fallback 적용 |
| FastAPI가 재시작하며 요청이 유실 | durable queue·작업 상태 영속화 필요성 검토 |
| 1개 replica가 중단되어도 AI 계속 제공이 필요 | 최소 2개 replica를 두고 N-1 C_safe로 수량 산정 |
8. 게시글 기반 혼잡도 판단
① 서비스 동작과 사용자 행동
혼잡도는 사용자 조회와 분리된 이벤트 처리 + 주기 집계 구조로 동작한다.
[게시글·댓글 이벤트]
게시글 생성
→ Spring Boot가 post_id·spot_id·content_version을 이벤트로 생성
→ FastAPI worker가 Spring 내부 API로 게시글·현재 댓글을 조회
→ 외부 LLM에 중간 혼잡도 판단 요청
→ post_id:content_version을 키로 메모리 캐시에 저장
댓글 생성·수정
→ 동일 post_id를 60초 debounce
→ 마지막 content_version으로 1회만 재판단
[20분 주기 거점 집계]
거점별 중간 결과를 읽음
→ 거점별 최종 혼잡도를 판단
→ spot_id:window_start를 멱등키로 Spring Boot에 전송
→ Spring Boot가 최신 결과와 calculated_at을 저장
→ 사용자 조회는 저장된 최신 값을 읽음
60초 debounce는 부하테스트용 초기 업무 가정이다. 사용자가 필요로 하는 정보 신선도에 따라 제품 정책으로 재조정한다.
② 예상 트래픽 지표
이벤트 요청률
= (신규 게시글 + debounce 후 댓글 재판단) ÷ 집중 시간(초)
주기 집계 필요 처리률
= 집계 대상 거점 수 ÷ 배치 허용 시간(초)
캐시 최대 항목 수
= 이벤트 요청률 × TTL(초)
운영 데이터 전의 초기 시험 범위는 다음과 같다.
| 축 | 기준·탐색값 | 상태 |
|---|---|---|
| 거점 수 | 6 → 30 → 100 |
6개는 V2 기준, 30·100은 용량 탐색 |
| 이벤트율 | 1 → 5 → 10 → 20 events/s |
운영 예상치가 아닌 C_knee 탐색 |
| 거점별 캐시 항목 | 10 → 100 → 1,000 |
20분 집계의 fan-in 비용 비교 |
| 텍스트 크기 | 1 KB·10 KB·50 KB | prompt 크기·직렬화·네트워크 비용 분리 |
| LLM 지연 | 50·200·1,000·3,000 ms | 외부 대기 민감도 |
| LLM 오류 | 0·1·5·20%, 429 | retry·circuit breaker 검증 |
갱신 주기가 20분이므로 초기 내부 기준은 거점의 95% 10분 이내, 100% 20분 이내 완료로 둔다. 제품 SLO가 확정되면 그 값으로 교체한다.
③ 부하 특성
- 이벤트 부하: 게시글·댓글 발생률에 따라 지속적으로 들어오는 작은 요청이다.
- 외부 대기 부하: LLM 처리 중 FastAPI worker·outbound connection이 점유된다.
- 캐시 상태 부하: 20분 윈도우 동안 중간 결과가 메모리에 누적된다.
- 주기 burst: 20분 경계에 여러 거점이 동시에 시작하면 순간 LLM·CPU·callback 부하가 만들어진다.
독립변수는 events/s·텍스트 크기·LLM 지연·거점 수·캐시 항목 수다. 대조 실험에서는 FastAPI worker 수·재시도 정책·TTL·집계 로직을 고정한다.
④ 점유하는 인프라 자원
| 구간 | 실행 경로 | 점유 자원 |
|---|---|---|
| 이벤트 생성 | Spring 업무 트랜잭션 후 이벤트 생성 | Spring thread, 이벤트 전달 통로 |
| 본문·댓글 조회 | FastAPI → Spring 내부 API → MySQL | FastAPI outbound, Spring thread·HikariCP, MySQL read, JSON 직렬화 |
| LLM 요청 | FastAPI → 외부 LLM | worker, outbound pool, DNS·TLS, timeout timer, queue |
| 중간 결과 | FastAPI in-memory cache | process RSS, Python object overhead, TTL·eviction |
| 20분 집계 | scheduler → 거점별 fan-in → 최종 판단 | scheduler lock, CPU, LLM concurrency, temporary payload |
| 최종 반환 | FastAPI → Spring callback → MySQL | HTTP connection, 멱등키, HikariCP·MySQL write |
⑤ 장애 가설
| 가설 | 원인 지표 | 사용자·시스템 영향 |
|---|---|---|
| LLM 지연·429 | outbound pending·queue depth·retry 증가 | 20분 집계 미완료·stale 결과 |
| retry storm | 원본보다 retry RPS가 커짐 | 외부 장애가 내부 queue 포화로 확대 |
| 메모리 캐시 유실 | FastAPI restart 후 cache item 0 | 해당 윈도우의 결과 품질 하락·RPO 발생 |
| replica별 캐시 불일치 | 동일 spot의 항목 수·결과 불일치 | replica에 따라 다른 최종 판단 |
| scheduler 중복 실행 | 동일 spot_id:window_start 여러 번 시작 | LLM 비용·부하 증가, callback 중복 |
| FastAPI 대기가 Spring으로 전파 | Tomcat busy·outbound pending 증가 | 팟 조회·참여·채팅 지연 |
⑥ 측정 목적과 관찰 지표
| 구분 | 지표 | 통과 기준 |
|---|---|---|
| 최종 SLI | 거점별 완료율, batch duration, result age·staleness | 95% 10분 이내, 100% 20분 이내 |
| 이벤트 | produced·consumed·failed·duplicated, end-to-end lag | 누락·중복 반영 0건 |
| LLM | latency p95·p99, timeout·429·5xx, active·pending, retry/original ratio | retry가 원본 부하의 1배를 초과하지 않음 |
| 캐시 | item·bytes, spot별 item, TTL·eviction, restart 유실량 | eviction으로 필요 윈도우 데이터가 사라지지 않음 |
| 장애 격리 | 핵심 API p95·오류율, Spring Tomcat·HikariCP | control 대비 p95 +20% 미만, 오류율 +0.5%p 미만 |
| 복구 | FastAPI restart RTO, 재시작 후 첫 정상 집계, cache RPO | 정의한 RPO 범위 안에서 재생성 가능 |
결과 age ≤ 20분은 fresh, 20분 < age ≤ 40분은 stale, age > 40분은 unavailable로 표시하는 것을 초기 UI·운영 기준으로 둔다. 이 값도 제품 SLO가 확정되면 교체한다.
⑦ 측정 설계
| 단계 | 목적 | 조건 | 기록할 결과 |
|---|---|---|---|
C0 |
기준선 | AI 처리 없이 조회 91 RPS | 핵심 API p95·p99·오류율 |
C1 |
이벤트 단가 | LLM 50 ms·오류 0%, 1→5→10→20 events/s | event lag, queue, 요청당 CPU·RSS |
C2 |
외부 지연 | C1 안전 이벤트율에서 LLM 200→1,000→3,000 ms |
outbound pending, queue 성장 기울기 |
C3 |
오류·재시도 | LLM 오류 1·5·20%·429, retry 최대 1회 | retry 증폭, circuit open, 누락 |
C4 |
배치 단가 | 거점 6·30·100 × 항목 10·100·1,000 | batch duration, LLM concurrency, peak RSS |
C5 |
동시 최악 | C1 이벤트 + C4 배치 + 조회 91·180 RPS |
핵심 API 간섭, 20분 내 완료율 |
C6 |
스케줄러 | 모든 거점 동시 시작 ↔ 20분 안에 jitter 분산 | peak concurrency·CPU·갱신 시간 비교 |
C7 |
재시작·RPO | 윈도우 25%·50%·90% 지점에서 FastAPI 중단 | cache 유실량, 최종 결과 품질, 재생성 시간 |
C8 |
replica 정합성 | FastAPI 2개에 이벤트를 분산하고 동일 spot 집계 | 항목 수·결과 일치, scheduler 중복 |
C1·C4·C5의 임계 근처는 3회 반복한다. 외부 LLM 실패를 재현할 때는 실제 제공자에 오류를 발생시키지 않고 고정 지연·오류율을 내는 stub을 사용한다.
⑧ 측정 결과와 신뢰 범위
현재는 미측정 상태다. 측정 후 아래 표를 조건별로 채우고 LLM stub 설정·캐시 항목 수·이벤트 원본 로그를 함께 남긴다.
| 조건 | event lag p95 | batch duration | 20분 완료율 | LLM 오류·retry | cache peak | 핵심 API 영향 | 판정 |
|---|---|---|---|---|---|---|---|
| 기준 6거점 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | — |
| 30거점 stress | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | — |
| 100거점 capacity | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | — |
| FastAPI restart | 미측정 | 미측정 | 미측정 | 미측정 | 유실량 미측정 | 미측정 | — |
이 결과로 확정할 수 있는 것은 용량·장애 격리·RPO이다. LLM 판단 정확도나 실제 제공자의 미래 요금·제한은 이 결과로 보증하지 않는다.
⑨ 판정과 인프라 결정
| 관측 결과 | 결정 |
|---|---|
| 외부 LLM 지연·429에서만 포화 | EC2 scale-up 대신 bounded queue·timeout·지수 backoff·circuit breaker |
| 동시 집계는 실패하고 jitter는 20분 내 완료 | 거점별 시작 시점 분산, 동시 scheduler 제거 |
| 재시작 후 윈도우 결과를 복구할 수 없음 | 중간 결과를 DB·Redis·durable queue 등 프로세스 밖으로 이동 |
| 2개 replica의 캐시가 달라 결과 불일치 | 공유 상태 도입 또는 spot_id 기반 파티셔닝·싱글 scheduler 소유권 적용 |
| callback 중복이 최종 값을 중복 저장 | spot_id:window_start로 unique·upsert 멱등성 보장 |
| AI 처리가 핵심 API를 20% 이상 악화 | FastAPI를 별도 호스트로 격리하고 Spring의 동기 대기를 제거 |
| 갱신 실패가 있어도 age 40분 이내 이전 결과를 제공 | UI에 calculated_at·stale 표시, 팟 조회·매칭·채팅은 계속 제공 |
9. 장애 범위와 가용성 기준
성능 용량과 가용성은 다른 문제다. 단일 서버가 목표 RPS를 처리해도 하나의 EC2·MySQL·Nginx가 중단되면 서비스 전체가 중단될 수 있다.
기능별 가용성 등급
| 등급 | 기능 | 장애 시 원칙 |
|---|---|---|
| 핵심 | 로그인, 팟 조회·참여, 좌석 정합성, 채팅·이력, 기본 정산 | SLO·정합성 유지, 부분 장애가 전체로 전파되지 않게 함 |
| 부가 | 혼잡도 AI, 이미지 판단 | stale 결과·수동 입력·재시도로 기능 축소 |
장애 범위별 검증
| 장애 대상 | 주입 방법 | 기대하는 축소 동작 | 통과·기록 기준 |
|---|---|---|---|
| FastAPI·경량 모델 | C_safe 중 FastAPI 정상 종료·강제 종료 |
AI만 실패하고 수동 입력 제공 | 핵심 API p95 +20% 미만·오류율 +0.5%p 미만, FastAPI RTO 기록 |
| 외부 LLM | stub에 3,000 ms·429·5xx를 10분간 주입 | bounded retry 후 circuit open, 이전 혼잡도 제공 | retry/original ≤ 1, queue가 무한 증가하지 않음, calculated_at·stale 표시 |
| FastAPI 메모리 캐시 | 20분 윈도우 25%·50%·90%에서 재시작 | 재생성 가능한 결과는 다시 생성, 불가능하면 RPO로 기록 | 유실 항목·거점 수, 최종 집계 완료율, 재생성 시간 |
| Spring Boot | 조회 91 RPS·WS 유지 중 컨테이너 재시작 | 재시작 후 정상 요청 복구, 채팅 재접속·이력 복구 | 감지·재시작·readiness·사용자 복구 시점별 RTO |
| MySQL | 테스트 환경에서 MySQL 중단 후 백업으로 복구 | 쓰기 차단·명확한 오류 응답, 복구 후 정합성 검사 | 백업 기준 RPO, 복구 RTO, 팟·참여·채팅·정산 대사 |
| Nginx | 업스트림은 유지한 채 Nginx 재시작 | 신규 요청만 잠시 실패하고 자동 복구 | HTTPS·WSS 각각의 실패창, readiness 복구 시간 |
| EC2 | 동일한 테스트 환경에서 인스턴스 중단 | V1은 전체 중단을 수용, V2 HA는 다른 인스턴스로 우회 | V1 전체 RTO, V2 failover RTO, N-1 상태 SLO |
장애 실험은 다음 순서로 진행한다.
- 개발·부하테스트 전용 환경인지 확인한다.
- 백업·복구 절차와 중단 조건을 먼저 확인한다.
- 장애 전 5분 control을 측정한다.
- 계획 peak 또는
C_safe에서 장애를 주입한다. - 장애 감지, 트래픽 차단·우회, 프로세스 복구, readiness, 사용자 정상화 시점을 각각 기록한다.
- 복구 후 10분간 SLI·정합성·미처리 queue를 확인한다.
가용성 결정 표
| 질문 | 예 | 결정 |
|---|---|---|
| AI 장애 중 수동 fallback을 허용하는가? | 허용 | FastAPI는 부가 기능 장애 범위로 격리 |
| 호스트 장애 중에도 팟 조회·참여·채팅을 제공해야 하는가? | 필요 | 최소 2개 스탯리스 인스턴스 + LB, N-1 용량 확보 |
| DB 장애 중 자동 failover가 필요한가? | 필요 | Multi-AZ 또는 동등한 복제·failover 구성 |
| 혼잡도 중간 결과 20분치 유실을 허용하는가? | 불허 | in-memory 단독 저장 제거, 프로세스 밖의 durable state 사용 |
| 부가 AI 기능도 중단 없이 제공해야 하는가? | 후속 결정 | 필요하면 FastAPI N+1, 아니면 수동·stale fallback 유지 |
사업 목표가 호스트 장애 중에도 핵심 여정을 계속 제공하는 것이라면 scale-up만으로는 충족할 수 없다. 최소 2개의 스탯리스 업스트림과 로드밸런서, 프로세스 밖으로 이동한 공유 상태가 필요하다. DB 장애 중 자동 failover가 필요하면 Single-AZ RDS만으로는 목표를 충분히 설명할 수 없으므로 Multi-AZ 또는 동등한 복구 구성을 별도로 검토한다.
V1 한계 지점과 scale-out · scale-up 결정
컴포넌트별 한계와 확장 방향
| 순위 | 최초 포화 자원 | 실측 임계값 | V2 요구 수준 | 충족 | 확장 방향 |
|---|---|---|---|---|---|
| 1 | WebSocket 동시 연결 | 약 1,700개 | 1,800개 | ❌ | 원인 규명 후 WS 분리 또는 진입 계층 조정 |
| 2 | Spring JVM heap·GC (REST·WS 공유) | WS 1,642개에서 조회 p99 27배 | 1,800 연결 + 180 RPS 동시 | ❌ | REST·WS 자원 격리, 이후 각각 scale-out 검토 |
| 3 | HikariCP 공용 풀 10개 | 저장 47 msg/s, 동시 승인 20건 | 36 msg/s, 동시 승인 재산정 필요 | ❌ | 풀 분리 + 저장 비동기화 |
| 4 | 동일 팟 row lock | 동시 50건에서 실패율 59.5% | — | ❌ | 좌석 차감 구조 변경 |
| 5 | Nginx 연결 수립 처리 | 200 conn/s에서 p95 3.57초 | 6.00 conn/s | ✅ | 조치 불필요 |
| 6 | Spring CPU quota (0.75) | 641 RPS에서도 throttling 0 | 91 RPS (행사 180 RPS) | ✅ | 조치 불필요 |
| 7 | MySQL CPU (0.5) | 274 RPS 조회에서 배정량의 21% | — | ✅ | 조치 불필요 |
| 8 | FastAPI 이미지 추론 | 미측정 | V2 이미지 요청률 확정 필요 | ❓ | 동시 추론 C_knee·worker별 메모리 측정 후 결정 |
| 9 | FastAPI·LLM 혼잡도 파이프라인 | 미측정 | 게시글 이벤트 + 20분 집계 | ❓ | queue·공유 상태·fallback 측정 후 결정 |
장애 전파 경로 (실측값 부착)
[행사 유입] [인기 팟 마감] [조회 증가]
WS 연결 약 1,700개에서 포화 동시 승인 20건 초과 641 RPS까지 여유
│ handshake 30초 타임아웃 │ 풀 10개 고갈 │ throttling 0
↓ ↓ ↓
같은 JVM heap·GC 압박 공용 HikariCP 10개 (병목 아님)
GC pause 3.1 → 257 ms │
│ ├─ 좌석 승인 실패 59.5%
↓ ├─ 조회 p99 48배
조회 p99 27배 · 오류 4.54% └─ 채팅 저장 오류 1~3/s
│
[채팅 저장 36~47 msg/s] ────────────────┘
풀 active 10/10 · 대기 53 → 조회 p95 13.6배 · 실패 1.53%
세 시나리오가 서로 다른 입구로 들어와 같은 두 자원(Spring JVM, HikariCP 10개) 에서 만난다. 이것이 V1 구조의 핵심 한계다.
확장 용어와 판정 원칙
| 구분 | 의미 | 이 문서의 예 |
|---|---|---|
| scale-up | 단일 인스턴스의 CPU·메모리·I/O 증가 | t3.medium → 상위 스펙, FastAPI 전용 인스턴스 상향 |
| 자원 격리 | 서로 다른 부하를 프로세스·컨테이너·호스트로 분리 | REST·WebSocket·FastAPI 분리 |
| scale-out | 동일 역할의 스탯리스 replica 증가 | REST replica N개, FastAPI replica N개 |
| 고가용성 | 인스턴스·AZ 장애 중에도 계속 제공 | 다중 EC2 + LB, DB failover |
| 구조 개선 | 자원 포화 원인 제거 | 채팅 저장 비동기화, 원자 카운터, 풀 분리 |
scale-up으로 완화되는 것과 해결되지 않는 것
scale-up(인스턴스 상향)의 적용 범위
- 단순 조회에서 CPU 여유가 있었다. 조회 641 RPS(기준 91 RPS의 7배)에서 backend CPU 0.148 cores, throttling 0, 호스트 CPU 27.8%였다. 따라서 단순 조회량만을 근거로 인스턴스를 상향할 필요는 없다.
- 연결 상한 1,700개가 메모리·CPU로 설명되지 않는다. 연결당 heap 28.2 KB 기준 수용량은 약 11,800개, CPU 사용률은 2.2%였다. 자원을 키우는 것이 상한을 올린다는 근거가 없다.
- 공유 자원 경합은 인스턴스 크기만의 문제가 아니다. 메모리 증설은 GC 임계점과 OOM 시점을 뒤로 미룰 수 있지만, REST·WebSocket의 같은 JVM과 조회·채팅·좌석 경로의 공용 풀을 없애지는 못한다.
- 따라서 scale-up은 임계점을 이동시키는 단기 완화책으로 A/B 측정할 수 있지만, 장애 격리·단일 장애점·무중단 목표를 해결하는 최종 조치로 보지 않는다.
자원 격리·scale-out·고가용성이 해결하는 것
| 조치 | 해결하는 한계 | 근거 |
|---|---|---|
| REST와 WebSocket 프로세스 분리 | JVM heap·GC 공유로 인한 조회 p99 27배·오류 4.54% | 4절 |
| 채팅 저장 비동기화 (큐 + 배치 insert) | 커넥션 점유시간으로 인한 풀 포화, 조회 p95 13.6배 | 3절 |
| 읽기·쓰기 커넥션 풀 분리 | 쓰기 경합이 조회를 잠식하는 전파 경로 | 3·5절 |
| 좌석 차감 구조 변경 (원자 카운터 + 멱등키) | 동시 20건부터의 풀 고갈, 실패율 59.5~79.9% | 5절 |
| MySQL 분리 (별도 호스트 또는 RDS) | 컨테이너 메모리·I/O 상호 간섭, 단일 장애점 | 6절 |
| 다중 EC2 + 로드밸런서 | 단일 장애점, 무중단 배포 | 6절 |
V2 확장 설계 결정
아래 순서로 한 번에 하나만 적용하고, 같은 시나리오·같은 판정 기준으로 재측정한다.
| 순위 | 결정 | 근거 지표 | 재검증 기준 |
|---|---|---|---|
| 1 | REST와 WebSocket 프로세스 분리 | 조회 p99 27배, GC pause 82배 (4절) | 분리 후 조회 p95 악화 20% 미만 |
| 2 | 채팅 저장 비동기화 | 풀 active 10/10, 대기 53, 획득 p95 445 ms (3절) | 동일 47 msg/s에서 풀 대기 0 |
| 3 | 읽기·쓰기 풀 분리 | 조회 p95 13.6배, p99 48배 (3·5절) | treatment 대비 악화 20% 미만 |
| 4 | 좌석 차감 구조 변경 | 동시 50건 실패율 59.5% (5절) | 동시 100건에서 실패율 0%, 정합성 유지 |
| 5 | MySQL 분리 및 다중 EC2 + LB | 단일 장애점, 컨테이너 메모리 75~85% (6절) | 컨테이너 단위 장애 시 RTO 측정 |
| 6 | 연결 상한 1,700개 원인 규명 후 결정 | heap·CPU로 설명 불가 (2절) | 동시 연결 1,800개 달성, 오류 0% |
| 7 | FastAPI 이미지 처리 자원 예산 확정 | V1 CPU 예산 2.00 vCPU 전부 배분, AI 경로 미측정 (7절) | 계획 peak에서 AI SLO 충족, 핵심 API 지연 악화 20% 미만 |
| 8 | 혼잡도 queue·캐시·fallback 결정 | 20분 집계, LLM 지연·429, in-memory 유실 위험 (8절) | 20분 내 완료율·staleness·재시작 RPO/RTO 목표 충족 |
| — | 조회량을 근거로 한 인스턴스 상향 | 641 RPS에서 throttling 0 (1절) | 근거 없음, 보류 |
다음 인프라 스펙 산정 방법
다음 인프라는 임의의 인스턴스 유형을 먼저 선택하지 않고 실측된 안전 용량에서 역산한다.
C_knee = SLO 또는 정합성이 처음 깨지는 지속 부하
C_safe = 반복 측정으로 확인한 C_knee × 안전 계수
필요 replica = ceil(계획 peak ÷ replica 1개의 C_safe)
장애 중에도 peak를 처리해야 하면 정상 상태가 아니라 N-1 상태의 합산 C_safe로 replica 수를 결정한다. 이때 다음 제약도 함께 반영한다.
- 메모리 = 기본 RSS + worker별 모델 복제량 + peak 요청·캐시 + 운영 여유량
- DB 커넥션 = 모든 replica의 pool 합계 + 운영·배치 예비분 ≤ DB 허용 연결 수
- 20분 배치 = 거점 수 ÷ 배치 허용 시간이 안전 처리량 이하
- 외부 LLM = 호출 제한·예산·timeout·retry 정책을 만족
C_knee에 도달하지 못한 실험은 용량 상한을 증명하지 않는다. 그 경우 결과는 **"적어도 실측 최대치까지 통과"**라는 하한값으로만 기록한다.
측정의 신뢰 범위
결론의 적용 범위를 함께 남긴다.
측정 대상과 실제 서비스의 차이
측정은 실제 moyeota/be 이미지가 아니라 동일 자원 배분·동일 설정의 합성 픽스처로 수행했다.
- fan-out이 없다. 발신자에게만 ACK를 보내므로 4인 팟에서 3명에게 전달되는 비용(3절 추정 3배)은 미측정이다.
- 방(room) 개념이 없어 "한 방 집중 ↔ 여러 방 분산" 축을 측정하지 못했다.
- 인증·권한 검증이 없어 실제 토큰 검증 CPU가 빠져 있다.
- 혼잡도 외부 API(FastAPI·LLM) 호출 경로가 없다.
- 조회 endpoint가 실제 쿼리보다 단순하다. 1절의 절대 RPS는 상한 보증값이 아니라 "조회가 1차 병목이 아니다"의 근거로만 사용한다.
- 트랜잭션·lock 보유 시간은 각각 50 ms·100 ms의 합성 지연이다.
측정창과 대조군의 약점
- 시나리오당 75~150초, 반복 1회다. 동일 조건 반복의 CPU 차이가 8%였으므로 30% 미만 차이는 판정에 쓰지 않았다.
- 30분 유휴 유지와 soak 테스트가 없어 연결 누수와 완만한 heap 증가는 미검출이다. 컨테이너 메모리가 세션 전반에서 574 → 652 MB로 상승했으나 누수로 단정할 수 없다.
- 메시지율 축이 완전히 고정되지 않았다. 연결 1,800 시나리오는 목표 120 msg/s에 대해 42~47 msg/s만 달성했다.
- 분산 팟 대조군은 팟이 2개뿐이어서 동시 50건이 25건씩 두 행에 나뉘었다. 대조가 약하다.
- CPU credit 잔량을 기록하지 못해 세션 후반 시나리오의 credit 영향을 확인할 수 없다.
- 서버 설정 축(풀 10↔20,
-Xmx, 세션 상태 크기)은 재배포가 필요해 이번 창에서 다루지 않았다.
계측 오류로 결론에 사용하지 않은 값
| 값 | 문제 | 대응 |
|---|---|---|
| 서버 측 HTTP 응답시간 분위 | 거의 모든 구간에서 0.95 ms로 고정 → 히스토그램 분위 산정 오류 | 지연은 k6 클라이언트 값만 사용 |
| Nginx 컨테이너 CPU | 전 구간 0.0004 cores 고정 → 시리즈 매칭 오류 의심 | 2절에서 Nginx를 원인으로 지목하지 않음 |
| 채팅·좌석 오류 카운터 | 15~20초 창에서 increase()가 0 반환 |
서버 5xx rate로 대체 |
| WebSocket 오류율 53~57% | 실패 후 재접속을 반복해 분모가 커짐 | 동시 연결 최대값을 1차 근거로 사용 |
다음 측정 창의 과제
- 연결 상한 1,700개의 원인 규명 — Nginx 메모리·CPU, listen queue overflow 관측, Nginx 우회 직접 연결 비교, 발생기 2대 분산
- 이미지 처리 FastAPI 용량 탐색 — worker 1·2·4, cold·warm model, 이미지 크기, 동시 추론
C_knee - 혼잡도 이벤트·20분 집계 부하 — 게시글·댓글 이벤트율, 거점 수, 캐시 크기, LLM 지연·429·5xx
- AI 장애 격리 — FastAPI 재시작, 메모리 캐시 유실, stale 표시, 수동 fallback, 핵심 API 영향
- fan-out 비용 (수신자 1·3·9명)
- 행사 burst 삼각형 부하 180 RPS
- 컨테이너·EC2·MySQL 장애 복구 RTO/RPO와 N-1 용량
- 풀 크기 10↔20,
-Xmx및 인스턴스 메모리 상향 대조 실험