성능 테스트 - Team-likelion-3rd-Project/likelion-devops-7th-3rd-team01 GitHub Wiki
1주차(M1)에는 무엇을 어떻게 시험할지만 적는다. 실행 결과는 6주차(M6)에 채운다.
- 서비스가 목표 응답 시간·오류율을 지키는지 확인한다.
- 배포·장애 상황에서도 사용자 요청이 실패하지 않는지 확인한다.
- 병목 1개를 찾아 개선하고, 개선 전→후 수치를 남긴다.
| 항목 | 내용 |
|---|---|
| 부하 도구 | k6 2.2.0 (Docker로 실행, 스크립트와 실행 방법은 백엔드 레포 perf/README.md) |
| 실행 위치 | 클러스터 밖 PC에서 실행 (부하 도구가 서버 자원을 같이 쓰지 않도록) |
| 대상 환경 | 온프렘(M4 기준값, 10/23 측정)과 AWS EKS(M5 이후). 온프렘은 Tailscale로 접속하므로 클라이언트 지표에 VPN 지연이 섞인다 |
| 클라이언트 지표 | k6: 응답 시간 p95·p99, 실패율(http_req_failed), 초당 요청 수(RPS) |
| 서버 지표 | Grafana: Pod CPU·메모리, Pod 수(HPA), DB 커넥션 풀 사용량, Redis 적중률 |
| 화면 지표 | Lighthouse (FE 박영찬, 7장) |
mock AI (AI_MODE=mock) |
실제 AI (AI_MODE=bedrock) |
|
|---|---|---|
| 언제 | 모든 k6 부하 테스트, 평소 개발·CI | 점검일에만: 4주차 점검·6주차 리허설·발표 (각 최대 4시간, 총 12시간) |
| 모델 | - | Bedrock Claude Haiku 5.5 (Global 추론 프로파일, 10/22 비교 시험으로 선택) |
| 응답 | 고정된 가짜 요약·답변. 지연은 AI_MOCK_DELAY로 준다. 부하 테스트는 5s (10/22 실측: 질문 답변 3.6~10초) |
실제 AI 호출 |
| 비용 | 0 | 토큰 비용 발생 (예상: 입력 약 160만·출력 약 29만 토큰, 질문 150건 포함) |
| 측정 | 서버 처리 능력 (AI 외 구간) | AI 응답 시간 1회 측정 (질문 5회 내외) → mock 지연값에 반영 |
- 실제 AI로는 부하 테스트를 하지 않는다. 비용이 사용자 수에 비례해 늘고, AI 회사의 호출 제한에 먼저 걸리기 때문이다.
- mock일 때 화면에는 "샘플 데이터" 띠가 보인다.
Bedrock Playground에서 같은 지시문·실제 기사로 두 모델을 비교하고, 고른 모델로 앱에서 실제 호출했다.
| 모델 | 질문 답변 | 분야 요약 | 지시 준수 (해요체·출처·사건 구분) |
|---|---|---|---|
| Nova 2 Lite | 1.3초 | - | 부족 (합니다체, 답에 쓴 기사를 출처에서 빠뜨림, 서로 다른 사건을 묶음) |
| Claude Haiku 5.5 (선택) | 3.6~10초 | 분야당 15~19초 | 좋음 |
- 앱 실제 호출: 요약 배치 6개 분야 110초(분야당 입력 약 1만 / 출력 약 3천 토큰), 질문 6개 정상 (관련 답변 3개, 범위 밖 거절 2개, 기사 없는 주제 1개)
- 비용 추정 (Anthropic 공식가 기준, Bedrock 실제 가격은 확인 필요): 배치 1회 약 $0.016, 질문 1회 약 $0.001, 점검일 하루 약 $0.18, 프로젝트 전체 약 $0.55
- Claude Haiku 5.5는 답하기 전에 생각하는 토큰이 출력에 포함돼 출력 상한을 넉넉히 둔다 (요약 8,000 / 답변 2,000)
- 테스트 계정: 질문은 사용자당 하루 10회 제한이 있으므로 시나리오 2·4는 실행할 때마다 새 계정 100명(
perf-<실행 시각>[email protected]…)을 만들어 질문을 고르게 나눈다. 고정 계정을 미리 만들어 두면 같은 날 두 번째 실행부터 429가 나기 때문이다. - 대상 주소는
BASE_URL, mock 지연은MOCK_DELAY_MS(서버AI_MOCK_DELAY와 같은 값)로 넣는다. - 요약 데이터: 테스트 전 요약 배치를 1회 이상 실행해 IT 6분야 요약이 있는 상태에서 시작한다.
비로그인 사용자가 홈을 보고 분야 페이지로 이동하는, 가장 많이 일어나는 흐름.
| 항목 | 내용 |
|---|---|
| 요청 |
GET /api/news/home → GET /api/news/categories/{임의 분야} (사용자당 1~3초 간격) |
| 부하 | 가상 사용자 0 → 200명 (2분) → 200명 유지 (5분) → 0 (1분) |
| 목표 | p95 < 300ms, 실패율 < 1% |
| 볼 것 | Redis 캐시 적중 시 DB 조회가 거의 없는지, Pod CPU |
로그인 사용자가 질문을 보내고 결과가 나올 때까지 2초 간격으로 확인하는 흐름. (mock AI)
| 항목 | 내용 |
|---|---|
| 요청 |
POST /api/auth/login → POST /api/questions → GET /api/questions/{id} 2초마다 반복 (완료·실패·거절까지) |
| 부하 | 가상 사용자 0 → 50명 (1분) → 50명 유지 (5분) |
| 목표 | 접수 p95 < 500ms, 접수→완료 p95 < mock 지연 + 5초, 60초 타임아웃(FAILED) 0건 |
| 볼 것 | 대기(PENDING) 질문이 쌓이는지, 레플리카 2개 이상에서 같은 질문이 두 번 처리되지 않는지 |
| 항목 | 내용 |
|---|---|
| 요청 | 시나리오 1과 같음 |
| 부하 | 20명 유지 (1분) → 10초 만에 500명 → 500명 유지 (3분) → 20명 (2분) |
| 목표 | 폭증 구간 실패율 < 5%, HPA가 Pod를 늘린 뒤에는 p95가 시나리오 1 수준으로 회복 |
| 볼 것 | HPA가 Pod를 늘리기까지 걸린 시간, 늘어나는 동안의 오류 (CL 김차니와 함께 관찰) |
| 대상 | 기준 | 설정안 | 상태 |
|---|---|---|---|
| api | CPU | HPA minReplicas: 2, maxReplicas: 4, 평균 CPU 70% (요청 250m 기준 175m). 줄일 때 2분 대기 |
온프렘 측정에서 파드당 CPU 최대 약 0.17 core (조회 78 req/s). metrics-server 필요. EKS에서 적용·시나리오 3으로 확인 예정 |
| worker | Redis 대기열 길이 |
KEDA Redis 스케일러 (채택안). 대기열 questions:queue, 워커 1개(스레드 4개)당 대기 5건, 1~3개 |
KEDA 설치 필요. 미적용 |
워커 확장 방식 비교:
| 방식 | 장점 | 단점 |
|---|---|---|
| KEDA Redis 스케일러 | Redis 대기열 길이를 직접 읽어 앱 지표·어댑터가 필요 없다. 0개까지 줄일 수 있다 (PENDING 질문 재투입은 api가 하므로 워커가 0개여도 질문이 사라지지 않음) |
KEDA 설치 필요 |
| HPA + prometheus-adapter | 이미 있는 Prometheus와 앱 지표 newsbrief_questions_queue_length 사용 |
어댑터 설치·규칙 작성, 0개로는 못 줄인다 |
최소 개수는 1로 둔다. 0이면 첫 질문이 워커가 뜰 때까지(약 16초) 늦어진다.
| 항목 | 내용 |
|---|---|
| 요청 | 시나리오 1 + 2를 섞어 가상 사용자 50명으로 일정하게 유지 |
| 진행 | 부하 도중 새 이미지로 롤링 배포 (kubectl rollout restart 또는 develop/main push) |
| 목표 | 배포 중 실패 요청 0건 (M5 완료 기준) |
| 볼 것 | 실패가 있으면 readiness 통과 전 트래픽 유입, 종료 중 Pod로의 요청 등 원인을 기록하고 정상 종료(graceful shutdown) 설정으로 개선 |
시나리오 1·2를 실행하는 도중에 의존 서비스를 멈춘다.
| 상황 | 방법 | 기대 동작 |
|---|---|---|
| Redis 중단 | Redis Pod 삭제 / ElastiCache 접근 차단 | 조회는 DB에서 계속 응답 (응답 시간은 늘어도 실패 0). 복구 후 캐시 다시 사용 |
| AI 중단 |
AI_MODE=fail (mock이 항상 오류 반환) |
질문은 FAILED로 끝나고 화면에 실패 안내. 요약은 직전 요약 계속 표시 |
| 뉴스 소스 중단 | RSS 주소 접근 차단 | 요약 갱신만 실패, 직전 요약 계속 표시 |
시나리오 1~2 첫 측정에서 가장 느린 구간 1개를 골라 개선하고 같은 시나리오로 다시 측정한다. 예상 후보:
- 요약 캐시 적중률 (캐시 TTL, 배치 후 캐시를 미리 채우기)
- DB 커넥션 풀 크기
- 질문 기록·관련 기사 검색 쿼리 인덱스
| 후보 | 근거 | 개선 방향 |
|---|---|---|
| 1. 파드 시작 직후 느림 (워밍업) | 파드가 뜬 직후 첫 측정은 시나리오 1 p95 1.29초, 같은 부하로 예열 뒤 다시 재면 70ms. 첫 측정 중 Grafana에서 api 파드 CPU는 최대 약 0.17 core로 여유가 있었고, p95가 높은 건 시작 몇 분뿐이었다 (최대 부하 구간은 정상) | readiness 전에 주요 API를 미리 호출해 예열, startupProbe. 롤링 배포(시나리오 4)·HPA 확장(시나리오 3) 때 새 파드에도 같은 문제가 생길 수 있다 |
| 2. Tailscale(VPN) 경로 | 예열 뒤 측정에서도 k6 실패율 1.42%(441건), 최대 52초. 같은 구간 Grafana(Traefik)는 최대 약 78 req/s에서도 서버 p95 0.1초 아래, 응답 코드 전부 200 → 서버가 아니라 클라이언트~서버 사이 경로에서 끊기거나 지연된다 | VPN 없이 같은 시나리오 재측정(HyeobSang 예정), AWS 측정과 비교. 온프렘 k6 수치는 VPN 지연이 섞인 값으로 본다 |
| 시나리오 | 지표 | 개선 전 | 개선 후 | 개선 내용 |
|---|---|---|---|---|
| 1. 조회 200명, 파드 시작 직후 (온프렘) | p95 | 1.29초 (10/23) | 332ms (10/26) | 병목 후보 1 개선: api 파드가 readiness UP 전에 조회 API를 140회 미리 호출 (NewsWarmUp, 1.7~2.5초 걸림) |
| 〃 | p99 | - | 2초 | 〃 |
| 〃 | 실패율 | 0.10% | 0% (37,118건) | 〃 |
- 측정 조건: 둘 다 클러스터 밖 PC에서 Tailscale로, api 파드 2개가 막 뜬 상태에서 시나리오 1(0→200명 2분, 200명 5분)을 바로 시작. 10/26은
kubectl rollout restart deployment/api완료 직후 - p95는 74% 줄었지만 목표 300ms를 조금 넘는다. 예열을 끝낸 파드(10/23 두 번째 측정)는 70ms라 남은 차이는 예열 횟수 부족(JIT가 200명 부하에서 더 컴파일)이나 Tailscale 경로로 본다.
WARMUP_ROUNDS(기본 20)를 늘려 재측정하거나 EKS에서 비교한다
| 항목 | 내용 |
|---|---|
| 목적 | 화면 로딩 속도와 접근성을 숫자로 확인하고, 6주차에 개선 전후를 비교 |
| 도구 | Chrome DevTools의 Lighthouse (Performance, Accessibility, Best Practices, SEO) |
| 측정 화면 | 홈(/), 분야 페이지(/category/AI) |
| 측정 환경 | 모바일 기준, 시크릿 창, 확장 프로그램 끄기. 같은 조건으로 3회 측정해 중앙값 기록 |
| 측정 시점 | 4주차(운영 도메인 배포 후) 기준값 → 5주차 개선 후 → 6주차 최종 비교 |
| 기록 항목 | Performance 점수, LCP(가장 큰 요소 표시 시간), CLS(레이아웃 흔들림), TBT(입력 지연) |
| 개선 후보 | 번들 크기 줄이기(코드 분할), CloudFront 캐시, 이미지·폰트 최적화 |
| 시점 | 화면 | Performance | LCP | CLS | TBT | 개선 내용 |
|---|---|---|---|---|---|---|
| 10/23 온프레미스 | 홈 | 76 | 5.0s | 0.001 | 40ms | - |
| 10/23 온프레미스 | 분야 | 76 | 5.2s | 0.003 | 40ms | - |
| 10/23 AWS | 홈 | 88 | 3.5s | 0.001 | 0ms | - |
| 10/23 AWS | 분야 | 86 | 3.6s | 0.002 | 0ms | - |
| 5주차 개선 후 | 홈 | |||||
| 5주차 개선 후 | 분야 | |||||
| 6주차 최종 | 홈 | |||||
| 6주차 최종 | 분야 |
측정 조건: 크롬 시크릿 창(캐시 없음), Lighthouse 13.4.1 모바일(Moto G Power 에뮬레이션, Slow 4G), 비로그인. 홈과 분야(AI) 각각 3회, 중간값 기록. 서버는 온프레미스(VPN·HTTP), 데이터는 샘플. AWS 배포 전에 먼저 잰 참고 기준값.
결과: 홈 Performance 76, LCP 5.0s, CLS 0.001, TBT 40ms. 분야 Performance 76, LCP 5.2s, CLS 0.003, TBT 40ms. Accessibility 100, Best Practices 74, SEO 82.
해석: 홈과 분야 점수가 거의 같아 화면별 문제가 아니라 사이트 전체의 공통 원인임. LCP 요소는 기사 제목 텍스트이고 서버 응답(TTFB 10ms)은 빠르며, 지연은 JS를 받아 실행한 뒤 목록이 그려지기까지에서 생김. Best Practices 74는 HTTP라서 HTTPS 항목 2개가 실패한 것이고, 콘솔 에러 1건은 비로그인 상태의 /api/auth/me 401로 의도된 동작임. 샘플 데이터 기준이라 데이터 연동 후 재측정하고, AWS와는 네트워크 경로가 달라 같은 환경끼리 비교함.
개선 후보: 응답 압축(JS 약 293KiB, gzip 시 약 93KB), 정적 파일 캐시(JS·CSS에 Cache-Control 없음), 외부 글꼴(jsDelivr) 제거 또는 preload(렌더 차단, 약 0.45~0.95초), 화면별 코드 분할(미사용 JS 약 150KiB), SEO 기본 설정(meta description, robots.txt).
측정 조건: 크롬 시크릿 창, Lighthouse 모바일, Navigation, 기본 쓰로틀링(Slow 4G 시뮬레이션), 비로그인. 홈과 분야(AI) 각각 3회, 중간값 기록. 서버는 AWS(CloudFront + S3, API는 ALB 경유), 데이터는 실제 뉴스와 실제 AI 기준.
결과: 홈 Performance 88, LCP 3.5s, CLS 0.001, TBT 0ms. 분야 Performance 86, LCP 3.6s, CLS 0.002, TBT 0ms. Accessibility 100, Best Practices 96, SEO 90.
해석: 온프렘 대비 홈은 76에서 88, LCP 5.0초에서 3.5초로 개선됨. Best Practices는 HTTPS 적용으로 74에서 96으로 오름. LCP는 여전히 2.5초를 넘어 개선 필요 구간임.
개선 후보: 렌더 차단 요청(폰트, 예상 절약 약 1.3초), 안 쓰는 JS 약 45KiB, meta description 추가(SEO 90의 유일한 지적), 비로그인 콘솔 오류 확인(Best Practices 감점 항목).
온프렘 기준값은 10/23 M4-8에서 측정했다. 클러스터 밖 PC에서 Tailscale로 접속, 서버 AI_MODE=mock, AI_MOCK_DELAY=0s, api 파드 2개·worker 1개 (CPU limit 없음).
| 시나리오 | 환경 | p95 | p99 | 실패율 | RPS | 최대 Pod 수 | 비고 |
|---|---|---|---|---|---|---|---|
| 1. 조회 부하 | 온프렘 (10/23 첫 측정) | 1.29초 ❌ | - | 0.10% | 65 | api 2 | 파드 시작 직후. 최대 1분(타임아웃) |
| 1. 조회 부하 | 온프렘 (10/23 기준값) | 70ms ✅ | 376ms | 1.42% ❌ | 64 | api 2 | 예열 뒤. 최대 52초 |
| 1. 조회 부하 | EKS | ||||||
| 2. 질문 처리 | 온프렘 (10/23 기준값) | 접수 46ms ✅ / 전체 90ms | 접수 58ms / 전체 105ms | 0% ✅ | 5 | api 2, worker 1 | 접수→완료 p95 2.08초 ✅ (2초 확인 간격이 대부분), FAILED·타임아웃 0 |
| 2. 질문 처리 | EKS | ||||||
| 3. 스파이크 | EKS | ||||||
| 4. 롤링 배포 | 온프렘 (10/26, M5-9) | 105ms ✅ | 194ms | 0% ✅ (7,579건 중 0) | 19 | api 2 (교체 중 3) | 부하 50명 6분 중 kubectl rollout restart deployment/api, 롤아웃 41초. 질문 FAILED·타임아웃 0 |
| 4. 롤링 배포 | EKS |
서버 지표 (Grafana, 10/23 첫 측정 구간):
| 항목 | 값 |
|---|---|
| Traefik 초당 요청 | 최대 약 78 req/s |
| 5xx 비율 | 0% |
| api 파드 CPU | 파드당 최대 약 0.17 core |
| 메모리 | api 파드 약 400MiB, MySQL 약 500MiB |
시나리오 4 (10/26) 적용한 무중단 설정: 앱 graceful shutdown(처리 중 요청 최대 15초) + api preStop 10초 < terminationGracePeriodSeconds 30초, maxUnavailable: 0, 새 파드는 조회 API 예열 뒤 readiness UP, 워커는 처리 중 질문을 40초 기다리고 못 끝내면 대기열로 반환.