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. 1~10페이지 중 하나를 랜덤으로 선택
    2. 1페이지가 아니라면 request parameter로 전달할 커서를 가져옴
    3. request 요청 및 응답이 200인지 체크
  • 위 과정을 VUS 수 만큼 병렬로 수행하게 되는데요, VUS가 100명이라고 하고, 평균 응답시간이 1초라고 가정하면 RPS는 100이 나오게 됩니다. 평균 응답시간이 0.5초면 RPS는 300이 될 것이고요.
  • 따라서 RPS의 계산식은 다음과 같이 유추할 수 있습니다.
RPS = VUS / (평균 응답시간)

 

테스트 결과

K6 성능 테스트 지표

K6 전체 지표

 

  • 먼저 전체적인 지표인데요, 평균적으로 0.5ms 만에 응답을 받을 수 있었으며, 60만개의 요청 중 4개의 요청이 Timeout 되었습니다. 최대 RPS는 약 450 정도 측정되었습니다.

응답 시간

  • 응답시간은 RPS에 따라 비례해 증가하는 것을 알 수 있었는데요, 95%사용자가 1.4초만에 응답을 받음을 알 수 있었습니다.

 

JVM 지표

CPU Usage
Memory Usage

  • Spring Actuator로 측정한 CPU와 메모리 정보는 위와 같은데요, System CPU가 100%를 달성하였고, JVM Process는 그중 60%대를 사용하고 있는 것으로 보입니다. JVM Memory는 안정적입니다(오히려 많이 남아서 설정을 통해 줄여야 할것으로 보이네요)

GC Info

  • 다음으로 GC인데요, GC가 많이 일어나긴 했지만 Old GC가 발생하지는 않았고, Minor GC가 몇번 발생한 것으로 보입니다.

 

Thread Pool

  • 그 다음으로 Thread Pool 지표입니다. max Thread Pool을 300으로 설정해놨었는데, 현재 테스트는 최대 동시에 300명의 사용자가 요청을 보내기 때문에 괜찮지만, VUS를 더 늘리면 Thread Pool도 늘려줘야 할 것으로 보입니다.

Database Connection Pool

  • 마지막으로 Connection Pool인데요, Connection Pool을 얻으려고 대기하는 Thread가 최대 260개 까지 생기는 것을 확인할 수 있었습니다. 이를 해결하기 위해 Connection Pool의 갯수를 조절해야할 것으로 보입니다.

Java & Nginx Server 지표

CPU Usage(top)
CPU, Memory Usage
I/O Write

  • 다음으론 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 서버 지표

Slow Queries
CPU, Memory Usage

  • 다음으론 MySQL 성능 지표인데요, Slow Query도 발생하지 않았고, CPU와 메모리 또한 안정적으로 보였습니다.

 

개선해야할 점은 어디일까?

  • 위 지표들을 봤을때 병목이 발생할 수 있는 부분은 다음과 같아 보입니다.
    1. Connection Pool의 갯수
      • 지표에서도 알 수 있듯 Connection Pool의 갯수가 너무 적기 때문에 Connection을 얻기 위해 대기 상태에 놓인 Thread가 많은 것을 확인할 수 있었습니다. 이를 해결하기 위해서 Hikari CP의 갯수를 늘리거나 캐시의 도입이 필요해 보입니다.
    2. 과한 System CPU, Cache Memory
      • 요청이 많아짐에 따라 System CPU와 Soft IRQ의 비율이 증가하는 문제가 있었습니다. 이는 로그를 동기적으로 File에 쓰면서 생기는 문제가 아닐까 하는 의심이 드는데, 나중에 로그 저장 방식에 대해 최적화를 할 필요가 있어 보입니다.
  • 저는 일단 1번부터 해결할 생각인데요, Connection Pool의 갯수를 늘려서도 해결이 가능하지만 Cache의 도입을 통해 해결해 보도록 하겠습니다.
반응형