K6 성능 테스트

후니팍·2023년 9월 16일
1
post-thumbnail
post-custom-banner

오랜만의 글입니다.
요즘 학습 키워드들이 쏟아지고 있는데, 오늘은 그 중에서 성능테스트에 대해 알아보도록 하겠습니다.

성능 테스트란?

성능 테스트(Performance Testing)는 특정 상황에서 소프트웨어, cpu, 램 등의 성능을 측정하는 테스트입니다. 시스템이 특정 작업을 수행하는 데 걸리는 시간, 처리량, 사용 가능한 리소스 등을 평가하기 위해 수행됩니다.

즉, 실제 트래픽 상황에서 정상적으로 동작하는지, 언제 어떤 상황에서 서버가 터지는지 확인하는 작업입니다.
어느정도 트래픽까지 버틸 수 있는지 미리 확인하면 실제 서비스 운영 상황에서 부하가 발생할 것 같을 때 미리 여유롭게 대응할 수 있기 때문에 서버가 다운되는 일은 줄어들 것입니다!

성능 테스트에 여러 카테고리가 있는데 그 중에 부하 테스트(Load Testing)스트레스 테스트(Stress Testing)를 알아보도록 하겠습니다.

부하 테스트

시스템이 예상되는 작업 부하를 얼마나 잘 처리할 수 있는지 평가하는 테스트입니다. 시스템의 처리 능력, 응답 시간, 리소스 사용량 등을 측정합니다.
실제 있을법한 트래픽을 시나리오로 두고 시스템이 잘 처리하는지 확인하는 과정이라고 이해했습니다.

스트레스 테스트

시스템이 극도로 높은 부하나 다양한 스트레스 조건에서 어떻게 동작하는지를 확인하는 테스트입니다. 시스템의 한계를 찾고, 언제 어떻게 실패하는지, 그리고 얼마나 빨리 정상 상태로 회복하는지를 알아보기 위해 진행합니다.
스트레스 테스트를 진행하면 서버가 터지는 순간을 알기 때문에 실제 서비스에서 최악의 재앙을 미리 대비할 수 있습니다.


k6 vs jmeter

성능 테스트 도구들 중에 떠오르는 신성 K6와 가장 널리 쓰이는 Jmeter 중에서 고민했습니다.

작성해야하는 스크립트 형식

  • K6: javascript(typescript)
  • Jmeter: XML

두 스크립트 모두 상관이 없었지만, 자바스크립트가 XML 보다는 5배 이상 짧았습니다.
K6가 Jmeter에 비해 빠르게 코드를 작성할 수 있고, 작성한 코드를 이해하기 편했습니다.


레퍼런스 양

아무래도 오래되었고, 부동의 1위를 지키고 있는 JMeter가 레퍼런스 수가 많았습니다.
하지만 K6 공식 문서가 잘 되어있기 때문에 트러블 슈팅만 조금 힘들 수 있을 뿐이지 크게 곤란할 것 같진 않았습니다.


리소스 효율

K6는 Go로 작성되어 있고, Jmeter는 Java로 작성되어 있습니다. Go가 Java보다 더 적은 메모리와 CPU를 사용하므로 리소스 사용 면에서 K6가 더 유리합니다.


GUI 제공

  • K6: GUI 미제공
  • Jmeter: 자체 GUI 제공

Jmeter의 경우 GUI를 지원합니다. Jmeter 깃허브에서 GUI 사진을 따왔는데, 사진과 같습니다.

썩 예쁜 디자인은 아니었습니다. 하지만 확실히 알아보기에는 편한 것 같습니다.
K6는 GUI를 지원하지 않는데 저희 팀은 딱히 상관 없다고 생각했습니다. 스크립트 가독성이 좋아서 GUI에 밀린다고 생각하지 않았습니다.


스레드

  • K6: 가상 사용자 1명당 goroutine 1개
  • Jmeter: 가상 사용자 1명당 스레드 1개

goroutine은 OS 쓰레드보다 훨씬 가볍게 비동기 Concurrent 처리를 구현하기 위하여 만든 것으로, 기본적으로 Go 런타임이 자체 관리한다.

고루틴의 정의 중 일부인데요. java는 OS의 스레드에 직접 매핑되는 반면 고루틴은 경량화된 스레드로 동작합니다. 성능면에서 K6가 더 우월했습니다.


결론

속도, 리소스 효율성 면에서 K6가 더 유리하기 때문에 K6를 선택하게 되었습니다.


K6 설치

맥 기준입니다.

brew install k6

위 명령어로 k6가 설치됩니다.


스크립트 작성

미리 간단한 게시글 GET api를 만들었습니다.

@RequiredArgsConstructor
@RequestMapping("/posts")
@RestController
public class PostController {

    private final PostService postService;

    @GetMapping
    public ResponseEntity<List<PostResponse>> readAll() {
        return ResponseEntity.ok(postService.readAll().stream()
                .map(PostResponse::from)
                .toList());
    }
}

해당 api의 스크립트를 작성해보겠습니다.

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

export const options = {
  vus: 100, // 가상 사용자 수
  duration: "10s", // 테스트 시간
};

export default function () {
  http.get("http://localhost:8080/posts");
  sleep(1);
}

function 부분은 http 요청을 보내는 부분이라 생략하겠습니다.
options 부분은 테스트 시에 환경을 설정하는 부분입니다. 처음에는 간단하게 100명의 유저가 10초동안 접속했을 때를 측정했습니다.
요청이 끝나고 1초 동안 행동을 중지했는데요. 처음에는 왜 sleep(1)을 설정했는지 궁금했습니다. 공식 문서와 GPT께서 실제 서비스처럼 모방하기 위함이라고 하네요. "한 번 요청하고 나면 같은 요청을 1초 동안은 보내지 않을 것이다." 라고 가정한 것입니다.


실행 결과

k6 run {파일명}

명령어를 통해 테스트를 진행하면 아래와 같이 성능을 보여줍니다.

그리고 각각의 요소가 의미하는 것은 아래의 표와 같습니다. 참고

기본 메트릭 (프로토콜 상관 없이 수집)

METRIC NAMETYPE설명
vusGauge현재 활성화된 가상 사용자 수
vus_maxGauge가능한 최대 가상 사용자 수 (성능에 영향을 주지 않도록 VU 리소스는 미리 할당됨)
iterationsCounter가상 사용자가 JS 스크립트 (기본 함수)를 실행한 총 횟수
iteration_durationTrend한 번의 완전한 반복을 완료하는 데 걸리는 시간, 설정 및 해체에 소요되는 시간을 포함
dropped_iterationsCounterVU 또는 시간 부족으로 시작되지 않은 반복 횟수
data_receivedCounter수신된 데이터의 양
data_sentCounter전송된 데이터의 양
checksRate성공적인 체크의 비율

HTTP 관련 메트릭 (HTTP 요청을 할 때만 생성)

METRIC NAMETYPE설명
http_reqsCounterk6가 생성한 총 HTTP 요청 수
http_req_blockedTrend요청을 시작하기 전에 차단된(무료 TCP 연결 슬롯을 기다리는) 시간
http_req_connectingTrend원격 호스트에 TCP 연결을 설정하는 데 걸린 시간
http_req_tls_handshakingTrend원격 호스트와 TLS 세션을 핸드셰이킹하는 데 걸린 시간
http_req_sendingTrend원격 호스트에 데이터를 전송하는 데 걸린 시간.
http_req_waitingTrend원격 호스트의 응답을 기다리는 데 걸린 시간
http_req_receivingTrend원격 호스트로부터 응답 데이터를 받는 데 걸린 시간
http_req_durationTrend요청에 걸린 총 시간. (http_req_sending + http_req_waiting + http_req_receiving)
http_req_failedRatesetResponseCallback에 따른 실패한 요청의 비율

http_req_duration != http_req_sending + http_req_waiting + http_req_receiving 인데, 그건 sleep(1) 때문입니다. 1초를 쉬었으니 1.05 sec가 된 것이고, sleep을 제외한 시간을 계산해보면 딱 맞는다는 것을 확인할 수 있습니다.


실제 테스트

윗 내용까지는 K6 사용법을 알아보기 위해 진행한 테스트이기 때문에 아주 간단한 테스트로 진행했습니다.
성능 테스트는 실제 환경처럼 세팅하고 진행하는 것이 중요하기 때문에 실제 환경으로 셋업하고 시나리오도 작성해서 진행해보도록 하겠습니다.


부하 테스트

목표

메인 화면의 게시글 목록을 보여줄 때 100ms 내에 응답하는 것을 목표로 잡겠습니다.

  • 하루 최대 rps(request per second): 100rps

저희 서비스는 1시간에 1번 요청이 오는 때도 있기 때문에 평균 요청 횟수를 계산하지 않고 런칭 이벤트와 같은 피크 타임을 기준으로 잡겠습니다.
그렇다면 1초에 100건의 요청을 100ms 안에 보내면 됩니다!

스크립트 작성

최대 가상 사용자 수를 100rps * 0.1s 로 설정하겠습니다. (vus = 10)

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

export const options = {
  stages: [  
    { duration: '2m', target: 5 }, 
    { duration: '10m',target: 5 },
    { duration: '3m', target: 10 },
    { duration: '30m',target: 10 },
    { duration: '3m', target: 0 },
  ],

  thresholds: { 
    http_req_duration: ['p(95)<100'],
  }
};

export default function () {
  const response = http.get("http://localhost:8080/posts");
  check(response, {
    "success": (res) => res.status === 200
  });
}

코드를 해석하자면,
2분 동안 5명으로 유저를 증가시키고
10분 동안 5명의 유저를 유지시킵니다.
그리고 3분 동안 10명으로 유저를 늘립니다.
피크 시간은 30분 동안 진행되고 10명의 유저를 유지시킵니다.
그리고 3분간 0명의 유저로 줄입니다.

또, 95%가 100ms 안에 응답을 받는 것을 목표로 했습니다.


스트레스 테스트

목표

서버가 언제 터지는지 확인해보겠습니다.
그리고 어떤 이유로 터지는지 확인해보도록 하겠습니다.

스크립트

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

export const options = {
  stages: [
    { duration: "1m", target: 10 },
    { duration: "3m", target: 10 },
    { duration: "1m", target: 50 },
    { duration: "3m", target: 50 },
    { duration: "1m", target: 100 },
    { duration: "3m", target: 100 },
    { duration: "1m", target: 200 },
    { duration: "3m", target: 200 },
    { duration: "1m", target: 300 },
    { duration: "3m", target: 300 },
    { duration: "1m", target: 0 },
  ],

  thresholds: {
    http_req_duration: ["p(95)<100"],
  },
};

export default function () {
  const response = http.get("http://localhost:8080/posts");
  check(response, {
    "success": (res) => res.status === 200,
  });
}

테스트 서버 환경 설정

스크립트를 모두 작성했으니 이제 실제 환경에서 테스트를 진행해보겠습니다.
t4g.small 서버를 사용한다고 가정하고 t4g.small에 실제 서비스 환경을 조성해주도록 하겠습니다.

grafana/k6는 arm64를 지원하지 않아 공식 설치 문서를 참고하여 우분투 내에 직접 설치했습니다.
그런데 공식 문서 또한 되지 않았습니다. k6가 arm64를 지원하지 않는 것 같습니다.
그래서 github를 찾아보았는데 다행히도 arm64 버전이 업데이트 되어있었습니다.
github에서 다운받아 우분투로 scp 해주었습니다.

K6를 제외한 grafana와 influxdb를 docker-compose 로 컨테이너를 띄웠습니다.

version: '3.4'

services:
  influxdb:
    image: influxdb:1.8
    ports:
      - "8086:8086"
    environment:
      - INFLUXDB_HTTP_AUTH_ENABLED=false
      - INFLUXDB_DB=k6

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_BASIC_ENABLED=false
    volumes:
      - ./grafana:/etc/grafana/provisioning/

influxdb에서 k6 라는 데이터베이스를 미리 만들었습니다.
또, 그라파나에서 로그인 된 상태로 접속할 수 있도록 설정했습니다.

컨테이너가 모두 잘 띄워져 있습니다!
원래는 독립적인 공간에서 성능 테스트를 해야합니다. 비용 이슈로 저는 한 서버에서 진행했습니다. 연습용이기도 하구요~
스트레스 테스트에서 서버 터지면 성능 테스트 결과도 볼 수 없기 때문에 부하 테스트만 진행해보도록 하겠습니다.

influxdb 연결

aws public ip 주소를 통해 grafana에 접속합니다. 설정에 맞췄다면 {ip주소}:3000 입니다.
화면에서 datasource를 클릭하여 새로 influxdb를 만들어줍니다.
저는 이미 생성해서 아래와 같이 생성된 화면이 나왔습니다.

아래와 같이 포트와 DB 이름을 설정하고 save & test버튼을 클릭하면 정상적이라는 메시지가 뜹니다.

대시보드 생성

이제 GUI로 볼 수 있는 대시보드를 만들어보도록 하겠습니다.
grafana 홈 화면에서 햄버거 버튼을 클릭하면 아래와 같은 화면이 나옵니다

Dashboards를 클릭합니다.

그리고 import를 해서 예쁜 디자인의 대시보드를 들고오도록 하겠습니다.

https://grafana.com/grafana/dashboards/2587 를 입력하고 load 버튼을 클릭하면 아래와 같은 화면이 나타납니다.

이전에 생성한 k6 데이터 소스를 import 하면 모든 준비는 끝입니다.

테스트 진행

드디어 테스트를 gui로 보는 과정입니다.
다시 aws로 돌아와 성능 테스트를 실행합니다.

./k6 run --out influxdb=localhost:8086/k6 {스크립트 파일명} 

그리고 그라파나를 확인해볼까요~

성공적으로 떴습니다.


마무리

이제 스트레스 테스트의 지표로 어떤 부분을 개선해야할지 알아봐야 할 것 같습니다...

참고

profile
영차영차
post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 5월 6일

요즘 k6로 테스트코드 작성중인데 흥미로운 내용이 많네요
참고가 많이 됐습니다, 감사합니다

답글 달기