개발에서 안정성을 가져가기 위한 조건은 여러가지가 있다. 가장 많이 사용하는 방법으로 테스트 코드가 있다.
본인이 작성한 코드가 로직대로 제대로 돌아가는지를 각 언어마다 각자의 방식으로 코드를 작성하여 검증할 수 있다.
하지만 백엔드 개발의 경우에는 로직이 정상적으로 돌아가는 것 뿐만 이 아니라 실제 서비스에 릴리즈 되었을 때 여러 사용자가 서버에 접속하여도 정상적으로 작동하는지 확인을 해야 한다.
여러 사람이 동시에 서버에 접속해도 정상적으로 서비스가 작동하는지 검증하는 과정을 성능 테스트라고 한다.
성능 테스트에는 여러가지 종류가 있다. Load, Stress, Soak, Spikerk 가장 많이 수행되는 테스트이다.
사진을 보면 각 테스트가 어느정도의 부하를(y축) 얼만큼(x축) 유지하는지 보여준다.
부하 테스트는 고정된 특정 부하에 다다를 때까지, 특정 시간동안 부여한다. 위 그래프의 초록색 선을 보면 어느 기간 동안 적절한 로드가 생기도록 한다.
부하 테스트는 일반적으로 계속해서 부하를 증가시킴으로서 시스템이 얼마나 부하를 견뎌내는지 알아내기 위해서 한다.
스트레스 테스트는 시스템이 받아들일 수 있는 부하보다 더 높은 부하를 부여한다.
시스템이 스트레스를 받더라도 성능이 괜찮은지, 시스템에 무너졌을 때 어떤 동작을 하는지 확인한다.
과부하가 되고 풀렸을 때 시스템이 장애조치를 하는지, 정상상태로 돌아 갔을 때 시스템이 다시 잘 돌아가는지를 확인한다.
갑자기 시스템에 사용자가 몰리는 시간대를 스파이크 타임이라고도 하는데, Spike Test는 이렇게 사용자가 갑자기 몰리는 경우의 테스트를 말한다.
위 그래프의 노란색 선을 보면, 짧은 시간동안 Stress Test보다 더 높은 부하를 갑자기 주는 것을 확인할 수 있다.
Soak은 무언가가 푹 담겨 젖는 상황을 의미한다. 테스트에서 Soak Test는 오랜시간 동안 부하를 견딜 수 있는지에 대해서 테스트한다.
예를 들면 한 두 시간은 잘 돌아가던 시스템이 조금씩 메모리 누수가 발생하면 오래 돌아갈 수 없는 이런 경우를 테스트한다.
이제 위 테스트를 실제로 실행해보려면 테스트를 위한 툴이 필요하다. 옵션은 정말 많지만 가장 많이 사용되는 세 가지만 간단히 알아보자.
Java 기반의 오픈 소스 툴이다. XML로 테스트 스크립트를 작성할 수 있다. 또 GUI를 지원하며 HTTP, FTP, JDBC, REST등 다양한 프로토콜을 지원한다.
가장 오래된 테스트 툴로 레퍼런스도 많고 JMeter만을 다루는 책이 있을 정도로 알아야 할 것도 많고 오래되었다.
네이버에서 만든 부하 테스트 툴로 Groovy로 테스트 스크립트를 작성한다. Groovy를 사용하는 덕에 테스트 스크립트가 XML을 사용하는 JMeter보다 짧다.
네이버에서 만들어서 한국어를 잘 지원하며 국내 회사에서 꽤 많이 사용한다.
2017년, 비교적 최근에 개발된 테스트 툴로 Grafana에서 유지보수를 하고 있다. 테스트 스크립트 작성에 Javascript를 사용한다. Jmeter, nGrinder와 다르게 JVM에서 동작하지 않으므로 가볍고 빠르다.
필자는 성능 테스트를 경험해보기 위해서 k6를 선택했다. 일단 가볍고 빠르다는 점이 마음에 들었으며 Grafana에서 관리하기 때문에 결합이 용이할 것 이라고 생각했다.
또 Javascript로 스크립트를 작성할 수 있는 점도 마음에 들었다.
nGrinder, JMeter와 다르게 GUI를 메인으로 지원하지 않는다는 단점이 있지만 k6 cloud를 통해 연결해서 사용할 수 있고 일단 테스트 자체는 Script로 진행하는게 조금 더 개발자스럽다? 라는 생각도 있었다.
또 가장 최근에 만들었으니 좀 더 모던 시스템에 적합하지 않을까라는 생각도 있었다.(실제로 Go언어로 만들어서 빠르다고 한다.)
macOS 기준으로 homebrew를 통해 설치할 수 있다.
brew install k6
그리고 이제 앞으로 작성할 자바스크립트 파일을 만들고 간단히
k6 run script.js
라는 커맨드를 통해 테스트를 실행할 수 있다.
script.js
라는 파일을 만들고 아래와 같이 작성한다.
import http from 'k6/http';
import { sleep } from 'k6';
export default function () {
http.get('http://localhost:8000/pokemon-type-service/pokemon-types');
sleep(1);
}
위 코드는 간단히 k6로 API를 테스트한다. 먼저 k6/http
로 부터 http 모듈을 가져오고 get
메소드를 통해 위 주소로 GET 요청을 보낼 수 있다.
또 한번 요청을 보낸 후엔 sleep
을 통해 1초의 텀을 둔다.
이렇게 작성하고, k6 run script.js
을 터미널에서 실행하면
위와 같은 결과를 볼 수 있다.
scenarios를 통해서, 몇 명의 유저로 몇 번의 테스트를 얼마나 시도했는지 볼 수 있다. VUs는 Virtual User로 가상의 유저를 말한다. 기본적으로 1명으로 설정되어 있다.
또 gracefulStop이라는게 있는데, 디폴트로 30초가 설정되어 있으며 테스트 종료 후 필요한 정리 작업을 하는 시간을 의미하며 바꿀 수도 있다.
이제 아래 설정 값들을 보면 http_req_duration이 있는데, 이를 통해 http 요청이 평균적으로 얼마가 나왔는지 확인할 수 있으며, 그 아래 http_req_failed를 통해 몇 번의 요청 중 몇 번이 실패했는지도 볼 수 있다.
또 가상의 유저와 테스트 시간을 임의로 정할 수 도 있는데,
k6 run --vus 10 --duration 30s k6/script.js
이렇게 옵션을 주면 된다. 위 옵션을 10명의 가상의 유저로 30초 동안 테스트를 한다.
하지만 이렇게 커맨드에 옵션을 주는 것보다
export const options = { // 성능 테스트 옵션
vus: 10, // 가상 유저 수
duration: "10s" // 몇 초 동안 테스트 하는지
}
위 코드를 스크립트에 추가하면 자동으로 k6가 인식하고 k6 run script.js
라는 커맨드로 옵션을 추가할 수도 있다.
또 코드 자체를 Javascript로 작성하다보니
const payload = JSON.stringify({ name: 'John Doe' });
const params = { headers: { 'Content-Type': 'application/json' } };
http.post('https://test-api.example.com/users', payload, params);
위와 같이 Javascript로 헤더와 파라미터를 설정해서 넘길 수도 있다. 다양한 Input이 필요하다면 이 역시 Javascript로 만들어서 테스트를 진행하면 된다.
이제 실제로 부하 테스트를 진행해보자. 필자는 이전에 만들었던 서버의 API를 사용해보기로 했다.
export const options = { // 성능 테스트 옵션
stages: [
{ duration: '5s', target: 10 }, // 처음 5초간 10명의 사용자로 증가
{ duration: '20s', target: 30 }, // 이후 20초간 30명의 사용자로 증가
{ duration: '5s', target: 10 }, // 다음 5초간 10명의 사용자로 감소
]
}
테스트 옵션을 위와 같이 할당했다. 기존에 vus
, duration
을 스테이지로 나눠 각 구간마다 다른 가상 유저수로 테스트를 하는 옵션이다.
또 target 옵션을 통해 천천히 5초간 10명으로 증가하고 다음 20초간 30명으로 증가하고, 마지막 5초간은 10명으로 감소하여 테스트가 된다.
옵션을 위와 같이 주고 테스트를 하면
이렇게 결과가 나온다. 5s, 20s, 5s 총 30초간 테스트했고 시나리오를 보면 30 max VUs로 최대 30명의 사용자까지 테스트 했음을 볼 수 있다.
가장 아래 vus 결과를 보면 min=2, max=30으로 최소 사용자와 최대 사용자를 확인할 수 있다.
이제 적절히 옵션을 조정해서 시스템이 뻗을 정도로 테스트를 해보자.
일단 올린 서버의 스펙은 아래와 같다.
pokemon-type-service:
container_name: pokemon-type-service
image: peppermint100/pokemon-type-service:1.7
networks:
- app-network
depends_on:
- poke-eureka-service
- poke-gateway-service
- poke-mysql
- zookeeper
- kafka
environment:
- spring.profiles.active=prod
deploy:
resources:
limits:
cpus: 0.5
memory: 256M
도커 컴포즈를 통해서 배포를 했는데, 아래 deploy를 통해서 cpu 0.5, 메모리는 256mb만큼만 설정을 해줬다. 어느정도 성능 테스트에서 뻗을 수 있도록 이렇게 설정을 했다.
export const options = { // 성능 테스트 옵션
stages: [
{ duration: '1s', target: 10 },
{ duration: '5s', target: 3000 },
{ duration: '1s', target: 10 },
]
}
옵션은 위와 같이 급격하게 사용자가 증가하도록 옵션을 주었다. 실행을 하니
이렇게 대실패를 하게되었다. http_req_failed의 수치가 64.56%로 절반 이상이 실패했고, 3000명의 vuser를 준비하는 시간도 많이 들고 vuser 생성 뿐만 아니라 해제하는데도 gracefulStop을 오랫동안 유지하게 된다.
도커로 배포된 앱의 로그를 확인해도 heap out of size에러가 뜨면서 너무 많은 요청이 와서 메모리가 넘쳤다는 에러를 확인할 수 있었다.
이번엔 도커 컴포즈 파일을 아래와 같이 수정했다.
pokemon-type-service:
container_name: pokemon-type-service
image: peppermint100/pokemon-type-service:1.7
networks:
- app-network
depends_on:
- poke-eureka-service
- poke-gateway-service
- poke-mysql
- zookeeper
- kafka
environment:
- spring.profiles.active=prod
deploy:
resources:
limits:
cpus: 2.0
memory: 1024M
cpu와 메모리 할당을 충분히 해주고, 다시 같은 옵션으로 k6 테스트를 돌린 결과
굉장히 빠르고 깔끔하게 한번의 실패없이 테스트가 성공한다. 간단히 더미로 서버의 스케일 업을 통해 부하 테스트를 통과시킨 경우를 재현했다.
실무에서 따로 부하 테스트를 해본 경험이 없어 최대한 실무에서 이런식으로 하지 않을까라는 시나리오로 부하 테스트를 진행해 보았다. 다른 툴은 사용해본적 없으나 k6는 Javascript로 테스트 시나리오를 짤 수 있어 확실히 유연해서 앞으로 부하 테스트를 진행한다면 k6를 사용하지 않을까 싶다.
그래프 이미지 출처 Types of load testing | Grafana Labs