Spring/Ticketing 프로젝트
공연 조회 API 성능 측정 및 개선 사안 찾아보기
minturtle
2024. 11. 16. 00:06
반응형
개요
- 이번에는 저번 글에서 개선한 API에 대해 RPS가 얼마나 찍히는지, 병목지점은 어디인지 확인해 보는 시간을 갖기로 했습니다.
- API는 공연의 리스트를 조회하는 페이지로, 테스트는 1~10페이지를 조회하는 것으로 결정하였습니다.
- 성능 테스트는 어떠한 특정 목표를 갖기 보다는, 성능 측정 → 개선 과정을 거치면서 제가 할 수 있는한 최대한의 처리량과 최소한의 응답시간을 갖는 것을 목표로 정했습니다.
서버 구조
일단 저희가 테스트할 서버의 구조 중, 현재 API에서 사용하는 구조는 다음과 같습니다.
- 여기서 nginx와 Spring은 같은 서버내에 위치하고 있으며 Docker로 구분되어 있습니다. User - Nginx간의 접속은 Https로 연결되며, Nginx - Spring 간의 연결은 http로 변환되어 연결됩니다. 그리고 서버 자원이 모잘라 현재 NGINX와 Spring이 한 서버에 띄워져 있는 상태입니다.
현재 보고 계신 글은 공연 조회 API 최적화 시리즈 입니다!
1편 :공연 정보 조회 API 쿼리 분석하고 개선하기
2편 :공연 조회 API 성능 측정 및 개선 사안 찾아보기
3편 :공연 조회 API에 캐싱을 적용하고 성능 테스트하기
4편 :아키텍처 최적화, 로그 방식 변경을 통한 공연 정보 조회 API 최적화 하기
서버 성능
- 서버(Spring, Nginx)의 성능 정보는 다음과 같습니다.
💡 서버 : 2 물리 코어(2 OCPU), 12GB RAM, 12GB Swap Memory
- DB 서버의 성능 정보는 다음과 같습니다.
💡 DB : 2 논리 코어(1OCPU) , 6GB RAM, 12GB Swap Memory
- 데이터의 갯수는 다음과 같습니다.
Performance: 150만 개 PerformanceDateTime : 330만 개
테스트 스크립트
import http from 'k6/http';
import { check, sleep } from 'k6';
import { URL } from 'https://jslib.k6.io/url/1.0.0/index.js';
export const options = {
scenarios: {
ramp_up_scenario: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '5m', target: 50 },
{ duration: '5m', target: 100 },
{ duration: '5m', target: 200 },
{ duration: '5m', target: 300 },
{ duration: '10m', target: 300 },
],
gracefulRampDown: '2m',
},
},
};
const BASE_URL = 'https://minturtle.kro.kr/api/performances';
const MAX_PAGE = 10
export default function ({ cursors }) {
let page = Math.floor(Math.random() * 10)
let url = new URL(BASE_URL);
// 0페이지는 커서를 설정하지 않음.
if (page > 0 && cursors[page - 1] !== null) {
url.searchParams.append('cursor', cursors[page - 1]);
}
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
const res = http.get(url.toString(), {
headers: headers
});
check(res, {
'status is 200': (r) => r.status === 200
});
}
// 2~10페이지 까지의 Cursor 정보를 조회하여 테스트에 전달
export function setup() {
let cursors = []
let currentCursor = null;
for (let i = 0; i < MAX_PAGE - 1; i++) {
let url = new URL(BASE_URL);
if (currentCursor) {
url.searchParams.append('cursor', currentCursor);
}
const response = http.get(url.toString(), {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: '10s'
});
try {
const data = response.json();
if (data.cursor) {
cursors[i] = data.cursor;
currentCursor = data.cursor;
}
console.log(`Setup: Initialized cursor for page ${i + 1}`);
} catch (e) {
console.error(`Failed to initialize cursor for page ${i + 1}:`, e.message);
}
// API 부하 방지를 위한 대기
sleep(0.1);
}
console.log('테스트 시작 - 모든 페이지의 커서 초기화 완료');
console.log(cursors)
return { cursors }
}
- 먼저 테스트 스크립트에 대한 설명을 드리자면 아래와 같습니다.
- setup 메서드는 성능 테스트를 하기 전에 최초 1회 실행되는 메서드인데요, setup 부분에서는 커서 페이지네이션에 사용할 커서를 미리 가져와서 사용자 시나리오에서 사용할 수 있게 커서 리스트를 반환해줍니다.
- export default function 이 가상 사용자가 실행하는 시나리오입니다. 이 스크립트는 아래의 과정을 거칩니다.
- 1~10페이지 중 하나를 랜덤으로 선택
- 1페이지가 아니라면 request parameter로 전달할 커서를 가져옴
- request 요청 및 응답이 200인지 체크
- 위 과정을 VUS 수 만큼 병렬로 수행하게 되는데요, VUS가 100명이라고 하고, 평균 응답시간이 1초라고 가정하면 RPS는 100이 나오게 됩니다. 평균 응답시간이 0.5초면 RPS는 300이 될 것이고요.
- 따라서 RPS의 계산식은 다음과 같이 유추할 수 있습니다.
RPS = VUS / (평균 응답시간)
테스트 결과
K6 성능 테스트 지표
- 먼저 전체적인 지표인데요, 평균적으로 0.5ms 만에 응답을 받을 수 있었으며, 60만개의 요청 중 4개의 요청이 Timeout 되었습니다. 최대 RPS는 약 450 정도 측정되었습니다.
- 응답시간은 RPS에 따라 비례해 증가하는 것을 알 수 있었는데요, 95%사용자가 1.4초만에 응답을 받음을 알 수 있었습니다.
JVM 지표
- Spring Actuator로 측정한 CPU와 메모리 정보는 위와 같은데요, System CPU가 100%를 달성하였고, JVM Process는 그중 60%대를 사용하고 있는 것으로 보입니다. JVM Memory는 안정적입니다(오히려 많이 남아서 설정을 통해 줄여야 할것으로 보이네요)
- 다음으로 GC인데요, GC가 많이 일어나긴 했지만 Old GC가 발생하지는 않았고, Minor GC가 몇번 발생한 것으로 보입니다.
- 그 다음으로 Thread Pool 지표입니다. max Thread Pool을 300으로 설정해놨었는데, 현재 테스트는 최대 동시에 300명의 사용자가 요청을 보내기 때문에 괜찮지만, VUS를 더 늘리면 Thread Pool도 늘려줘야 할 것으로 보입니다.
- 마지막으로 Connection Pool인데요, Connection Pool을 얻으려고 대기하는 Thread가 최대 260개 까지 생기는 것을 확인할 수 있었습니다. 이를 해결하기 위해 Connection Pool의 갯수를 조절해야할 것으로 보입니다.
Java & Nginx Server 지표
- 다음으론 CPU, Memory, Disk IO에 대한 지표인데요, java와 nginx가 대부분의 CPU를 사용하는 것으로 보입니다.
- 두번째 그림에서 CPU는 System(초록색), User(파란색), Soft IRQ(빨간색)이 많이 차지 했는데요, Soft IRQ는 웹 검색을 통해 찾아 보니 네트워크 패킷 송/수신, I/O 처리 등에 사용된다고 합니다.
- 다음으론 Disk I/O인데요, 평균적으로 약 1.75MB/s의 속도로 Disk Write가 발생하고 있는 것으로 보입니다. 이는 로그 데이터로 보입니다.
MySQL 서버 지표
- 다음으론 MySQL 성능 지표인데요, Slow Query도 발생하지 않았고, CPU와 메모리 또한 안정적으로 보였습니다.
개선해야할 점은 어디일까?
- 위 지표들을 봤을때 병목이 발생할 수 있는 부분은 다음과 같아 보입니다.
- Connection Pool의 갯수
- 지표에서도 알 수 있듯 Connection Pool의 갯수가 너무 적기 때문에 Connection을 얻기 위해 대기 상태에 놓인 Thread가 많은 것을 확인할 수 있었습니다. 이를 해결하기 위해서 Hikari CP의 갯수를 늘리거나 캐시의 도입이 필요해 보입니다.
- 과한 System CPU, Cache Memory
- 요청이 많아짐에 따라 System CPU와 Soft IRQ의 비율이 증가하는 문제가 있었습니다. 이는 로그를 동기적으로 File에 쓰면서 생기는 문제가 아닐까 하는 의심이 드는데, 나중에 로그 저장 방식에 대해 최적화를 할 필요가 있어 보입니다.
- Connection Pool의 갯수
- 저는 일단 1번부터 해결할 생각인데요, Connection Pool의 갯수를 늘려서도 해결이 가능하지만 Cache의 도입을 통해 해결해 보도록 하겠습니다.
반응형