부하테스트, Jmeter와 K6 - kakaotech-19/backend GitHub Wiki

부하 테스트와 고려 요소

예상되는 부하 상황에서 정상적으로 동작하는지 확인하는 성능 테스트

부하 테스트를 통해 1) 병목 현상 및 2) 메모리 누수 문제를 사전에 발견할 수 있다.

부하 테스트 개념

메트릭

메트릭이란, 시스템 성능을 측정 및 평가할 수 있는 지표이다.

CPU 사용률, 메모리 사용량, 응답 시간, 트랜잭션 처리량 등을 확인하면 되겠다. 아울러 정상적으로 응답 받은 동시 사용자를 확인한다.

테스트 종류

  1. Stress Test 시스템의 한계점(수용 능력)을 찾기 위한 부하 테스트이다. 지속적으로 상당한 요청과 쿼리를 실행함으로써 메모리 누수 및 서버 다운 현상을 확인한다.

  2. Spike Test 일정 기간에 상당한 요청을 호출해서 안정성을 확인하는 테스트이다.

부하 테스트 툴

Jmeter

Jmeter 개념

  1. Thread Group 테스트 주체라고 볼 수 있는 Thread를 설정해야한다.
  • Number of Threads: 동시 사용자 수 (가상 사용자(VU)/쓰레드 수)
  • Ramp-up: 설정한 전체 쓰레드가 생성되는데 걸리는 시간(초). 예를 들어, VU가 100이고 50초 램프업이면 2초마다 1쓰레드 생성
  • Loop Count: 각 쓰레드가 테스트를 반복할 횟수
  1. HTTP Request 설정
  • Protocol: 통신 프로토콜 (http/https)
  • Server: 테스트할 서버의 도메인명/IP 주소 (ex: localhost)
  • Port: 포트 번호
  • Path: API 엔드포인트

Jmeter 설치하고 실행하기

  1. Mac 기준으로 brew install jmeter를 한 후, jmeter 명령어를 실행하면 GUI가 표시된다.
  2. Test Plan의 이름 적어주고, 왼쪽 상단의 Save 버튼을 눌러서 어디에 jmx(테스트 계획이 적힌 xml 형식 파일)jtl(테스트 실행 결과 로그 파일)을 저장할지 설정한다.
  3. 왼쪽 사이드바의 Test Plan에 오른쪽 마우스 버튼을 누르고, Add > Threads > Thread Group을 클릭한다.
  4. Thread Properties를 설정한다.
  5. 사이드바에 표시된 Thread Group에 오른쪽 마우스 버튼을 누르고, Add > Sampler > Http Requset를 클릭한다.
  6. Protocol(http), Server(localhost), Port Number(8080), Path(GET /hello)를 설정한다.
  7. 만약, jwt 토큰을 위해 헤더 설정이 필요하면, Thread Group에 오른쪽 마우스 버튼을 누르고, Add > Config Element > HTTP Header Manager를 클릭해서 헤더를 설정한다.
  8. Thread Group에 오른쪽 마우스 버튼을 누르고 Add > Listener > View Results Tree를 클릭하고, 테스트를 실행하면 테스트 결과를 확인할 수 있다.
  9. Summary Report도 통계 형태로 보여줘서 만들면 좋다.
  10. 아울러, jtl 저장 경로를 설정하면, 파일 형식으로 테스트 결과를 확인할 수 있다.

Jmeter로 성능 지표 확인

Summary Report에는 다음과 같은 정보가 있다.

  • Average, Min, Max: 응답 시간(밀리초)
  • Throughput: 초당 처리된 요청 수

간단한 PATCH API에 대해 3000 VU, Ramp up 10초, loop 5번 설정 후 테스트를 했는데

VU Ramp up Loop Throughput(/s) Max(ms) Avg(ms)
3000 10 5 266 125 3
1000 10 20 2003 889 25
정도의 지표가 나왔다.

K6

K6 개념

K6자바스크립트 코드로 부하 테스트를 진행하는 방식이다. Jmeter의 경우 GUI 상에서 jmx를 만들어야 테스트가 가능한 점에서 차이가 있다. 이런 점에서 K6 쪽이 부하 테스트 버전 관리하기 용이하다고 느껴진다. 또한, Jmeter에 비해 웹소켓, gRPC 테스트 설정이 더 간편하다고 한다.

  • vus: VU 수
  • duration: 테스트 실행 시간
  • iterations: 테스트동안 실행총 요청 횟수
  • stages: 단계적으로 VU 수를 변경하는 옵션
  • sleep: 요청 후 쉬는 시간

K6 설치하고 실행하기

brew install k6를 설치해주자. js 파일(test.js)을 만들어서 스크립트를 작성하고, k6 run test.js를 실행하면 결과가 나온다. --out 옵션으로 json, csv 등으로 결과를 저장할 수 있다. 시계열 DB에도 저장 가능하다.

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

// duration 동안 vus가 총 iterations만큼 요청
// sleep이 1이면, 모든 vus가 동시에 1초 간격으로 요청.
export const options = {
  vus: 100,
  duration: "10s", // 총 테스트 시간. 10s, 1m, 1h 같은 형식으로 적는다.
  iterations: 1000, // 테스트 시간동안의 요청 횟수 총합. vus보다 크거나 같아야 한다.
};

const headers = {
  Authorization: `Bearer ${JWT_KEY}`,
  "Content-Type": "application/json",
};

const payload = JSON.stringify({
  nickname: "123",
});

export default function () {
  const response = http.patch(
    "http://localhost:8080/api/v1/member/nickname",
    payload,
    { headers }
  );

  // 특정 조건을 만족하며 테스트가 진행됐는지 확인
  check(response, {
    "status is 200": (r) => r.status === 200,
    "response time < 500ms": (r) => r.timings.duration < 500,
  });

  sleep(1); // VU는 1초마다 요청을 보낸다.
}

결과는 다음과 같이 나온다. 왼쪽 항목들을 thresholds라고 한다.

     ✓ status is 200
     ✓ response time < 500ms

     checks.........................: 100.00% 2000 out of 2000
     data_received..................: 402 kB  39 kB/s
     data_sent......................: 440 kB  43 kB/s
     http_req_blocked...............: avg=360.65µs min=0s     med=2µs     max=5.19ms  p(90)=232µs   p(95)=3.66ms  
     http_req_connecting............: avg=261.42µs min=0s     med=0s      max=3.14ms  p(90)=128µs   p(95)=2.68ms  
     http_req_duration..............: avg=21.23ms  min=1.52ms med=13.78ms max=77.41ms p(90)=59.28ms p(95)=65.83ms 
       { expected_response:true }...: avg=21.23ms  min=1.52ms med=13.78ms max=77.41ms p(90)=59.28ms p(95)=65.83ms 
     http_req_failed................: 0.00%   0 out of 1000
     http_req_receiving.............: avg=520.79µs min=6µs    med=40µs    max=62.68ms p(90)=181.3µs p(95)=578.89µs
     http_req_sending...............: avg=21.18µs  min=2µs    med=7µs     max=8.34ms  p(90)=23µs    p(95)=32µs    
     http_req_tls_handshaking.......: avg=0s       min=0s     med=0s      max=0s      p(90)=0s      p(95)=0s      
     http_req_waiting...............: avg=20.69ms  min=1.51ms med=13.21ms max=77.15ms p(90)=59.08ms p(95)=65.3ms  
     http_reqs......................: 1000    97.540078/s
     iteration_duration.............: avg=1.02s    min=1s     med=1.01s   max=1.07s   p(90)=1.06s   p(95)=1.06s   
     iterations.....................: 1000    97.540078/s
     vus............................: 100     min=100          max=100
     vus_max........................: 100     min=100          max=100

점진적으로 VU 늘려 테스트하기

export const options = {
  stages: [
    { duration: "10s", target: 100 },
    { duration: "2s", target: 100 },
  ],
};

이런 식으로 option을 stages 바꿔주면 단계적으로 수행하게 된다. 위의 코드를 예로들면,

  1. 10초동안 100 VU까지 점진적으로 늘린다. (생성된 VU는 설정된 sleep 간격으로 지속적으로 요청)
  2. 2초동안 100 VU까지 점진적으로 늘린다.

스트레스와 스파이크 테스트 모두를 진행해줄 수 있겠다.

메트릭 설정하기

thresholds를 설정하면 테스트 성공 기준을 구체적으로 작성할 수 있다.

export const options = {
  vus: 3000,
  duration: "10s",
  iterations: 30000,
  thresholds: {
    http_req_failed: ["rate<0.01"], // 실패 비율이 1% 미만이어야 함 (4XX, 5XX status)
  },
};

sleep(0.01);

이 코드는 3000 VU가 0.01초마다 요청, 총 3만 번 요청하는 테스트이다.

     ✗ status is 200
      ↳  88% — ✓ 26532 / ✗ 3468
     ✗ response time < 500ms
      ↳  35% — ✓ 10523 / ✗ 19477

     checks.........................: 61.75% 37055 out of 60000
     data_received..................: 11 MB  1.8 MB/s
	 (중략...)
   ✗ http_req_failed................: 11.55% 3468 out of 30000


running (06.0s), 0000/3000 VUs, 30000 complete and 0 interrupted iterations
default ✓ [======================================] 3000 VUs  06.0s/10s  30000/30000 shared iters
ERRO[0006] thresholds on metrics 'http_req_failed' have been crossed 

Docker K6로 테스트하기

K6 도커 컨테이너로 스크립트를 실행해보자.

docker pull grafana/k6
docker run --name myk6 --network=host -v .:/app -i grafana/k6 run --out json=/app/results.json /app/k6.js

Jmeter와 K6에 대한 생각

K6가 유지보수 측면에서 Jmeter보다 낫다고 느낀다. 즉, JS 코드가 jmx보다 유지보수 및 버전 관리하기 쉬울 것 같다.(jmx는 Jmeter GUI에서 만들어야 하며, xml이어서 태그 이름을 알아야함)