2단계. v1의 부하테스트를 통한 한계점 파악 - 100-hours-a-week/KTB4-17th-wiki GitHub Wiki
결론: 사용자 시나리오 중심 테스트
부하테스트에는 DB의 연결과 CUD에 대한 검증이나 외부 서비스에 대한 연동, DB 연결과 조회에 대한 검증 등 여러 목적의 테스트가 존재합니다. 이번 v1배포에서는 개발 기간에 비해 볼륨이 전체의 70%로 api의 종류가 많기 때문에 세부적인 성능보다는 일단 사용자의 시나리오대로 잘 동작하는 것이 가장 중요하다고 판단했습니다. 따라서 설계에서 세웠던 목표에 따라서 사용자 시나리오를 작성해 사용자가 사용할때 성능이 잘 나오는지 판단했습니다.
- 부하 도구: k6
- 모니터링 도구: granfana, Prometheus, Loki
- 테스트용 별도의 서버 구성하고 dev.star23sharp.com으로 연결
- 테스트용 DB에 테스트 데이터 구성 : 1775명
도구는 k6를 선택했습니다. 시나리오를 JS로 작성할 수 있어서 비교적 직관적인 문법을 가지고 있었습니다. python이나 java에 특화된 도구들도 있지만 사용성을 먼저 생각했습니다. 무엇보다 초당 몇 건처럼 목표 RPS를 직접 지정하는 실행 방식을 지원해서 설계 RPS를 그대로 테스트할 수 있다는 점이 좋았습니다. 다만 k6만으로는 결과를 그래프로 보기가 불편해 설계에는 없었지만 시각화를 위해 Grafana로 모니터링을 추가했습니다.
서비스에 따라서 요청마다 비용이 부과되기도 하기 때문에 비용에 대해서 추가적으로 고려했습니다.
| 항목 | 무료 범위 | 과금 단위 |
|---|---|---|
| CloudFront 요청 | 월 1000만 건 | 요청 수만큼 과금 |
| CloudFront 전송 | 월 1TB | GB당 과금 |
| EC2 에서 인터넷으로 나가는 비용 | 모든 서비스 합산 월 100GB | GB당 과금 |
| EC2 들어오는 요청(인바운드) | X | X |
평균 운영 부하와 API별 피크 부하에서는 목표를 달성했습니다. 에러 없이 빠르게 응답했고, 서버 자원도 여유가 충분했습니다.
다만 피크(33 RPS) 혼합 부하는 1차 실행이 중단됐고 2차 결과도 다시 확인해야 해서, 최대 처리량은 아직 확정하지 못했습니다. 다음 단계는 Stress 중단 원인 확인과 결과 재정리입니다.
# 1. 부하테스트 도입결론: 사용자 시나리오 중심 테스트
부하테스트에는 DB의 연결과 CUD에 대한 검증이나 외부 서비스에 대한 연동, DB 연결과 조회에 대한 검증 등 여러 목적의 테스트가 존재합니다. 이번 v1배포에서는 개발 기간에 비해 볼륨이 전체의 70%로 api의 종류가 많기 때문에 세부적인 성능보다는 일단 사용자의 시나리오대로 잘 동작하는 것이 가장 중요하다고 판단했습니다. 따라서 설계에서 세웠던 목표에 따라서 사용자 시나리오를 작성해 사용자가 사용할때 성능이 잘 나오는지 판단했습니다.
- 부하 도구: k6
- 모니터링 도구: granfana, Prometheus, Loki
- 테스트용 별도의 서버 구성하고 dev.star23sharp.com으로 연결
- 테스트용 DB에 테스트 데이터 구성 : 1775명
도구는 k6를 선택했습니다. 시나리오를 JS로 작성할 수 있어서 비교적 직관적인 문법을 가지고 있었습니다. python이나 java에 특화된 도구들도 있지만 사용성을 먼저 생각했습니다. 무엇보다 초당 몇 건처럼 목표 RPS를 직접 지정하는 실행 방식을 지원해서 설계 RPS를 그대로 테스트할 수 있다는 점이 좋았습니다. 다만 k6만으로는 결과를 그래프로 보기가 불편해 설계에는 없었지만 시각화를 위해 Grafana로 모니터링을 추가했습니다.
서비스에 따라서 요청마다 비용이 부과되기도 하기 때문에 비용에 대해서 추가적으로 고려했습니다.
| 항목 | 무료 범위 | 과금 단위 |
|---|---|---|
| CloudFront 요청 | 월 1000만 건 | 요청 수만큼 과금 |
| CloudFront 전송 | 월 1TB | GB당 과금 |
| EC2 에서 인터넷으로 나가는 비용 | 모든 서비스 합산 월 100GB | GB당 과금 |
| EC2 들어오는 요청(인바운드) | X | X |
AI 기능은 외부 LLM을 호출하기 때문에 부하테스트 중에 실제로 호출하면 비용이 발생해 우회 방법을 고려했습니다.
- Mock 서버: AI 서버가 호출하는 LLM 주소를 Mock 서버(로컬 :8090)로 바꾸는 방법입니다. Mock은 실제와 비슷하게 3~6초 뒤에 응답하고 스트리밍도 흉내 냅니다. 응답을 기다리는 동안 연결, 스레드, 메모리를 붙잡고 있는 모습까지 재현되기 때문에 부하 측정에는 이 방법이 맞다고 판단했습니다.
- 환경변수로 고정 응답: LLM을 호출하지 않고 즉시 정해진 응답을 돌려주는 방법입니다. 기능 흐름을 확인하기에는 편하지만 기다리는 시간이 없어서 실제보다 훨씬 가볍게 측정됩니다. 그래서 기능 확인용으로만 쓰기로 했습니다.
-
브라우저에서 토큰 복사하기 (개발자 도구에서 직접하는 방법)
- 구현이 간단하고 실제 로그인 과정을 통해 발급된 토큰을 사용할 수 있다는 장점이 있지만 토큰이 만료될 때마다 사람이 다시 로그인하여 토큰을 복사해야 한다. VUser를 1명으로 놓고 api를 여러번 호출하는 방식은 문제가 없겠지만 여러 사용자를 가정하는 경우 VUser별로 토큰을 준비해야 하므로 VUser 수가 많아질수록 계정 및 토큰 관리가 어려워져서 해당 방법은 사용하지 않았다
-
JWT secret으로 토큰에 직접 서명. (채택)
- 테스트 회원이 이미 들어가 있기 때문에 id 구간만 지정하면 수만 개의 토큰을 한 번에 만들 수 있어 VUser 수가 많아져도 테스트가 가능하다. 백엔드에 테스트용 발급 API를 추가하거나 재배포할 필요가 없어서 로컬에서 바로 실행할 수 있다.
-
실행 방식 테스트
- 세션에 jwt토큰 등록:
export jwt = 입력 - 토큰 생성 스크립트 실행: user id n개 생성 (테스트 회원과 동일해야함)
- k6로 스트립트 실행: 스크립트 내에서 vuser에게 토큰 분배하고 테스트 진행함.
- 세션에 jwt토큰 등록:
실제 로그인 우회 방식이 잘 동작하는지 확인하기 위한 세미테스트 진행.
연결 테스트: vuser 10명 30초동안 onboarding에 특정api 대해서만 테스트함.
목적: 평균 운영 부하에서 응답시간과 실패율이 존재하는지.
- 목표 RPS: 6.2
- 시간: 30분
- 사용자의 하루 평균 사용 시간이 10분 (600초)
- 부하테스트 실행시 api 호출 비율
- 유저 조회 2회
- 유저 프로필 상세 조회 15회
- 좋아요 12회
- 채팅방 생성 4회
- 채팅 api 15회
- 사용자 한명이 10분동안 호출하는 api갯수 = 44회
-
avg_vuser = 85 -
결과 파일: [load-10-02-AM-0341.json](https://github.com/100-hours-a-week/KTB4-17th-wiki/blob/loadtest/load/Load-20261002-am0341.json)
목적: 어느 단계부터 성능이 저하되었다 회복되는지 (최대 처리량)
- 목표 RPS: 33
- 시간: 30분
- 사용자의 하루 평균 사용 시간이 10분 (600초)
- 부하테스트 실행시 api 호출 비율
- 유저 조회 2회
- 유저 프로필 상세 조회 15회
- 좋아요 12회
- 채팅방 생성 4회
- 채팅 api 15회
- 사용자 한명이 10분동안 호출하는 api갯수 = 44회
avg_user = 225
원하는 출력 최고 부하일때 피크 상태는 33 RPS인데 해당 값을 만족하려면 avg_user = 225가 되어야 합니다.
avg_user / 600(초) * 44 * 2 = 33 RPS
| 구간 | 부하(평균 대비) | 목표 RPS | 시간 |
|---|---|---|---|
| 1 | 50% | 8.3 | 0~6분 |
| 2 | 100% | 16.7 | 6~12분 |
| 3 | 150% | 25 | 12~18분 |
| 4 | 200% | 33 | 18~24분 |
| 회복 관찰 | 50% | 8.3 | 24~28분 |
| 램프다운 | 0 | - | 28~30분 |
- 결과 파일: [stress-10-02-AM-0505.json](https://github.com/100-hours-a-week/KTB4-17th-wiki/blob/loadtest/stress/Stress-20261002-am0505.json)
실행중 오류
stress ✗ [=================>--------------------] 024/225 VUs 14m21.9s/30m0s 0.56 iters/s
ERRO[0862] thresholds on metrics 'http_req_failed{kind:write}' were crossed; at least one has abortOnFail enabled, stopping test prematurely
중단 이유는 로그 보고 찾아야함. 변경된 값은 없는데 다시 실행하니까 성공 했음.(찾아보기)
목적: 한 번에 API 하나만 정한 RPS로 반복 호출. 각 실행에서 해당 API의 성공률, p95, p99, p99.5 기준시간 이내 성공률을 기록합니다. API별 결과를 비교해 SLO 후보를 정합
- 시간: 5분
| API 묶음 | 피크 목표(분당 요청 수) | 5분 전체 요청 수 |
|---|---|---|
| 추천 조회 합계 (15/44) | 689회/분 | 3,445 |
| 메시지 전송 (15/44) | 689회/분 | 3,445 |
| 좋아요 (12/44) | 551회/분 | 2,755 |
| 채팅방 목록 (4/44) | 184회/분 | 920 |
| 온보딩 조회 (2/44) | 92회/분 | 460 |
export DURATION=5m LATENCY_LIMIT_MS=1000 PRE_VUS=10 MAX_VUS=20 ACCOUNT_INDEX=0- 평균 운영 부하에서는 실패 없이 빠르게 응답했고 서버 자원도 여유가 충분합니다.
- Stress 1차 실행이 쓰기 요청 실패로 중단됐고 원인은 아직 확인하지 못했습니다.
- 조회 API 3개에서 일부 요청만 느린 현상이 있습니다.
| 항목 | 설계 | 실제 |
|---|---|---|
| VUser | 85명 | 88명 |
| 시간 | 30분 | 39분 24초 |
| 목표 RPS | 6.2 | 최대 6.97 |
| 지표 | 결과 |
|---|---|
| 요청 수 | 10,848건 (설계 11,160건의 97%) |
| 에러율 | 0% |
| 응답시간 | 평균 27ms / Median 22ms / p95 41ms / 최대 525ms |
| p99 순간 최고점 | 1.6초 (03:09~03:11) |
| 자원 | 사용률 | 포화(대기) | 판정 |
|---|---|---|---|
| CPU | 최대 8.08% | - | 여유 |
| DB 커넥션 | 20개 중 최대 2개 (10%) | 대기 0건 | 여유 |
- 설계한 부하가 정상적으로 들어갔고, 에러 없이 빠르게 응답했습니다.
- CPU와 DB 커넥션 모두 10% 정도만 사용해 여유가 큽니다.
- 03:09~03:11에 p99가 1.6초까지 잠깐 올랐다가 회복했습니다. 일시적인 현상이지만 원인(GC, 느린 쿼리)은 확인이 필요합니다.
| 항목 | 설계 |
|---|---|
| VUser | 최대 225명 |
| 시간 | 30분 |
| 목표 RPS | 8.3 → 16.7 → 25 → 33 → 회복 8.3 |
| 지표 | 결과 |
|---|---|
| 요청 수 | 10,848건 |
| 에러율 | 0% |
| 응답시간 | 평균 27ms / p95 41ms / p99 140ms / 최대 525ms |
| p99 순간 최고점 | 367ms (05:36:45, 같은 시점 p95 33ms) |
요청 유형별 응답시간
| 유형 | 평균 | p95 | p99 | 최대 |
|---|---|---|---|---|
| Read | 22ms | 31ms | 119ms | 335ms |
| Write | 28ms | 39ms | 105ms | 352ms |
| Heavy | 82ms | 149ms | 164ms | 525ms |
| 자원 | 사용률 | 포화(대기) | 판정 |
|---|---|---|---|
| CPU | 최대 10.3% | - | 여유 |
| 메모리 | 63.6% | - | 안정 |
| 디스크 | 69.1% | - | 주의 |
| DB 커넥션 | 10개 중 최대 3개 (30%) | 대기 0건 | 여유 |
- 모든 요청 유형이 기준(p95 500ms)을 통과했습니다.
- Heavy 요청이 다른 요청보다 3~5배 느립니다. 부하가 커지면 가장 먼저 느려질 요청입니다.
- 05:36:45에 p99만 크게 튀었습니다. 소수 요청만 느린 꼬리 지연입니다.
- 목표인 33 RPS에 도달했는지는 결과 파일을 다시 확인한 뒤 판단해야 합니다.
| 항목 | 내용 |
|---|---|
| 방식 | API 1개씩 목표 RPS로 고정 호출 |
| 시간 | API당 5분 |
| 기준시간 | 1초 (LATENCY_LIMIT_MS=1000) |
| 기능 | Endpoint |
|---|---|
| 추천 활성 배치 조회 | GET /api/v1/recommendation-batches/active |
| 온보딩 상태 조회 | GET /api/v1/users/me/onboarding |
| 채팅방 목록 조회 | GET /api/v1/chat-rooms |
| 좋아요 전송 | POST /api/v1/likes |
| 메시지 전송 | POST /api/v1/chat-rooms/{chatRoomId}/messages |
| 홈 화면 페이지네이션 (DB 20개 조회) | GET /api/v1/recommendation-batches/{batchId}/items |
| 기능 | 실행 일시 | 목표 RPS | 실제 RPS | 요청 수 | 성공률 |
|---|---|---|---|---|---|
| 추천 활성 배치 조회 | 10-02 13:15~13:20 | 11.48 | 11.49 | 3,446 | 100% |
| 온보딩 상태 조회 | 10-02 13:21~13:26 | 1.53 | 1.54 | 461 | 100% |
| 채팅방 목록 조회 | 10-02 13:26~13:31 | 3.07 | 3.07 | 921 | 100% |
| 좋아요 전송 | 10-03 21:19~21:24 | 9.20 | 9.20 | 2,760 | 100% |
| 메시지 전송 | 10-03 21:24~21:29 | 11.48 | 11.48 | 3,445 | 100% |
| 홈 화면 페이지네이션 | 10-03 21:13:19~21:18:19 | 11.48 | 11.48 | 3,445 | 100% |
| 기능 | 평균 | Median | p95 | p99 | p99.5 | 최대 |
|---|---|---|---|---|---|---|
| 추천 활성 배치 조회 | 32.03ms | 18.65ms | 102.68ms | 132.81ms | 150.78ms | 221.04ms |
| 온보딩 상태 조회 | 33.48ms | 20.04ms | 107.91ms | 133.87ms | 162.99ms | 184.97ms |
| 채팅방 목록 조회 | 39.79ms | 26.40ms | 109.35ms | 129.83ms | 150.02ms | 248.58ms |
| 좋아요 전송 | 33.70ms | 32.92ms | 37.79ms | 52.22ms | 69.25ms | 116.00ms |
| 메시지 전송 | 21.94ms | 21.06ms | 27.34ms | 38.77ms | 50.56ms | 247.88ms |
| 홈 화면 페이지네이션 | 41.84ms | 41.05ms | 46.74ms | 61.48ms | 70.23ms | 129.63ms |
[사진: API별 테스트 결과 그래프]
- 6개 API 모두 목표 RPS에 도달했고, 실패 0건, Dropped 0건, 1초 이내 응답 비율 100%입니다.
- 좋아요, 메시지, 홈 화면 페이지네이션은 응답시간이 고르게 빠릅니다.
- 추천 활성 배치, 온보딩, 채팅방 목록은 대부분 20ms대지만 일부 요청이 100ms 이상 걸립니다. 세 API의 부하량이 7배 넘게 차이 나는데도 같은 패턴이라, 공통 원인(인증, 사용자 조회, 캐시 등)이 있을 가능성이 높습니다.
- 단독 실행에서는 쓰기 실패가 없었습니다. 그래서 Stress 1차 중단은 여러 API가 동시에 호출될 때 생긴 문제로 볼 수 있습니다.
측정값에 약 2배 여유를 둔 값입니다.
| 대상 | p95 | p99 | 성공률 |
|---|---|---|---|
| 추천 활성 배치, 온보딩, 채팅방 목록 | 200ms 미만 | 300ms 미만 | 99.9% 이상 |
| 홈 화면 페이지네이션, 좋아요, 메시지 | 100ms 미만 | 150ms 미만 | 99.9% 이상 |
| 우선순위 | 과제 | 할 일 |
|---|---|---|
| 1 | Stress 1차 중단 원인 확인 | 실패한 쓰기 요청의 상태 코드와 에러 로그 확인 |
| 2 | Stress 결과 재정리 | 결과 파일에서 구간별 RPS와 응답시간을 다시 뽑아 33 RPS 도달 여부 확인 |
| 3 | 조회 API 꼬리 지연 확인 | 같은 조건으로 재측정하고 GC와 쿼리 지표를 함께 확인 |
| 4 | DB 커넥션 설정 통일 | Load(20개)와 Stress(10개)의 Pool Size 맞추기 |
| 5 | 후속 테스트 | Stress 재측정, Soak(장시간 유지), Spike(급격한 증가) |
평균 운영 부하와 API별 피크 부하에서는 목표를 달성했습니다. 에러 없이 빠르게 응답했고, 서버 자원도 여유가 충분했습니다.
다만 피크(33 RPS) 혼합 부하는 1차 실행이 중단됐고 2차 결과도 다시 확인해야 해서, 최대 처리량은 아직 확정하지 못했습니다. 다음 단계는 Stress 중단 원인 확인과 결과 재정리입니다.