본문 바로가기

Proj/Tripot

k6을 이용한 성능 테스트

1. 개요

 이번에는 원활한 유지보수를 위해 성능 테스트를 해보고자 한다. 성능 테스트를 통해 가상의 사용자를 만들어 서비스를 이용시킨 후 그 성능을 측정할 수 있다. 만약 생각하는 성능에 못 미칠 경우 개선하는 방향을 생각해볼 수 있다. 이번에는 처음 사용하는 만큼 로컬에서 스크립트를 구성하고, 실행해볼 것이다.

 

2. K6 vs Jmeter

 성능 테스트 툴은 여러 가지가 있지만, 두 가지 도구를 놓고 비교하였다.

2-1. K6

 

  • Go 기반으로 Grafana Labs에서 개발된 성능 테스트 툴
  • 상대적으로 리소스를 덜 사용함
  • javascript를 사용한 스크립트 작성 및 실행 가능

2-2. Jmeter

  • Java 기반으로 개발된 오픈 소스 성능 테스트 툴
  • 리소스를 많이 사용함
  • GUI가 잘 되어있어 러닝커브가 상대적으로 낮음
  • xml을 이용한 테스트 스크립트 작성

 

 

2-3. 선택

이 중 k6을 사용하기로 결정했다. 사용하게 된 근거는 다음과 같다.

  • jmeter에 비해 리소스를 덜 사용한다.
  • 상대적으로 스크립트 작성이 간편하다.
  • Grafana에서 개발했으므로 현재 사용하는 모니터링 툴과 연결이 편하다.

 

3. 테스트 시나리오 작성

 Tripot 프로젝트의 기본적인 동작 방식을 간단히 작성하면 다음과 같다.

 우선 카카오 OAuth2를 통해 정보를 얻는다. 닉네임 등 추가 정보를 받는다.

 이후 본인이 작성한 글, 추천 여행지, 최근 많이 간 곳 등 스토리의 리스트가 주어진다.

 

위의 동작방식을 기반으로 시나리오를 다음과 같이 작성했다.

 

  1. 회원 가입
    1. 소셜 로그인: /api/v1/oauth2/kakao(post)
    2. 추가 정보 입력(회원 활성화): /api/v1/members/activate(patch)
  2. 스토리 작성
    1. 스토리 작성: /api/v1/stories/new(post)
  3. 스토리 조회(둘러보기)
    1. 지금 인기있는 스토리: /api/v1/public/stories/recommended/recent-popular-story (get)
    2. 오늘의 추천 여행지: /api/v1/public/stories/recommended/recent-popular-city (get)
    3. 최근 많이 가는 곳: /api/v1/public/stories/recommended/random (get)
    4. 전체 스토리 조회: /api/v1/public/stories/recommended/search(get)

4. 테스트 스크립트 작성

k6 관련 코드는 다음 문서에서 확인할 수 있다.

https://grafana.com/docs/k6/latest/javascript-api/k6-http/

 

k6/http | Grafana k6 documentation

User-centered observability: load testing, real user monitoring, and synthetics Learn how to use load testing, synthetic monitoring, and real user monitoring (RUM) to understand end users' experience of your apps. Watch on demand.

grafana.com

 

 이 중 스크립트 작성에 필요한 기본적인 것들을 알아보자.

import http from 'k6/http';
import { sleep } from "k6";

해당 import로 k6에 필요한 것들을 불러올 수 있다.

export let options = {
  insecureSkipTLSVerify: true,                  //TLS 생략
  noConnectionReuse: false,                     //true: disable keep-alive connections
  stages: [                                     //VUs 수 상향/하향 시나리오
    { duration: "1m", target: 1000 },           //점진적으로 증가하여 1분 후 VUs 수가 1_000명 달성
    { duration: "10s", target: 0 },             //이후 10초간 VUs 수가 0까지 하락
  ],

//  //95% 유저가 100ms 안에 응답받는 것을 목표
//  thresholds: {
//      http_req_duration: ['p(95)<100'],
//  }
};

options를 위와 같이 작성하여 가상 사용자 수 시나리오를 작성하거나, threshold 등 갖가지 옵션을 설정할 수 있다.

 

export default function () {

	const serverUrl = 'http://localhost:8080';

    const headers = {
        "Content-Type": "application/json",
    };

   //가상 소셜 로그인 body
    const loginRequest = JSON.stringify({
        id: generateRandomNumber(8),
        nickname: generateRandomString(10),
    });

    //회원 가입(소셜 로그인)
    const tokenResponse = http.post(`${serverUrl}/api/v1/login/oauth2/kakao`, loginRequest, {
        headers: headers
    });

    let accessToken;
    try {
        accessToken = "Bearer " + JSON.parse(tokenResponse.headers).Authorization;
    } catch (error) {
        return;
    }

default function에 테스트 시나리오를 작성한다. 우선 소셜 로그인 후 response에서 액세스 토큰을 받아온다.

const AuthorizationHeaders = {
        "Content-Type": "application/json",
        Authorization: accessToken,
    };


   //해당 회원 활성화(로그인)
    const activateRequest = JSON.stringify({
        nickname: generateRandomString(10),
        recommendLocation: generateRandomString(5)
    });

    http.patch(`${serverUrl}/api/v1/members/activate`, loginRequest, { headers: AuthorizationHeaders });

 해당 토큰을 넣고, 회원 추가정보 입력을 진행한다.

    //각 회원의 스토리 작성

    const myStoryRequest=JSON.stringify({
                 title: generateRandomString(10),
                 content: generateRandomString(100),
                 city: generateRandomString(10),
//                 thumbnailImg,
                 latitude: 0.0,
                 longitude: 0.0,
                 isHidden: false,
//                 imgUrls
    });


    http.post(`${serverUrl}/api/v1/stories/new`, myStoryRequest, {headers: AuthorizationHeaders});

    //지금 인기있는 스토리 조회
    http.get(`${serverUrl}/api/v1/public/stories/recommended/recent-popular-story`, {headers: AuthorizationHeaders});

    //오늘의 추천 여행지 조회
    http.get(`${serverUrl}/api/v1/public/stories/recommended/recent-popular-city`, {headers: AuthorizationHeaders});

    //최근 많이 가는 곳 조회
    http.get(`${serverUrl}/api/v1/public/stories/recommended/random`, {headers: AuthorizationHeaders});

    //전체 스토리 조회
    http.get(`${serverUrl}/api/v1/public/stories/recommended/search`, {headers: AuthorizationHeaders});
    
    
    sleep(1);

}

 스토리를 작성하고, 스토리를 갖가지 방식으로 조회한다. sleep(1)은 실제 서비스와 유사한 환경을 구사하기 위한 것이다. "한 번 요청하고 나면 같은 요청을 1초 동안은 보내지 않을 것이다." 

k6 run test.js

해당 명령어로 성능 테스트를 진행할 수 있다.

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: test.js
        output: -

     scenarios: (100.00%) 1 scenario, 160 max VUs, 45s max duration (incl. graceful stop):
              * default: Up to 160 looping VUs for 15s over 2 stages (gracefulRampDown: 30s, gracefulStop: 30s)


     data_received..................: 38 MB 2.5 MB/s
     data_sent......................: 15 MB 1.0 MB/s
     http_req_blocked...............: avg=34.93µs  min=0s med=0s      max=47.49ms  p(90)=0s       p(95)=0s
     http_req_connecting............: avg=16.62µs  min=0s med=0s      max=17.17ms  p(90)=0s       p(95)=0s
     http_req_duration..............: avg=15.21ms  min=0s med=12.86ms max=177.94ms p(90)=29.32ms  p(95)=34.18ms
       { expected_response:true }...: avg=15.21ms  min=0s med=12.86ms max=177.94ms p(90)=29.32ms  p(95)=34.18ms
     http_req_failed................: 0.00% 0 out of 76165
     http_req_receiving.............: avg=407.49µs min=0s med=0s      max=122.94ms p(90)=704.56µs p(95)=1ms
     http_req_sending...............: avg=74.61µs  min=0s med=0s      max=59.31ms  p(90)=0s       p(95)=0s
     http_req_tls_handshaking.......: avg=0s       min=0s med=0s      max=0s       p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=14.73ms  min=0s med=12.55ms max=102.87ms p(90)=28.7ms   p(95)=33.12ms
     http_req_sending...............: avg=74.61µs  min=0s med=0s      max=59.31ms  p(90)=0s       p(95)=0s
     http_req_tls_handshaking.......: avg=0s       min=0s med=0s      max=0s       p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=14.73ms  min=0s med=12.55ms max=102.87ms p(90)=28.7ms   p(95)=33.12ms
     http_reqs......................: 76165 5077.624793/s
     iteration_duration.............: avg=15.76ms  min=0s med=13.33ms max=182.08ms p(90)=29.96ms  p(95)=35.33ms
     iterations.....................: 76165 5077.624793/s
     vus............................: 1     min=1          max=159
     vus_max........................: 160   min=160        max=160


running (15.0s), 000/160 VUs, 76165 complete and 0 interrupted iterations
default ✓ [======================================] 000/160 VUs  15s

 다음과 같이 결과가 나왔다. 필요한 정보를 살펴보자.

  • http_req_failed: http 요청이 실패한 비율
  • http_req_sending: 데이터를 전송하는 데 걸린 시간
  • http_req_waiting: 응답을 기다리는 데 걸린 시간
  • http_req_receiving: 응답을 받는 데 걸린 시간
  • http_req_duration: 요청을 받고 응답하는 데 걸린 시간, p(90): 요청 90%의 응답 시간, min, med, max
  • vus: 가상 사용자 수