Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

부하 테스트 도구 Grafana k6

k6Grafana Labs에서 만든 오픈소스 부하 테스트 도구(load testing tool)이다. 부하 테스트는 간단히 쓸 수 있는 ab가 있고 유명한 도구로는 Apache JMeternGrinder가 보통 유명한 것 같고 난 GatlingArtillery를 사용해 봤고 간단히는 autocannon같은 것도 써봤지만 딱히 만족스럽게 어디 정착을 못 하고 있었다.

부하 테스트를 매번 하는 것도 아니고 또 상황에 따라 요구사항이 복잡할 때도 있고 간단히 트래픽만 좀 흘려보길 원할 때도 있어서 매번 다르게 사용했던 것 같다. 딱히 이전에도 만족스럽지 않아서였던지 할 때마다 매번 새로운 도구는 없나 하고 찾아보게 되는 것 같다. k6는 동료가 사용하는 걸 보고 알게 되었다가 최근에 간단히 사용해 보게 되어서 사용법을 정리해 본다.

설치

설치는 macOS 기준으로는 Homebrew로 설치할 수 있지만 개인적으로는 이런 도구는 릴리스 버전을 다운 받아 쓰는 걸 좋아해서 릴리스 페이지에서 플랫폼에 맞는 버전을 받아서 설치했다. 설치하고 나면 k6 version으로 버전을 확인해 볼 수 있다.

$ k6 version
k6 v0.39.0 (2022-07-05T10:50:14+0000/v0.39.0-0-g5904fd8, go1.18.3, darwin/amd64)


간단한 부하 테스트

K6에서 부하 테스트를 해볼 수 있는 test.k6.io를 제공하고 있어서 여기를 이용해서 실행해 볼 수 있다.(대부분은 공식 문서를 많이 참고 했다.)

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

export default function () {
  http.get('https://test.k6.io');
  sleep(1);
}

JavaScript로 작성한 간단한 부하 테스트 코드다. JavaScript로 시나리오를 작성할 수 있는 것도 마음에 들었다. 이 글에서는 간단한 사용방법을 살펴볼 예정이라 더 복잡한 시나리오는 다음에 살펴보려고 한다. 이렇게 작성한 테스트는 k6 run 명령어로 실행할 수 있다.

$ k6 run demo.js

          /\      |‾‾| /‾‾/   /‾‾/
     /\  /  \     |  |/  /   /  /
    /  \/    \    |     (   /   ‾‾\
   /          \   |  |\  \ |  (‾)  |
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: demo.js
     output: -

  scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
           * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)


running (00m01.9s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs  00m01.9s/10m0s  1/1 iters, 1 per VU

     data_received..................: 17 kB 8.7 kB/s
     data_sent......................: 438 B 227 B/s
     http_req_blocked...............: avg=619.69ms min=619.69ms med=619.69ms max=619.69ms p(90)=619.69ms p(95)=619.69ms
     http_req_connecting............: avg=364.24ms min=364.24ms med=364.24ms max=364.24ms p(90)=364.24ms p(95)=364.24ms
     http_req_duration..............: avg=302.73ms min=302.73ms med=302.73ms max=302.73ms p(90)=302.73ms p(95)=302.73ms
       { expected_response:true }...: avg=302.73ms min=302.73ms med=302.73ms max=302.73ms p(90)=302.73ms p(95)=302.73ms
     http_req_failed................: 0.00% ✓ 0        ✗ 1
     http_req_receiving.............: avg=673µs    min=673µs    med=673µs    max=673µs    p(90)=673µs    p(95)=673µs
     http_req_sending...............: avg=94µs     min=94µs     med=94µs     max=94µs     p(90)=94µs     p(95)=94µs
     http_req_tls_handshaking.......: avg=246.02ms min=246.02ms med=246.02ms max=246.02ms p(90)=246.02ms p(95)=246.02ms
     http_req_waiting...............: avg=301.96ms min=301.96ms med=301.96ms max=301.96ms p(90)=301.96ms p(95)=301.96ms
     http_reqs......................: 1     0.518954/s
     iteration_duration.............: avg=1.92s    min=1.92s    med=1.92s    max=1.92s    p(90)=1.92s    p(95)=1.92s
     iterations.....................: 1     0.518954/s
     vus............................: 1     min=1      max=1
     vus_max........................: 1     min=1      max=1

--vus는 가상 사용자(virtual users)이고 VU는 사실상 while(true) 루프를 돌리는 횟수라고 한다. --duration은 이 테스트를 돌릴 시간이므로 demo.js를 30초동안 10개씩 루프를 돌린다고 생각하면 된다. 예제에서 sleep(1);가 있으므로 총 요청은 300 요청(10 VUS * 30초)이 아니라 221 요청이 수행된 걸 알 수 있다.

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

          /\      |‾‾| /‾‾/   /‾‾/
     /\  /  \     |  |/  /   /  /
    /  \/    \    |     (   /   ‾‾\
   /          \   |  |\  \ |  (‾)  |
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: demo.js
     output: -

  scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
           * default: 10 looping VUs for 30s (gracefulStop: 30s)


running (0m30.5s), 00/10 VUs, 221 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs  30s

     data_received..................: 2.6 MB 84 kB/s
     data_sent......................: 25 kB  821 B/s
     http_req_blocked...............: avg=31.73ms  min=2µs      med=4µs      max=714.54ms p(90)=8µs      p(95)=40µs
     http_req_connecting............: avg=11.28ms  min=0s       med=0s       max=249.64ms p(90)=0s       p(95)=0s
     http_req_duration..............: avg=339.38ms min=214.82ms med=246.85ms max=1.48s    p(90)=548.41ms p(95)=610.37ms
       { expected_response:true }...: avg=339.38ms min=214.82ms med=246.85ms max=1.48s    p(90)=548.41ms p(95)=610.37ms
     http_req_failed................: 0.00%  ✓ 0        ✗ 221
     http_req_receiving.............: avg=19.87ms  min=29µs     med=67µs     max=1.23s    p(90)=2.54ms   p(95)=222.15ms
     http_req_sending...............: avg=21.69µs  min=8µs      med=19µs     max=120µs    p(90)=34µs     p(95)=40µs
     http_req_tls_handshaking.......: avg=14.99ms  min=0s       med=0s       max=344.3ms  p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=319.48ms min=214.72ms med=245.66ms max=1.29s    p(90)=528.76ms p(95)=605.66ms
     http_reqs......................: 221    7.244676/s
     iteration_duration.............: avg=1.37s    min=1.21s    med=1.25s    max=2.48s    p(90)=1.6s     p(95)=1.95s
     iterations.....................: 221    7.244676/s
     vus............................: 10     min=10     max=10
     vus_max........................: 10     min=10     max=10

앞에서 demo.js와 아래처럼 default로 익스포트 한 함수가 VU에 대한 엔트리포인트가 된다. 이 코드는 VU 당 한 번씩 실행되고 테스트를 얼마나 많이 반복할지는 테스트 실행 옵션에 따라 달라진다.

export default function () {
  // vu code: do things here...
}

실행할 때 k6 run --vus 10 --duration 30s처럼 플래그를 주고 싶지 않다면 아래처럼 options를 지정해 줄 수도 있다.

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

export const options = {
  vus: 10,
  duration: '30s',
};

export default function () {
  http.get('http://test.k6.io');
  sleep(1);
}

간단한 옵션을 CLI에서 지정해도 되지만 복잡한 시나리오는 코드로 제어하는 게 더 편하다. 아래는 stages 옵션 을 사용해서 10초 동안 0 VU에서 10 VU로 증가시키고 다시 20 VU, 30 VU로 증가 시간 뒤 마지막 10초 동안은 0 VU로 내리도록 한 것이다.

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

export const options = {
  stages: [
    { duration: '10s', target: 10 },
    { duration: '10s', target: 20 },
    { duration: '10s', target: 30 },
    { duration: '10s', target: 0 },
  ],
};

export default function () {
  http.get('http://test.k6.io');
  sleep(1);
}

k6의 실행은 지금까지 본 것처럼 로컬에서 실행할 수도 있고 k6-operator도 제공하고 있어서 Kubernetes 클러스터에서 여러 K6 리소스를 띄워서 분산으로 실행할 수도 있다. 유료 서비스로 K6 클라우드를 사용할 수도 있다.

테스트 결과

앞에서 실행했듯이 테스트 결과를 stdout으로 출력해 준다. 테스트 결과를 좀 더 자세히 살펴보자.

execution: local
   script: demo.js
   output: -
  • execution은 실행 모드를 보여주고 localcloud가 있는데 여기선 local 모드로 실행했다.
  • script는 실행된 파일이다.
  • output은 외부 출력을 의미한다. JSON, CSV, CloudWatch, Datadog, New Relic 등 다양한 외부 출력을 사용할 수 있는데 여기서는 따로 지정하지 않았으므로 -로 표시되고 집계된 테스트 결과만 stdout으로 출력된다.
scenarios: (100.00%) 1 scenario, 30 max VUs, 1m10s max duration (incl. graceful stop):
         * default: Up to 30 looping VUs for 40s over 4 stages (gracefulRampDown: 30s, gracefulStop: 30s)

scenarios는 테스트에 사용된 시나리오의 요약을 보여준다.

위에서 보면 (100.00%)는 실행 세그먼트이고 1개의 시나리오이고 최대 30 VU로 사용되었고 1분 10초 동안 실행되었음을 보여준다. default는 이 테스트에서 시나리오를 설명해 주는데 4개 스테이지로 40초 동안 30번 반복했음을 보여준다.

running (0m41.1s), 00/30 VUs, 463 complete and 0 interrupted iterations
default ✓ [======================================] 00/30 VUs  40s

     data_received..................: 5.4 MB 132 kB/s
     data_sent......................: 56 kB  1.4 kB/s
     http_req_blocked...............: avg=35.28ms  min=1µs      med=5µs      max=922.12ms p(90)=8µs      p(95)=458.99ms
     http_req_connecting............: avg=17.23ms  min=0s       med=0s       max=505.93ms p(90)=0s       p(95)=223.8ms
     http_req_duration..............: avg=297.15ms min=212.98ms med=244.83ms max=740.89ms p(90)=464.64ms p(95)=484.28ms
       { expected_response:true }...: avg=297.15ms min=212.98ms med=244.83ms max=740.89ms p(90)=464.64ms p(95)=484.28ms
     http_req_failed................: 0.00%  ✓ 0         ✗ 463
     http_req_receiving.............: avg=22.27ms  min=21µs     med=66µs     max=493.46ms p(90)=1.97ms   p(95)=228.61ms
     http_req_sending...............: avg=23.42µs  min=6µs      med=20µs     max=110µs    p(90)=37µs     p(95)=48µs
     http_req_tls_handshaking.......: avg=17.42ms  min=0s       med=0s       max=496.72ms p(90)=0s       p(95)=234.54ms
     http_req_waiting...............: avg=274.86ms min=212.89ms med=241.69ms max=551.04ms p(90)=423.96ms p(95)=455.44ms
     http_reqs......................: 463    11.271742/s
     iteration_duration.............: avg=1.33s    min=1.21s    med=1.24s    max=2.16s    p(90)=1.5s     p(95)=1.72s
     iterations.....................: 463    11.271742/s
     vus............................: 1      min=1       max=30
     vus_max........................: 30     min=30      max=30

상세한 메트릭 값이 나오는데 대충 보면 의미를 알 수 있긴 하지만 자세한 내용은 Metrics 문서를 참고하면 된다.

테스트 결과 출력

--out 플래그를 사용하면 위 stdout 외에 다양한 곳으로 결과를 내보낼 수 있다. --out은 여러 개를 동시에 사용할 수도 있는데 아래와 같은 출력을 기본으로 지원하고 있다.

  • Amazon CloudWatch
  • Grafana Cloud / Prometheus
  • Prometheus
  • Datadog
  • New Relic
  • K6 Cloud
  • InfluxDB
  • TimescaleDB
  • StatsD
  • Netdata
  • CSV
  • JSON

JSON으로 출력하려면 k6 run --out json=result.json test.js와 같이 사용하면 되고 아래와 같이 result.json 파일에 결과가 출력된다.

{"type":"Metric","data":{"name":"http_reqs","type":"counter","contains":"default","tainted":null,"thresholds":[],"submetrics":null},"metric":"http_reqs"}
{"type":"Point","data":{"time":"2022-07-31T16:17:52.494829+09:00","value":1,"tags":{"group":"","url":"https://test.k6.io","method":"GET","status":"200","proto":"HTTP/1.1","scenario":"default","name":"https://test.k6.io","tls_version":"tls1.3","expected_response":"true"}},"metric":"http_reqs"}
{"type":"Metric","data":{"name":"http_req_duration","type":"trend","contains":"time","tainted":null,"thresholds":[],"submetrics":[{"name":"http_req_duration{expected_response:true}","suffix":"expected_response:true","tags":{"expected_response":"true"}}]},"metric":"http_req_duration"}
{"type":"Point","data":{"time":"2022-07-31T16:17:52.494829+09:00","value":200.538,"tags":{"group":"","url":"https://test.k6.io","method":"GET","status":"200","proto":"HTTP/1.1","scenario":"default","name":"https://test.k6.io","tls_version":"tls1.3","expected_response":"true"}},"metric":"http_req_duration"}
{"type":"Metric","data":{"name":"http_req_blocked","type":"trend","contains":"time","tainted":null,"thresholds":[],"submetrics":null},"metric":"http_req_blocked"}
{"type":"Point","data":{"time":"2022-07-31T16:17:52.494829+09:00","value":593.658,"tags":{"status":"200","proto":"HTTP/1.1","group":"","url":"https://test.k6.io","method":"GET","expected_response":"true","scenario":"default","name":"https://test.k6.io","tls_version":"tls1.3"}},"metric":"http_req_blocked"}

그 외 출력을 사용하려면 직접 만들거나 커뮤니티에서 만들어 놓은 출력을 사용해야 한다.

2022/07/31 16:45 2022/07/31 16:45