[k6]JavaScript로 성능 테스트 하기

JavaScript로 성능/부하테스트를 할 수 있는 서비스인 k6와 테스트 시나리오를 작성하는 방법을 설명합니다.
2022.08.15

안녕하세요. 클래스메소드JP CX사업본부의 임홍기입니다.

여러분들은 부하 테스트나 성능 테스트라는 말을 들어본 적 있나요?

프로덕트의 성능을 테스트하기 위해서 또는 대량의 액세스를 받았을 때 프로덕트가 얼마나 많은 액세스를 견딜 수 있는지 확인하는등 프로덕트가 가지고 있는 성능의 한계를 측정하기 위해서 수행하는 테스트입니다.

오늘은 수많은 성능 테스트 서비스 중에 JavaScript로 테스트 시나리오를 작성할 수 있는 k6라는 툴을 소개합니다.

k6

k6의 실행이미지는 아래와 같습니다. (k6 GitHub)

k6

k6을 한마디로 말하면 JS로 성능 테스트의 시나리오를 작성하는 툴로 프로덕트의 성능과 부하테스트를 목표로 하고 있는 OSS입니다.

또한 k6 커뮤니티에서는 시나리오 작성을 유연하게 하기 위해 Postman이나 Swagger, Open API 등으로 정의한 API도 정의한 API그대로 시나리오에 이용할 수 있도록 대응하고 있습니다.

API실행에서 자주 사용되는 Postman의 경우 postman-to-k6등의 툴에서 바로 스크립트를 작성하는 것도 가능합니다!

test-api-postman-collection

Postman에서 이용하던 API를 Export하여 변환하면 다음과 같습니다.
자세한 내용은 load-testing-with-postman-collections를 참조해주세요!

export default function () {
  group("Public APIs", function () {
    postman[Request]({
      name: "List all public crocodiles",
      id: "3ddd46c4-1618-4883-82ff-1b1e3a5f1091",
      method: "GET",
      address: "{{BASE_URL}}/public/crocodiles/",
    });

    postman[Request]({
      name: "Get a single public crocodile",
      id: "9625f17a-b739-4f91-af99-fba1d898953b",
      method: "GET",
      address: "{{BASE_URL}}/public/crocodiles/1/",
    });
  });
}

Lifecycle

k6의Lifecycle은 크게 네가지입니다.

// 1. 초기화
// 2. 전처리
export function setup() {
  // 로그인 토큰취득 등 API실행전에 필요한 처리 구현
}
// 3. API실행(시나리오 실행)
export default function (data) {
  // API를 실행할 시나리오를 구현
}
// 4. API 실행후 처리
export function teardown(data) {
  // API 실행후에 필요한 처리가 있다면 구현
}

시나리오 작성 및 실행

설치

$ brew install k6

로그인이 필요한 간단한 API을 실행하는 스크립트를 작성하면서 이해해봅시다!

스크립트 작성

script.js

import http from "k6/http";
import { sleep, check } from "k6";
import { Trend } from "k6/metrics";

const trends = {
  scenario1: new Trend("scenario1) Response time", true),
  scenario2: new Trend("scenario2) Response time", true),
};

const VUS = 1;
const DURATION = "10s";

export const options = {
  scenarios: {
    scenario1: {
      executor: "constant-vus",
      exec: "scenarioFunc",
      vus: VUS,
      duration: DURATION,
      env: {
        SCENARIO_ID: "1",
      },
    },
    scenario2: {
      executor: "constant-vus",
      exec: "scenarioFunc",
      vus: VUS,
      duration: DURATION,
      env: {
        SCENARIO_ID: "2",
      },
    },
  },
};

export function setup() {
  const url = ""; // token취득
  const params = {
    headers: {
      Authorization: "Bearer XXX",
    },
  };
  const res = http.get(url, params);
  const token = JSON.parse(res.body).account.token;

  return token;
}

export function scenarioFunc(token) {
  const scenarioUrl = ""; // 실행할 API
  const scenario = http.get(scenarioUrl, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });
  __ENV.SCENARIO_ID === "1"
    ? trends.scenario1.add(scenario.timings.duration)
    : trends.scenario2.add(scenario.timings.duration);

  check(scenario, {
    "scenario status is 200": (res) => res.status === 200,
  });

  sleep(1);
}

설명

위의 스크립트에 대한 해설입니다. 먼저 options 설정입니다.

$ k6 run --vus 10 --duration 30s script.js

k6는 CLI에서 실행할 수 있습니다. 실행시 파라미터를 지정하는 방법으로도 실행할 수 있지만, 스크립트 코드안에 파라미터를 지정하고 실행하는 것도 가능합니다.

export const options = {
  scenarios: {
    scenario1: {
      executor: "constant-vus",
      exec: "scenarioFunc",
      vus: VUS,
      duration: DURATION,
      env: {
        SCENARIO_ID: "1",
      },
    },
  },
};

executor: k6의 실행 엔진을 나타냅니다.
여기에서 VU(Virtual user)나 스크립트 실행 패턴을 지정할 수 있습니다.
자세한 내용은 k6의 executors를 참조해주세요.

exec: 실행하고자 하는 시나리오를 지정합니다.

vus: Virtual Users API를 실행할 가상 유저입니다. 필요한 만큼의 병렬 실행수를 여기에 설정합니다.

duration: VUS가 반복 시나리오를 실행하는 시간을 설정합니다.

env: 공통으로 사용되는 변수를 설정할 수 있습니다.

const trends = {
  scenario1: new Trend("scenario1) Response time", true),
  scenario2: new Trend("scenario2) Response time", true),
};

Trend는 실행 결과에 포함할 User 지정 Metric입니다. 이번에는 시나리오별 응답 시간을 통해 실행 결과에 포함시키기 위해 이용했습니다.
Trend를 추가하면 아래와 같이 실행 결과에서 볼 수 있습니다.

scenario1) Response time.......: avg=1.29s    min=1.26s    med=1.29s max=1.35s    p(90)=1.34s    p(95)=1.34s
scenario2) Response time.......: avg=1.29s    min=1.25s    med=1.29s max=1.36s    p(90)=1.32s    p(95)=1.34s

다음은 'setup`함수에 대해서입니다.

export function setup() {
  const url = "";
  const params = {
    headers: {
      Authorization: "Bearer XXX",
    },
  };
  const res = http.get(url, params);
  const token = JSON.parse(res.body).account.token;

  return token;
}

여기에서는 토큰 취득 등 시나리오를 실행하기 전에 실행되어야할 처리를 설정하는 곳입니다.
로그인이 필요한 서비스를 가정하여 토큰을 취득했습니다.

다음은 작성한 시나리오를 실행하는 함수입니다.

export function scenarioFunc(token) {
  const scenarioUrl = "";
  const scenario = http.get(scenarioUrl, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });
  __ENV.SCENARIO_ID === "1"
    ? trends.scenario1.add(scenario.timings.duration)
    : trends.scenario2.add(scenario.timings.duration);

  check(scenario, {
    "scenario status is 200": (res) => res.status === 200,
  });

  sleep(1);
}

여기서 실행하고싶은 시나리오를 작성합니다.
이번에는 같은 API를 실행하는 시나리오를 2개 준비했기 때문에 그에따라 Trend를 실행 시나리오 아이디로 분리했습니다.

check메서드는 값에 대해 true/false를 반환합니다.
여기에서는 API실행이 성공했는지 실패했는지 확인하고 결과에 기록합니다.
실패해도 중간에 멈추지 않고 실행하기 때문에 유연하게 대응할 수 있습니다.

실행결과

위 스크립트를 실행한 결과입니다.

image

실행 결과의 설명

checks

  • 결과
    • 100.00% ✓ 16 ✗ 0
  • 의미
    • 요청이 성공한 비율(%)

data_received

  • 결과
    • 176 kB 15 kB / s
  • 의미
    • 응답한 데이터 양 (Total, /s)

data_sent

  • 결과
    • 3 . 4 kB 282 B/s
  • 의미
    • 요청한 데이터 양 (Total, /s)

http_req_blocked

  • 결과
    • avg=40.62ms min=0s med=1µs max=430.85ms p(90)=129.65ms p(95)=191.16ms
  • 의미
    • TCP 접속 대기시간(avg, min, med, max, p(90), p(95)

http_req_connecting

  • 결과
    • avg=7.32ms min=0s med=0s max=42.24ms p(90)=41.03ms p(95)=41.69ms
  • 의미
    • TCP 접속에 걸린시간(avg, min, med, max, p(90), p(95)

http_req_duration

  • 결과
    • avg=1.27s min=986.44ms med=1.28s max=1.36s p(90)=1.34s p(95)=1.35s
  • 의미
    • http_req_sending + http_req_waiting + http_req_receiveing(avg, min, med, max, p(90), p(95)

{ expected_response:true }

  • 결과
    • avg=1.27s min=986.44ms med=1.28s max=1.36s p(90)=1.34s p(95)=1.35s
  • 의미
    • 정상응답만 http_req_duration(avg, min, med, max, p(90), p(95) 정상응답이 없을 경우 이 항목은 표시되지 않음

http_req_failed

  • 결과
    • 0.00% ✓ 0 ✗ 17
  • 의미
    • 요청이 실패한 비율(%)

http_req_receiving

  • 결과
    • avg=194.76µs min=78µs med=95µs max=994µs p(90)=420.4µs p(95)=761.19µs
  • 의미
    • 응답의 1바이트가 도달하고 나서 마지막 바이트를 수신할 때까지의 시간(avg, min, med, max, p(90), p(95)

http_req_sending

  • 결과
    • avg=139.76µs min=38µs med=48µs max=1.21ms p(90)=166.4µs p(95)=401.99µs
  • 의미
    • 요청을 전송하는데 걸린시간(avg, min, med, max, p(90), p(95)

http_req_tls_handshaking

  • 결과
    • avg=29.7ms min=0s med=0s max=328.35ms p(90)=88.19ms p(95)=136.75ms
  • 의미
    • TLS 세션의 핸드쉐이크에 걸린 시간(avg, min, med, max, p(90), p(95) http에서는 0

http_req_waiting

  • 결과
    • avg=1.27s min=985.15ms med=1.28s max=1.36s p(90)=1.34s p(95)=1.35s
  • 의미
    • 요청이 전송 완료된 후 응답이 시작될 때까지의 시간(avg, min, med, max, p(90), p(95) TTFB(Time To First Byte)

http_reqs

  • 결과
    • 17 1.423196/s
  • 의미
    • 총 리퀘스트수 (Total, /s)

iteration_duration

  • 결과
    • avg=1.31s min=1.25s med=1.29s max=1.49s p(90)=1.44s p(95)=1.48s
  • 의미
    • 시나리오 1회 반복에 걸린 시간(avg, min, med, max, p(90), p(95)

iterations

  • 결과
    • 16 1.339479/s
  • 의미
    • 시나리오 반복 횟수(Total, /s)

vus

  • 결과
    • 2 min=0 max=2
  • 의미
    • Virtual UserS, 시나리오 실행시 유저수(병렬)

vus_max

  • 결과
    • 2 min=0 max=2
  • 의미
    • 최대 Virtual UserS, 시나리오의 최대 실행유저수(병렬)

참조

k6 docs
postman-to-k6
Load Testing Your API with Postman