[cloud] 3Tier Max User Load Test - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

1. 문서 개요

  • 작성자: aaron.lee(이유성)/클라우드
  • 작성일: 2025-06-30
  • 목적
    • 이벤트 상황(예상 동시 접속자 수 100~200명) 발생 시, GCP HTTPS Load Balancer와 Managed Instance Group(MIG)의 오토스케일링 기능을 통해 서버 인프라가 안정적으로 요청을 처리할 수 있는지를 검증하기 위해 수행되었습니다.

2. 테스트 스크립트 개요

export const options = {
  stages: [
    { duration: '15m', target: 200 },  // 15분간 VU 0→200 램프업
    { duration: '5m',  target:   0 },  //  5분간 VU 200→0 램프다운
  ],
};
  • 시나리오 분기
    • 5%: 사진 30장 업로드 → 앨범 생성
    • 95%: 월별 앨범 조회 → 앨범 요약 조회 → 앨범 상세 조회
  • 사이클: 각 VU가 1회 수행 후 sleep(1)

3. 테스트 환경

  • 인스턴스: M2-medium (2 vCPU / 4 GB RAM) × MIG 2대 (최대 3대)
  • 로드밸런서: GCP HTTPS Load Balancer
  • 데이터베이스: Cloud SQL (MySQL 8.4) Master-Replica
  • 부하 도구: k6 v0.45.0
  • 모니터링: HTTP 응답 시간, 처리량, 오류율, CPU 사용률(Cloud Monitoring)

4. 실행 프로필 및 주요 지표

4.1 테스트 개요

  • 테스트 기간 (Duration): 20분 (15분 램프업 → 5분 램프다운)
  • 최대 VUs (vus_max): 200

4.2 Counters & Rates

지표 Count Rate
수신 데이터량 (data_received) 14.1 GB 11.8 MB/s
전송 데이터량 (data_sent) 11.8 GB 9.8 MB/s
HTTP 요청 수 (http_reqs) 61,200 50.91 req/s
Iterations 13,500 11.23 iter/s
HTTP 실패율 (http_req_failed) 0 건 0.01 /s

4.3 Trends (응답·반복 소요 시간)

지표 평균 (avg) 중앙값 (med) P90 P95 최대 (max)
전체 요청 소요 시간 (http_req_duration) 1 s 65 ms 4 s 13 s 30 s
서버 응답 대기 시간 (TTFB) (http_req_waiting) 1 s 61 ms 4 s 13 s 30 s
사이클당 전체 소요 시간 (iteration_duration) 9 s 3 s 28 s 31 s 1 m

5. Auto Scaling(MIG) 동작 결과

  • 초기 인스턴스 수: 2
  • 테스트 구간 전체: 평균 CPU 사용률 > 60%
  • 최종 인스턴스 수: 4

6. 분석

  1. 처리량 & 응답성
    • VU 200 상태에서 HTTP 요청 약 50 req/s, Iteration 약 11 iter/s로 안정적 처리
    • p50 65 ms / p95 13 s 수준, 읽기 중심 시나리오(95%)에 대해 허용 범위 내 응답
      • p95 기준 15s 예상 - 비동기 처리 방식으로 앨범을 생성해주기 때문에 허용범위
  2. 구성 한계
    • 급격한 스파이크(순간 대량 업로드) 시에는 스케일링 지연 가능
    • 장시간 부하 → 스케일 인/아웃 사이클 검증 필요

7. 추후 고려 사항

  1. 부하 스크립트 강화
    • 생성 시나리오 비율 확대 및 사진 개수 증가로 쓰기 부하 강화
    • 즉각적인 VU 스파이크 테스트 추가
  2. 다중 지표 스케일링
    • CPU 외 응답 시간(p95 > 500 ms), 메모리, 네트워크 지표 복합 정책 도입
  3. 예비 용량 확보
    • Spike 대응을 위해 최소 인스턴스 3대 검토
    • 프로비저닝 지연 최소화를 위한 버퍼 확보
  4. 캐시 계층 도입
    • Cloud Memorystore(Redis)로 조회 결과 캐싱 → 애플리케이션 서버 부하 저감
  5. 장시간 스트레스 테스트
    • 1시간 이상 지속 부하 → 스케일 인/아웃 주기 및 안정성 검증

8. 보고서

image image

전체 스크립트

import http from 'k6/http';
import { sleep } from 'k6';
import { uuidv4, randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.1.0/index.js';
import { postJson, putBinary, getJson } from '../utils/request.js';
import { BASE_URL, HEADERS } from '../config.js';

let ACCESS_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyIiwiZXhwIjoxNzUyNDYwNjU5fQ.5XU1Vqi-ZVACOzOaVE1UIAIrRYiOvEpR0rF9_-RHiTU';
const photoCount = 30;

const IMAGE_FILES = [
  'ongi.png',
  'ongi2.png',
  'ongi3.png',
  '닝닝1.jpeg',
  '닝닝2.jpeg',
  '에스파단체1.jpeg',
  '에스파단체2.jpeg',
  '에스파단체3.jpeg',
  'Mount.jpeg',
  'cat.jpeg',
];
const IMAGES = IMAGE_FILES.map(f => open(`../assets/${f}`, 'b'));
const CONTENT_TYPES = IMAGE_FILES.map(f =>
  f.endsWith('.png') ? 'image/png' : 'image/jpeg'
);

export const options = {
  stages: [
    { duration: '15m', target: 200 },  // 15분간 VUs 0→200
    { duration: '5m',  target: 0  },  // 5분간 VUs 200→0
  ],
};

function createAlbumScenario() {
  const headers = HEADERS(ACCESS_TOKEN);

  // 1) presigned-url 요청
  const pictures = Array.from({ length: photoCount }, () => {
    const idx = randomIntBetween(0, IMAGES.length - 1);
    return {
      idx,
      pictureName: `ongi_${uuidv4().slice(0, 8)}${IMAGE_FILES[idx].match(/\.[^.]+$/)[0]}`,
      pictureType: CONTENT_TYPES[idx],
    };
  });

  // 2) Presigned URL 발급
  const presignedRes = postJson(
    `${BASE_URL}/api/presigned-url`,
    { pictures },
    headers
  );
  const presignedFiles = presignedRes.json().data.presignedFiles;

  // 3) 이미지 업로드
  const uploadedUrls = [];
  presignedFiles.forEach((file, i) => {
    const { idx, pictureType } = pictures[i];
    putBinary(
      file.presignedUrl,
      IMAGES[idx],
      { 'Content-Type': pictureType }
    );
    uploadedUrls.push(file.pictureURL);
  });

  // 4) 앨범 생성
  const pictureUrlObjects = uploadedUrls.map(url => ({
    pictureUrl: url,
    latitude: null,
    longitude: null,
  }));
  postJson(
    `${BASE_URL}/api/album`,
    { albumName: '부하테스트', pictureUrls: pictureUrlObjects },
    headers
  );
}

function retrieveAlbumScenario() {
  const headers = HEADERS(ACCESS_TOKEN);

  // 1) 월별 앨범 조회
  const monthlyRes = getJson(
    `${BASE_URL}/api/album/monthly`,
    headers
  );
  const albumInfo = monthlyRes.json().data.albumInfo;
  if (!albumInfo || albumInfo.length === 0) {
    console.error('경고: 앨범이 하나도 없습니다.');
    return;
  }

  // 2) 첫 앨범 요약 조회
  const albumId = albumInfo[0].albumId;
  getJson(`${BASE_URL}/api/album/${albumId}/summary`, headers);

  // 3) 첫 앨범 사진 리스트 조회
  getJson(`${BASE_URL}/api/album/${albumId}`, headers);
}

export default function () {
  // 5% 생성, 95% 조회
  if (Math.random() < 0.05) {
    createAlbumScenario();
  } else {
    retrieveAlbumScenario();
  }
  sleep(1);
}