부하테스트 시나리오 4: 채널방 – 메세지 수신 - 100-hours-a-week/2-hertz-wiki GitHub Wiki
시나리오 설명: 사용자가 개인채널에서 상대방이 전송한 메세지를 수신
사용자가 개인채널에 들어와 상대방의 메세지를 수신합니다. 3초마다 메세지를 수신하는 /api/v1/channel-rooms/{channelRoomId}?page={페이지번호}&size={페이지크기}
API를 호출함(Polling). 백엔드에서는 DB를 검색하여 해당 채팅방의 대화내용을 응답합니다.
항목 | 내용 |
---|---|
테스트 목적 | 사용자가 상대방과 대화를 나누는 과정에서의 성능 및 부하 검증 |
테스트 환경 | - Monolithic: e2-custom-2-6144 (vCPU 2개, 메모리 6GB) |
대상 시스템 | 백엔드(SpringBoot + MySQL) |
테스트 도구 | k6 |
테스트 일시 | 2025년 5월 27일 |
사용자가 시그널을 받은 상대방과 대화를 나누는 과정에서의 성능 및 부하를 검증하기 위함입니다.
어플리케이션 | API 엔드포인트 | 메서드 | 설명 |
---|---|---|---|
SpringBoot | /api/v1/channel-rooms/{channelRoomId}?page={페이지번호}&size={페이지크기} | GET | 채널방의 메세지 내역 반환 API (Authorization 필요) |
차수 | 1차 | 2차 | 3차 |
---|---|---|---|
동시 사용자 수 | 50명 | 100명 | 500명 |
총 요청 시간 | 30초 | 30초 | 30초 |
요청 간격 | 3초 | 3초 | 3초 |
부하 생성 패턴 | shared-iterations (고정 개수 요청 배분) | shared-iterations (고정 개수 요청 배분) | shared-iterations (고정 개수 요청 배분) |
배치 처리 방식 | 멀티스레드 활용, 동시 요청 처리 | 멀티스레드 활용, 동시 요청 처리 | 멀티스레드 활용, 동시 요청 처리 |
🔹 1차 테스트 결과
K6
항목 | 값 | 정의 |
---|---|---|
최대 VU 수 (vus_max) | 50 | 동시에 실행된 최대 가상 사용자 수 (Virtual Users) |
총 요청 수 (http_reqs) | 501 | 전체 테스트 동안 실행된 HTTP 요청 수 |
평균 응답 시간 (avg) | 129.8ms | 전체 요청의 평균 응답 시간 |
최대 응답 시간 (max) | 909.84ms | 가장 느린 요청의 응답 시간 |
90% 응답 시간 (p90) | 270ms | 90%의 요청이 이 시간 이하로 응답됨 |
95% 응답 시간 (p95) | 541.65ms | 95%의 요청이 이 시간 이하로 응답됨 |
에러율 (http_req_failed) | 0.00% | 실패한 HTTP 요청의 비율 |
성공률 (checks_succeeded) | 100.00% | 성공 조건(응답 200, 유효 JSON)을 만족한 요청 비율 |
요청 완료 시간 (iteration_duration.avg) | 3.13s | 1회 요청당 평균 전체 실행 시간 |
총 테스트 시간 | 약 37.3초 | 전체 부하 테스트가 완료되기까지 걸린 시간 |
전체 리소스 사용량


어플리케이션별 리소스 사용량


APM 결과(API 지연 시간, 호출 수, 에러율)

🔹 2차 테스트 결과
K6
최대 VU 수 (vus_max) | 100 | 동시에 실행된 최대 가상 사용자 수 (Virtual Users) |
---|---|---|
총 요청 수 (http_reqs) | 955 | 전체 테스트 동안 실행된 HTTP 요청 수 |
평균 응답 시간 (avg) | 294.63ms | 전체 요청의 평균 응답 시간 |
최대 응답 시간 (max) | 2.26s | 가장 느린 요청의 응답 시간 |
90% 응답 시간 (p90) | 609.03ms | 90%의 요청이 이 시간 이하로 응답됨 |
95% 응답 시간 (p95) | 1.34s | 95%의 요청이 이 시간 이하로 응답됨 |
에러율 (http_req_failed) | 0.00% | 실패한 HTTP 요청의 비율 |
성공률 (checks_succeeded) | 100.00% | 성공 조건(응답 200, 유효 JSON)을 만족한 요청 비율 |
요청 완료 시간 (iteration_duration.avg) | 3.3s | 1회 요청당 평균 전체 실행 시간 |
총 테스트 시간 | 약 38.2초 | 전체 부하 테스트가 완료되기까지 걸린 시간 (running 기준) |
전체 리소스 사용량


어플리케이션별 리소스 사용량


APM 결과(API 지연 시간, 호출 수, 에러율)


🔹 3차 테스트 결과
K6
항목 | 값 | 정의 |
---|---|---|
최대 VU 수 (vus_max) | 500 | 동시에 실행된 최대 가상 사용자 수 (Virtual Users) |
총 요청 수 (http_reqs) | 1456 | 전체 테스트 동안 실행된 HTTP 요청 수 |
평균 응답 시간 (avg) | 6.86s | 전체 요청의 평균 응답 시간 |
최대 응답 시간 (max) | 20.76s | 가장 느린 요청의 응답 시간 |
90% 응답 시간 (p90) | 10.48s | 90%의 요청이 이 시간 이하로 응답됨 |
95% 응답 시간 (p95) | 11.9s | 95%의 요청이 이 시간 이하로 응답됨 |
에러율 (http_req_failed) | 0.00% | 실패한 HTTP 요청의 비율 |
성공률 (checks_succeeded) | 100.00% | 성공 조건(응답 200, 유효 JSON)을 만족한 요청 비율 |
요청 완료 시간 (iteration_duration.avg) | 10.04s | 1회 요청당 평균 전체 실행 시간 |
총 테스트 시간 | 약 1분 5.2초 | 전체 부하 테스트가 완료되기까지 걸린 시간 (running 기준) |
❗리소스 부족으로 서버가 다운됨
전체 리소스 사용량
어플리케이션별 리소스 사용량
APM 결과(API 지연 시간, 호출 수, 에러율)
- 동시 요청 100건에서는 평균 1.4초, 최대 4.71초의 응답 시간이 측정됨. 이는 다소 높은 수준으로, 대기 시간이 사용자에게 인지될 수 있음.
- 동시 요청 500건에서는 서버가 일시적으로 다운되는 현상이 발생하였고, 평균 응답 시간은 7.4초, 최대 응답 시간은 22.52초로 서비스 품질에 심각한 영향을 미칠 수 있는 수준임.
- 반면 10명 동시 요청 환경에서는 1000건의 요청이 있었음에도 평균 24.93ms, 최대 107.31ms로 매우 빠르고 안정적인 응답을 보임.
- 또한 APM Trace 결과에 따르면, DB 쿼리 처리 속도는 빠르지만, 어플리케이션 처리 속도에 지연이 있음을 알 수 있음. 이는 서버의 스레드가 부족하거나 JVM의 잦은 GC 수행으로 인한 문제일 가능성이 있으며, 사용자 경험에 심각한 영향을 줄 수 있는 지점임.
- 100명 동시 요청 시, CPU 사용률이 90%에 육박하고, 메모리 역시 4GB 전체를 소진하는 등 자원 사용률이 매우 높음.
- 500명 동시 요청 시, CPU와 메모리 사용량이 급증하면서 시스템이 응답 불가 상태에 도달함.
- 이 때 평균 응답 시간은 6.86초, 최대 20.76초, p90 10.48초, p95 11.9초로 사용자 경험에 부정적인 영향을 미치는 수준임.
- 반면 10명 동시 요청 시에는 CPU 사용률이 일시적으로 증가하더라도 전반적으로 안정적인 사용률과 빠른 응답 시간을 유지함.
- 모든 테스트에서 HTTP 요청 실패율은 0%, 기능적 오류 없이 정상 응답이 반환되었음.
- 현재 응답 지연은 주로 요청 수 증가에 따른 서버 자원 부족에서 발생함.
- 특히, Trace에서 확인된 어플리케이션 처리 지연은 리소스 부족으로 인한, 메모리 부족, 스레드 부족, 잦은 GC 수행 등의 문제를 일으켰을 가능성이 높음.
- 현재 서버 자원으로는 100건 이상의 동시 요청을 안정적으로 처리하기 어려우며, 인프라 확장 또는 구조적 최적화가 필요하다.
- 현재 도커 컨테이너 환경으로의 마이그레이션을 통해 nextjs, springboot, mysql을 각 각의 인스턴스로 분리 예정이므로, 마이그레이션 전에는 사용자 수 100명에 맞춰 인스턴스 리소스를 CPU 2코어, RAM 8GB까지 늘리는 것을 제안한다.
- 테스트 스크립트:
/home/devops/stress-testing/get-message-test.js
테스트 스크립트
import http from 'k6/http';
import { sleep, check } from 'k6';
// 테스트 구성 옵션
export const options = {
vus: 500, // 50명의 가상 사용자
duration: '30s', // 30초간 테스트
};
// API 구성 설정
const API_BASE_URL = '<https://dev.hertz-tuning.com>'; // 실제 API 도메인으로 변경 필요
const API_ENDPOINT = '/api/v1/channel-rooms';
const CHANNEL_ROOM_ID = 3;
const PAGE_SIZE = 20;
const ACCESS_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI3IiwiaWF0IjoxNzQ4MzI5ODUxLCJleHAiOjE3NDgzNDA2NTF9.tdUIMlG3blrZq6c8lrg74W9skqFk_h7QmKUhQ2JcduM';// 사전 테스트 실행 - 시작 전 API 가 정상 작동하는지 확인
export function setup() {
// API URL 구성
const url = `${API_BASE_URL}${API_ENDPOINT}/${CHANNEL_ROOM_ID}?page=0&size=${PAGE_SIZE}`;
// 헤더 설정
const params = {
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
};
console.log('사전 테스트: API 연결 확인 중...');
console.log(`요청 URL: ${url}`);
// API 호출
const res = http.get(url, params);
console.log(`사전 테스트 응답 상태: ${res.status}`);
console.log('응답 본문:');
console.log(res.body);
// 응답 검사
const success = res.status === 200;
// 테스트 실패 시
if (!success) {
console.error('사전 테스트 실패. API 연결을 확인하세요.');
console.error(`응답 상태: ${res.status}`);
console.error(`응답 본문: ${res.body}`);
throw new Error('사전 테스트 실패로 테스트를 중단합니다.');
}
console.log('사전 테스트 성공. 5초 후 테스트를 시작합니다...');
sleep(5);
return { accessToken: ACCESS_TOKEN };
}
// 메인 테스트 함수
export default function (data) {
// API URL 구성 (무작위 페이지)
const page = Math.floor(Math.random() * 1); // 0-2 페이지
const url = `${API_BASE_URL}${API_ENDPOINT}/${CHANNEL_ROOM_ID}?page=${page}&size=${PAGE_SIZE}`;
// 헤더 설정
const params = {
headers: {
'Authorization': `Bearer ${data.accessToken}`,
'Content-Type': 'application/json',
},
};
// API 호출
const res = http.get(url, params);
// 응답 검증 (선택적)
check(res, {
'API 응답이 성공적임 (200)': (r) => r.status === 200,
'응답이 유효한 JSON 형식임': (r) => {
try {
JSON.parse(r.body);
return true;
} catch (e) {
return false;
}
},
});
// 3초 대기 (풀링 주기)
sleep(3);
}