
K6 부하테스트, Grafana (+ influx DB) 모니터링, Prometheus 인스턴스 메트릭 수집
위 링크에서 K6 부하테스트 및 Grafana / Prometheus / InfluxDB 모니터링 환경 구축을 언급했다.
이에 이어서 K6 스크립트 작성법에 알아보고자 한다.
먼저 K6 란 Grafana 에 소속된 부하테스트 도구로써 Grafana 와의 호환성이 뛰어나다.
Grafana 사용자 수가 많은 만큼, 오픈소스도 잘 되어있고 K6 또한 많은 기능들을 지원한다.
또한 Go 언어의 코루틴 기반으로 작성되어 있어,
가상 사용자 (vUser) 수를 대폭 늘릴 수 있다는 것이 가장 큰 장점이다.
JMeter 나 Ngrinder 에서 Heap 메모리 제한으로 인해 가상 사용자 수 (VUs) 최대 값은 수천을 을 넘기기 힘들고, OOM 도 자주 발생한다.
가상 사용자 하나 당 쓰레드 생성으로 인한 메모리 한계 때문인데, 이게 과연 부하 테스트가 맞는지는 의문이다.
K6 는 최대 30,000 ~ 40,000 개의 가상 사용자 수를 지정해도 충분해, 진정한 의미로 부하 테스트가 가능하다.
JMeter vs K6 를 비교한 Grafana 블로그에서 비교 분석글을 확인할 수 있다.
코루틴에 대한 자세한 내용은 아래 포스팅에서 확인 가능하다.
K6 의 테스트 스크립트는 Javascript 로 작성한다.
런타임에는 Javascript Interpreter 를 사용해 Go 엔진으로 실행된다고 보면 된다.
개발자라면 한 번쯤은 다뤄보았을 언어이기도 하고, K6 스크립트 메서드가 직관적이라 어려울 것은 없다.
그래도 어떠한 기능들을 지원하는지는 알아야 하니 자세하게 살펴보자.
기본적인 구조는 아래와 같다.
import http from 'k6/http';
import { sleep } from 'k6';
export let options = {
vus: 10, // 가상 사용자 수
duration: '30s', // 테스트 실행 시간
};
export function setup() {
// setup code
return {
initData: 'initial setup data'
}
}
export default function (data) {
let res = http.get('https://test-api.com');
console.log(`Response time: ${res.timings.duration}ms`);
sleep(1);
}
export function teardown(data) {
// teardown code
}
기본적으로는 테스트 스크립트에 대한 options 를 지정하고, 아래와 같은 lifeCycle 을 가져간다.
추가적으로,
options 설정을 통해 부하 테스트의 스케줄을 세부적으로 지정할 수 있다.
stages 는 테스트 부하가 주입되는 단계를 설정할 수 있다.
일반적으로 테스트는 RampUp --> Load --> RampBackDown 의 순서로 수행이 된다.
export let options = {
stages: [
{ duration: '10s', target: 20 }, // 10초 동안 가상 사용자를 20명까지 증가
{ duration: '20s', target: 50 }, // 1분 동안 가상 사용자를 50명으로 유지
{ duration: '10s', target: 0 }, // 30초 동안 가상 사용자를 0명으로 감소
],
};
위 예제에서는 RampUp으로 20유저를 10초간 생성한다.
그리고 Load는 50 유저를 20초간 수행하고,
마지막으로 RampBackDown으로 사용자를 10초동안 0으로 만든다.
스크립트 자체에 Tag 를 붙여 해당 테스트 결과는 해당 Tag 를 붙여서 출력하도록 설정할 수 있다.
export let options = {
tags: { test_name: "test-script-1" }, // 태그 추가
};
thresholds 옵션을 사용하면 테스트 완료 후, 설정된 성능 목표가 충족되었는지 확인할 수 있다.
이를 통해 부하 테스트의 성공 여부를 자동으로 판단할 수 있다.
export let options = {
thresholds: {
http_req_duration: ['p(95)<500'], // 95%의 요청이 500ms 이하이어야 함
},
};
summaryTrendStats 옵션을 사용하면 요약 보고서에서 원하는 통계 정보만을 선택적으로 확인할 수 있다.
export let options = {
summaryTrendStats: ['avg', 'p(95)', 'max'],
};
# GET 요청 예시
export default function () {
let url = 'https://test-api.com/resource';
let res = http.get(url);
console.log(`Status code: ${res.status}`);
console.log(`Response body: ${res.body}`);
}
# POST 요청 예시
export default function () {
let url = 'https://test-api.com/resource';
let payload = JSON.stringify({ name: 'John Doe', age: 30 });
let params = {
headers: {
'Content-Type': 'application/json',
},
};
let res = http.post(url, payload, params);
console.log(`Status code: ${res.status}`);
}
부하 테스트를 하다 보면 단순히 요청을 보내는 것 외에도, 요청이 성공했는지 여부를 확인하고 싶은 경우가 많다.
이럴 때 check 메서드를 사용해 볼 수 있다.
import { check } from 'k6';
export default function () {
let res = http.get('https://test-api.com');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
}
위 스크립트에서 check 메서드는 응답 상태 코드가 200인지, 그리고 응답 시간이 500ms 이하인지 확인한다.
이렇게 조건을 걸어두면, 부하 테스트 후에 얼마나 많은 요청이 성공했는지 쉽게 알 수 있다.
하나의 테스트 스크립트 안에서도 특정 API 마다, 혹은 특정 로직 별로 구분하고 싶을 수가 있다.
이럴 때 사용되는 것이 Group 이다.
자세한 옵션은 아래에서 확인 가능하다.
grafana k6 - Tags and Groups document
export default function (data) {
group('POST api/books', function () {
// API 테스트 또는 로직 테스트 작성...
}
group('Review Create And Update', function () {
// API 테스트 또는 로직 테스트 작성...
}
}
이렇게 하면 HTTP 결과가 POST api/books, Review Create And Update 라눈 두개의 Group 으로 나뉘게 된다.
Console 의 결과창에는 자세한 정보가 나오지 않아, 저장한 DB 에서 조회하거나 DB 에 연동된 시각화 도구에서 확인이 가능하다.
나의 경우엔 InfluxDB 에 저장 후 Grafana DashBoard 와 연동했다.
k6 를 수행하는 명령어는 k6 run your-path/script.js 와 같은 형태이다.
다만 수행할때마다 script 를 일일이 바꾸기 힘드므로 특정 값을 변수로 지정해 수행할 때 외부에서 지정한 값 또는 기본값으로 수행하게 할 수 있다.
외부에서 변수를 주입해 수행하는 예시)
STAGE1_DURATION=5s
STAGE1_TARGET=5000
STAGE2_DURATION=20s
STAGE2_TARGET=10000
STAGE3_DURATION=5s
STAGE3_TARGET=0
k6 run --env STAGE1_DURATION=$STAGE1_DURATION --env STAGE1_TARGET=$STAGE1_TARGET --env STAGE2_DURATION=$STAGE2_DURATION --env STAGE2_TARGET=$STAGE2_TARGET --env STAGE3_DURATION=$STAGE3_DURATION --env STAGE3_TARGET=$STAGE3_TARGET
k6 script 예시)
// 외부 환경변수로부터 stages 값 주입
const stage1_duration = __ENV.STAGE1_DURATION || '10s';
const stage1_target = Number(__ENV.STAGE1_TARGET || 1);
const stage2_duration = __ENV.STAGE2_DURATION || '110s';
const stage2_target = Number(__ENV.STAGE2_TARGET || 1);
const stage3_duration = __ENV.STAGE3_DURATION || '10s';
const stage3_target = Number(__ENV.STAGE3_TARGET || 0);
// 테스트 설정
export let options = {
stages: [
{ duration: stage1_duration, target: stage1_target },
{ duration: stage2_duration, target: stage2_target },
{ duration: stage3_duration, target: stage3_target },
],
...
};
위 설명한 내용을 기반으로 복합적인 스크립트 작성 예시를 들어본다.
import http from 'k6/http';
import { sleep, check, group } from 'k6';
// 테스트 설정
import http from 'k6/http';
import { sleep, check, group } from 'k6';
// 테스트 설정
export let options = {
stages: [
{ duration: '10s', target: 20 }, // 10초 동안 가상 사용자를 20명까지 증가
{ duration: '20s', target: 50 }, // 20초 동안 가상 사용자를 50명으로 유지
{ duration: '10s', target: 0 }, // 10초 동안 가상 사용자를 0명으로 감소
],
tags: { // 태그 추가
team : 'server3',
test_name: 'test-script-2'
},
thresholds: {
http_req_duration: ['p(95)<100'], // 95%의 요청이 100ms 이하이어야 함
},
};
// setup 함수 - 테스트 실행 전 초기화 작업
export function setup() {
console.log('Setup: Initializing test setup...');
// 공통으로 사용할 헤더 초기화
let headers = {
'accept': '*/*',
'Content-Type': 'application/json',
};
// 필요한 데이터나 환경 초기화 등 설정
return {
initData: 'initial setup data', // 필요 시 데이터를 반환하여 main 함수에 전달
commonHeaders: headers // 헤더를 반환하여 main 함수에서 사용
};
}
// main 함수 - 실제 테스트가 수행되는 부분
export default function (data) {
let url = 'http://host.docker.internal:8080/api/books';
let bookId;
group('POST api/books', function () {
// __VU: 현재 가상 사용자 ID, __ITER: 해당 VU의 반복 횟수
let payload = JSON.stringify({
name: `The Lord of the Rings VU${__VU} ITER${__ITER + 1}`, // VU ID와 반복 횟수를 조합하여 고유한 값으로 변경
category: 'Fantasy',
author: {
name: 'JinUk Ye',
biography: 'English writer and philologist'
}
});
// POST 요청을 보낸다.
let res = http.post(url, payload, { headers: data.commonHeaders });
// POST 요청 응답 검증
check(res, {
'is POST status 200 or 201': (r) => r.status === 200 || r.status === 201, // 상태 코드가 200 또는 201인지 확인
});
console.log(`POST Status code: ${res.status}`);
// POST 응답에서 생성된 ID를 추출한다.
bookId = res.json().id;
});
sleep(0.1); // POST 저장 후 100ms 후에 GET 조회
// GET 요청 그룹
group('GET /api/books', function () {
// 책 ID로 GET 요청을 보낸다.
if (bookId) {
let getUrl = `${url}/${bookId}`;
let getRes = http.get(getUrl, { headers: data.commonHeaders });
// GET 요청 응답 검증
check(getRes, {
'is GET status 200': (r) => r.status === 200, // 상태 코드가 200인지 확인
});
console.log(`GET ${getUrl} Status code: ${getRes.status}`);
} else {
console.error('No book ID returned from POST request.');
}
});
sleep(0.1);
}
// teardown 함수 - 테스트 종료 후 정리 작업
export function teardown(data) {
console.log('Teardown: Cleaning up after test...');
// 테스트가 끝난 후 필요한 정리 작업 수행
}
host.docker.internal 은 내가 임의로 환경 테스트 중인 WSL 에서 docker 외부의 localhost 에 요청을 보내기 위해 작성한 것이니 무시해도 된다.
위와 같이 script 를 작성하고,
K6 부하테스트, Grafana (+ influx DB) 모니터링, Prometheus 인스턴스 메트릭 수집
에서 설정한 것을 기반으로 아래와 같이 k6 스크립트를 실행해보았다.
위와 같이 통합 모니터링 환경을 설정한 것이 아니라면, 그냥 로컬에서 k6 run test-script.js 와 같이 실행해도 무방하다.
docker run --rm --network monitoring_network \
-v ${docker 외부에서 마운트할 디렉토리}/load-test/${팀명}:/scripts grafana/k6:0.55.0 run \
--out influxdb=http://influxdb:8086/metrics \
/scripts/test-script.js
아래와 같은 결과가 콘솔에 출력된다.
50 명의 가상유저(VUs) 로 실행되었고,
모든 유저들이 40초 동안 테스트들이 5016 번을 테스트했음을 알린다.
checks 에는 5016 * 2 = 10034 번의 검증이 통과했고, (각 테스트마다 check 가 2개 있으므로)
이외 나머지 http 관련 값들이 출력된다.
/\ Grafana /‾‾/
/\ / \ |\ __ / /
/ \/ \ | |/ / / ‾‾\
/ \ | ( | (‾) |
/ __________ \ |_|\_\ \_____/
execution: local
script: /scripts/test-script.js
output: InfluxDBv1 (http://influxdb:8086)
scenarios: (100.00%) 1 scenario, 50 max VUs, 1m10s max duration (incl. graceful stop):
* default: Up to 50 looping VUs for 40s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)
# 테스트 진행..
.
.
.
time="2024-12-01T07:11:36Z" level=info msg="Teardown: Cleaning up after test..." source=console
✓ is POST status 200 or 201
✓ is GET status 200
checks.........................: 100.00% 10034 out of 10034
data_received..................: 3.2 MB 79 kB/s
data_sent......................: 2.3 MB 57 kB/s
http_req_blocked...............: avg=40.02µs min=1.7µs med=4.66µs max=16.68ms p(90)=8.02µs p(95)=10.81µs
http_req_connecting............: avg=32.39µs min=0s med=0s max=16.57ms p(90)=0s p(95)=0s
✓ http_req_duration..............: avg=3.76ms min=1.25ms med=2.83ms max=33.92ms p(90)=7.14ms p(95)=9.45ms
{ expected_response:true }...: avg=3.76ms min=1.25ms med=2.83ms max=33.92ms p(90)=7.14ms p(95)=9.45ms
http_req_failed................: 0.00% 0 out of 10034
http_req_receiving.............: avg=47.23µs min=12.33µs med=36.47µs max=1.72ms p(90)=74.5µs p(95)=95.79µs
http_req_sending...............: avg=21.8µs min=4.2µs med=13.99µs max=3.3ms p(90)=37.17µs p(95)=51.51µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=3.69ms min=1.22ms med=2.78ms max=33.7ms p(90)=7.05ms p(95)=9.35ms
http_reqs......................: 10034 249.732516/s
iteration_duration.............: avg=209.16ms min=203.57ms med=207.23ms max=243.99ms p(90)=216.19ms p(95)=220.55ms
iterations.....................: 5017 124.866258/s
vus............................: 1 min=1 max=49
vus_max........................: 50 min=50 max=50
이전에 내가 포스팅했던 모니터링 환경을 구축했다면, Grafana 에서 같이 모니터링해보자.
아래는 위 부하테스트를 두 건 (정상 1 건, 에러 발생 1 건) 결과를 Grafana DashBoard 로 모니터링한 결과를 캡처한 예시이다.

내친 김에 VUs (가상 사용자 수) 를 대폭 늘려보자.
JMeter 나 Ngrinder 에서는 상상도 못했던 10,000 으로 화끈하게 테스트해보자.
stages: [
{ duration: '10s', target: 4000 }, // 10초 동안 가상 사용자를 4,000명까지 증가
{ duration: '20s', target: 10000 }, // 20초 동안 가상 사용자를 1,000명으로 유지
{ duration: '10s', target: 0 }, // 10초 동안 가상 사용자를 0명으로 감소
]
결론적으로 40초동안 12만 건 이상의 HTTP 요청을 보냈는데,
아래를 보면 이제 슬슬 지연되는 것이 확인된다.
내 로컬PC 에서 Spring Boot 인스턴스를 띄우고 k6 를 구동해 실제 서버 스펙보다는 떨어진다는 것을 참고하자.

K6 는 기본적으로 HTTP/HTTPS 기반이기 때문에, Kafka / RabbitMQ 와의 직접적인 부하는 지원하지 않는다.
다만 k6-plugin-kafka 또는 k6-plugin-amqp 등의 플러그인을 사용하면 사용이 가능하다.
관련해 추가로 작성한 포스트