Repository navigation
perf: DAU 2만 부하테스트 — 처리량 1.55배·p95 6초→1.3초, 장애 전파 차단 - #326
Merged
Merged
Conversation
켜 두면 첫 트랜잭션이 잡은 커넥션을 요청이 끝날 때까지 쥐어, 트랜잭션을 쪼갠 외부 호출 동안에도 풀을 차지한다. 부하테스트(DAU 2만 시드)에서 가입 60건이 몰리자 닉네임 심사(Gemini ~1초) 내내 커넥션이 묶여 풀 15개가 다 찼고, RDS CPU 16% 인데 다른 API 까지 30초 대기 뒤 500 이 났다. 시험은 심사 호출 중 이 스레드가 커넥션을 쥐지 않는지 본다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/actuator/health 가 DB 검사를 하면 커넥션 풀이 잠깐 차기만 해도(대기 30초 > 헬스체크 5초·3회) 모든 태스크가 동시에 unhealthy 로 교체된다 — 부하테스트 가입 몰림에서 태스크 2대가 함께 내려가 502 가 났다. Redis 와 같은 이유로 뺀다. DB 상태는 RDS·풀 지표 경보로 본다(DB_HEALTH_CHECK=true 로 되돌릴 수 있다). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- findAllById 가 사용자마다 개인정보·관심사(EAGER)를 따로 읽어 방 홈·랭킹·멤버·스레드가 멤버 수 × 2 문장을 더 냈다 (트레이스: 멤버 3명 방 홈 16문장). fetch join 으로 한 문장. - 방 홈이 ACTIVE 멤버를 자기와 랭킹 계산에서 두 번 읽었다 — 읽은 목록을 넘긴다. - 로그인 분기는 기존 회원 id 만 읽는다(세션 트랜잭션이 잠그며 다시 읽는다). 같은 설치로 다시 로그인하면 설치 보유자 조회를 건너뛴다 (활성 보유자는 UNIQUE 로 하나뿐). 로그인 users 조회 3번 → 1번. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
탈퇴 제외·심사 중 조건을 원래 열에 걸어 가입·닉네임 확인마다 사용자 수만큼 행을 검사했다(느린 쿼리 로그: 7,676행, 부하 중 3초). 같은 규칙의 생성 열(active_requested_nickname·active_approved_nickname)과 UNIQUE 인덱스가 이미 있어 그 열로 찾는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
이탈 트랜잭션마다 전역 락을 잡고 진행 중인 방 수(수천)만큼 UPDATE 를 따로 보냈다. 창 함수로 모드마다 두 문장, 스냅샷에 없는 방(시작 전 등)이면 전역 락도 잡지 않는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
이벤트마다 공용 비동기 실행기(8스레드·대기열 무제한)에 쌓여, 인기 방 가입·확정이 몰리면 같은 방 재계산 여럿이 커넥션을 쥔 채 방 행 잠금을 줄 서 기다렸다. 도는 동안 들어온 요청은 끝난 뒤 한 번 더(그사이 커밋을 모두 본다), 전용 4스레드. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
후보 방마다 HGETALL 을 따로 보내 한 페이지에 최대 100번 왕복했고, 그동안 읽기 트랜잭션의 커넥션을 쥐었다. 파이프라인으로 묶는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
어제·오늘을 평가하며 귀속일마다 앞뒤 하루를 읽어 겹치는 날을 다시 읽었다(트레이스: sync 한 번 18문장 → 12문장). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SDK 기본은 호출 시간 제한이 없어 SQS·S3 가 느려지면 요청(과 커넥션)이 끝없이 붙잡힌다. 알림 보내기 3초(소비자 롱폴링은 그대로), S3 15초(시도당 8초). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 고스트 푸시(1시간): joined_at 순으로 멤버 전체를 FOR UPDATE 로 훑어(REPEATABLE READ 라 훑은 행이 잠김) 그대로 FCM 을 최대 200건 보냈다. (status, setup_status, joined_at) 인덱스로 대상만 읽고, 푸시는 커밋 뒤에 보낸다. - 인기 점수 배치(5분)·일별 지표: 최근 24시간 가입 사건을 시각 범위로 세는데 시각 인덱스가 없어 누적 사건 전체를 훑었다 (느린 쿼리 로그: 3.3만 행, 부하 중 4초). (joined_at, challenge_id) 인덱스. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DAU 2만 재측정용 — 홈·방 홈·랭킹·생성·가입·AI 초안 p95 목표, 저장·가입이 실제로 된 건수와 429/409 를 API·상태별로, steps(500→1000→1500→회복)·spike(30초에 3배), 한계점은 핵심 화면 p95 3초 초과로도 멈춤, 시드는 ROOM_EVERY 명 중 1명만 방 생성. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
초당 10건+10% 는 DAU 2만(평균 ~17 RPS)에서 월 ~2,700만 건 ≈ $135 로 ECS 비용보다 크다. 월 ~470만 건 ≈ $23. 분석용 표본으로는 충분하다(드문 오류·느린 요청은 로그·지표가 본다). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 인기 목록이 요청마다 인기 방 20곳의 ACTIVE 멤버를 모두 셌다(느린 쿼리 1위: 5,869행 × 678회/30분). 정원이 없거나 저장된 참여자 수가 정원보다 5 넘게 남은 방은 「마감 아님」이 확실하다 — 마감 근처 방만 원천에서 센다(가입은 여전히 잠금으로 판정). - 시작일 활성화·종료 완료 배치가 status+날짜 인덱스 없이 방 전체를 FOR UPDATE 로 훑어, 1분마다 모든 방 행이 잠겼다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5분마다 공개 방 전부의 challenge_stats 를 UPDATE 해(부하테스트: 수천 행, 느린 쿼리 81건) 통계 재계산 잠금과 부딪혔다. 지금 저장된 값과 같이 읽어 달라진 방만 고른다. popularity_updated_at 을 읽는 곳은 없다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
findById 로 없음을 확인한 뒤 save() 가 merge 로 같은 PK 를 한 번 더 SELECT 했다. 가입 1건에 동의 5종 × 2 + 소셜 토큰 2 = 12 SELECT → 6. Persistable 로 새 행임을 알린다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
빨라질수록 같은 사용자의 sync 가 10초 간격에 걸려 성공 건수가 줄어드는데, 허용 상태라 어디에도 안 잡혔다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
꽉 찬 인기 방은 Top 20 에 오래 머물러 요청마다 그 방 멤버 전체를 셌다 (단계 시험 느린 쿼리 1위: 5,200행 × 10,483회). 저장값은 늦으면 실제보다 적으므로 그 값으로 이미 찼다면 지금도 찼다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RDS 강제 장애조치(stg, 부하 중) 동안 요청이 커넥션을 30초씩 기다리다 500 으로 끝났다(55초간 수백 건). 커넥션 대기 상한을 5초로 줄이고(DB_CONNECTION_TIMEOUT_MS), 풀 대기 초과·연결 끊김은 503 으로 구분한다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
부하테스트 장애 주입(stg): Redis 재부팅으로 파생 인덱스가 비자, 새로 뜬 태스크가 30분짜리 재구성 잠금을 잡고 진행 중인 방 3.5만 개의 통계를 전부 다시 세다 RDS 장애조치에 걸려 사라졌다 — 탐색·인기 503 이 25분 넘게 이어졌다. - 재구성 잠금은 2분으로 잡고 도는 동안 30초마다 늘린다(주인일 때만). - 워밍업 재구성은 통계를 다시 세지 않고 지금 값으로 인덱스를 만든다. 통계는 MySQL 에 있어 Redis 유실과 무관하다. 대신 보정 기준 시각을 30분 앞에 남겨, 바로 이어지는 5분 보정이 유실 가능 구간만 증분으로 다시 센다. 전체 재계산은 03:30 대조가 그대로 맡는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
방 3.5만 개의 HMGET 을 파이프라인 하나에 실으면 응답 전체가 Redis 명령 제한 시간(2초)을 넘겨 재구성이 통째로 실패했다(부하테스트 2026-10 Redis 재부팅 복구, stg). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
트레이스 감사(2026-10)에서 DB 왕복의 ~10% 가 SET SESSION TRANSACTION READ ONLY/READ WRITE 였다. RDS 가 다음 병목이고 비용도 가장 커서 왕복을 줄인다. 쓰기 막기는 JPA readOnly(flush 끔)가 계속 맡는다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
탐색 투영의 원천 리비전 MAX(m.updated_at) 과 5분 통계 보정 대상 고르기가 방의 멤버 전체를 훑었다 — 방 5만 개에서 전수 조회가 56만 행·최대 74초(부하테스트 2026-10, RDS CPU 포화 구간). (challenge_id, updated_at) 인덱스를 둔다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
급증(500→1,500명)에서 요청 스레드 200개가 커넥션 15개를 기다리며 쌓여 쓰기 API p95 가 50초를 넘었고 ALB 가 60초에 끊어 504 가 났다. 받을 수 있는 만큼만 받아 받은 요청은 제 속도로 끝낸다. APP_HTTP_MAX_INFLIGHT(0=끔)·APP_HTTP_INFLIGHT_WAIT_MS 로 켠다. 헬스체크는 관리 포트라 영향 없다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 사용자 실행 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
가입부터 하면 가입 몰림이 풀 대기로 503 이 되어 정원 경합까지 가지 못했다(2차 J1: 300명 중 46명). HOT_USER_BASE(사용자 번호 시작)·START_DELAY_MS(setup 이 정한 공통 출발 시각). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test Coverage
|
필터에서 난 예외는 GlobalExceptionHandler 를 거치지 않고 /error 로 가는데 거기엔 인증이 없어 401 LOGIN_REQUIRED 가 됐다. 부하테스트(2026-10 J2, 300명 동시 참여)에서 풀 대기 5초 초과 49건이 전부 401 로 나갔다 — 앱은 로그아웃 화면으로 간다. 컨트롤러 안과 같게 503 으로 맞춘다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VU 하나가 사용자 한 명을 붙잡으면 그 사람 오늘 신호가 시험 내내 쌓여(2차 소크 뒤 ~160명이 하루 ~2,400건) sync 마다 전부 다시 판정하느라 요청당 CPU 가 1.7배가 됐다 — 실제 사용 패턴이 아니다. SYNC_POOL·SYNC_SALT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test Coverage
|
Test Coverage
|
2026-10-17 결정: 지금 prod 규모(CPU 평균 5%)에선 0.5 vCPU 를 유지하고, 지연·오류가 생기면 가장 먼저 CPU 를 올린다. 기본 실행은 헬스체크 관리 포트 분리·메모리 오토스케일 정책 삭제·X-Ray 규칙만 한다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sync 가 신호를 저장할 때 보관 기간 전체 날짜 범위로 「이미 받은 기록인가」를 묻는데, 기존 인덱스 앞 칸이 날짜 범위라 사용자 신호 전체를 훑었다(2차 P0 재측정 느린 쿼리 4,358건/30분, 스레드 덤프 sync 3위). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2차 P0 재측정(최종 코드, sync 사용자 순환) 스레드 덤프 71회에서 일하던 요청 스레드의 72% 가 sync 였고 대부분 DB 대기였다. 상위는 방마다 하는 챌린지 PK 조회(1위)와 그날·이웃 날 신호 전체 읽기(2위). 챌린지는 IN 한 번으로 묶고, 신호는 sync 한 번 동안의 읽기 창에 두어 확정된 날(어제가 이미 끝난 멤버뿐이면 그저께 하루치)을 읽지 않는다. 판정 결과는 그대로다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Test Coverage
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
요약
노션 「부하테스트 및 성능개선」 7~8절(DAU 2만 = 동시 ~1,000명, SCALE=2) 계획대로 stg 를 prod 와 같은 조건(1 vCPU/2GB, Hikari 15, RDS db.t4g.small Multi-AZ·TLS)으로 올려 시험하며 찾은 병목·과한 트랜잭션·장애 전파를 고친다. X-Ray 100% 규칙으로 전 API 96건의 트랜잭션을 수정 전후로 감사했다. 결과·원가·권고는 노션 9절.
결과 (SCALE=2 30분, 1 vCPU×2 고정, sync 사용자 순환, 같은 데이터)
¹ B8 은 직전 옛 코드 태스크가 남긴 30분 재구성 잠금 때문에 탐색·인기 목록 15% 가 503 으로 끝났다(그 몫을 뺀 유효 처리량). 그 밖의 API 는 정상.
커밋 (작업 단위)
APP_HTTP_MAX_INFLIGHTinfra/capacity/apply_prod_dau2.py— 헬스체크 관리 포트(8081)·메모리 정책 삭제·X-Ray, CPU 는--cpu줄 때만. main 머지 전에 사용자가 실행(온콜 중지 상태)main 머지 전·후
python3 infra/capacity/apply_prod_dau2.py(CPU 는 그대로)테스트
./gradlew test1,483건 전부 통과(sync 최적화 반영 후, 2026-10-17)🤖 Generated with Claude Code