...
부하 테스트
부하 테스트 (stress test) 란 서버가 얼마만큼의 요청을 견딜 수 있는지 테스트하는 방법이다.
우리는 작성한 API 에 병목 현상과 얼마 만큼의 트래픽을 수용할 수 있는지에 대한 여부를 확인하고자 스트레스 테스트를 작성한다.
내 코드가 실제로 배포되었을 때,
어떤 문법적, 논리적 문제가 있을지는 유닛 테스트와 통합 테스트를 통해 어느 정도 확인할 수 있다.
그러나 내 서버가 몇 명의 동시 접속자나 일일 사용자를 수용할 수 있는지 예측하는 것은 매우 어렵다.
코드에 문법적, 논리적 문제가 없더라도 서버의 하드웨어 제약으로 인해 서비스가 중단될 수 있다.
대표적인 것이 OOM(out of memory) 문제인데,
이는 서버가 접속자들의 정보 저장을 위해 각 사용자마다 일정한 메모리를 할당할 때, 메모리의 양이 계속 증가하다가 결국 서버의 메모리 용량을 넘어서게 되면 발생하는 문제이다.
바로 이것을 부하 테스트를 통해 어느 정도 예측할 수 있다.
통상 자바환경에서는 스트레스 테스트를 생각하면 jmeter, ngrinder, grinder를 사용되고, node 환경에서는 가벼우면서도 편한 Artillery 가 좋다.
Artillery
Artillery는 Node.js로 작성된 스트레스 테스트 도구이다.
무료 버전과 유료 버전으로 나뉘는데 무료 버전을 사용해도 대세에는 크게 지장 없다.
회사에서 운영하는 프로젝트이다보니 업데이트는 꾸준히 존재한다.
몇 가지 특징을 정리하면,
- HTTP(S), Socket.io, Websocket 등 다양한 프로토콜을 지원한다.
- 시나리오 테스트를 할 수 있다.
- JavaScript로 로직을 작성해서 추가할 수 있다.
- statsd를 지원해서 Datadog이나 InfluxDB 등에 실시간으로 결과를 등록할 수 있다.
- 풍부한 CLI 커맨드를 제공한다.
- 리포트 페이지를 따로 제공한다.
부하 테스트 해보기
artillery를 설치하고, 서버를 실행해보자.
> npm i -D artillery
> npm start
이제 서버가 켜진 상태에서, 새로운 콘솔을 하나 더 띄운 후 다음 명령어를 입력한다.
> npx artillery quick --count 100 -n 50 http://localhost:8001
위 명령어는 http://localhost:8001에 빠르게 부하 테스트를 하는 방법이다.
- --count 옵션은 가상의 사용자 수를 의미하고,
- -n 옵션은 요청 횟수를 의미한다.
- --rate 옵션은 초당 요청을 의미한다.
좀 더 상세한 옵션에 대한 내용을 보고 싶다면 artillery quick -h 를 통해 확인 할 수 있다.
즉, 위 명령어에선 100명의 가상 사용자가 50번의 요청을 각각 보내는 것이므로, 총 5000번의 요청이 서버로 전달될 것이다.
많다고 생각할 수도 있겠지만, 실제 서비스에서 5000번의 요청은 그렇게 많은 양이 아니다.
단지 절대적인 숫자가 아닌, 하나의 요청이 얼마나 많은 작업을 하는지가 더 중요하다.
median과 p95, p99의 차이가 크지 않으면 성능이 좋다고 보면 된다.
수치의 차이가 적을 수록 대부분의 요청이 비슷한 속도로 처리되었다는 의미이기 때문이다.
그러나, 실제 서비스를 부하 테스트할 때는 주의해야한다.
지금은 http://localhost:8001 서버에 요청을 보내고 있지만, 이 서버는 개발용 서버인 데다가 내 컴퓨터를 뜻하므로 서버가 중지된다 하더라도 문제가 없다.
그러나 실제 서비스 중인 서버에 무리하게 부하 테스트를 할 경우 실제 서비스가 중단될 수 있다.
또한, AWS나 GCP 같은 클라우드에 종량제 요금을 선택한 경우엔 과다한 요금이 청구될 수 있다.
따라서 실제 서비스에 부하 테스트를 하기보단 실제 서버와 같은 사양의 서버(이를 보통 staging 서버라고 부른다) 를 만든 후에 그 서버에 부하 테스트를 진행하는 것이 좋다.
시나리오 테스트 해보기
부하 테스트를 할 때 단순히 한 페이지에만 요청을 보내는 것이 아니라,
실제 사용자의 행동을 모방하여 시나리오를 작성할 수 있다.
이 때는 JSON 또는 YAML 형식의 설정 파일을 작성해야 한다.
익숙한 JSON 형식을 사용하여 진행해보자.
시나리오 설정 파일 옵션
loadtest.json
{
"config": {
"target": "http://localhost:8001",
"phases": [
{
"duration": 60,
"arrivalRate": 30
}
]
},
"scenarios": [
{
"flow": [
{
"get": {
"url": "/"
}
},
{
"post": {
"url": "/auth/login",
"json": {
"email": "test1@test.com",
"password": "123123"
}
}
},
{
"get": {
"url": "/hashtag?hashtag=test"
}
}
]
}
]
}
Config section
- target : 테스트할 앱의 url을 의미. 서버 주소
- phases : 테스트 요청 시간과 비율을 정한다.
- { "duration": 60, "arrivalRate": 30 } : 60초 동안 매초 30개의 요청을 보낸다.
- defaults : 뒤에서 테스트할 시나리오의 기본값을 설정할 수 있다.
- payload : 임의의 데이터를 보내기 위해서 사용.
- 실제 사용자 시나리오처럼 테스트하려면 테스트마다 보내는 데이터가 달라져야 하는데, 이런 테스트를 위해서 CSV 파일을 사용할 수 있다.
- 실제 사용자 시나리오처럼 테스트하려면 테스트마다 보내는 데이터가 달라져야 하는데, 이런 테스트를 위해서 CSV 파일을 사용할 수 있다.
- socketio : 소켓을 테스트 할 수 있다.
- query : socket 들이 들어오는 주소 끝에 roomID로 쿼리를 달고 있어서 쿼리 부분을 작성해줘야한다. 방 만들때마다 roomID가 매번 달라져서 테스트할때마다 이 부분을 바꿔주는게 번거로울수 있다
- plugins : 플로그인 설정- tls: configure how Artillery handles self-signed certificates
- ensure : 에러나 지연시간에 대해 성공 조건을 셋팅
- processor : 커스텀 js 코드를 load한다.
Secnarios Section
- name : 시나리오 이름
- flow : 시나리오에서 진행하는 테스트 동작을 순서대로 적으면 된다.
- ex) GET, POST, GET 요청을 순서대로 보낸다.
- 각 요청에서는 url로 호스트 명을 제외한 path를 지정하면 되고,
json 필드에서 보낼 데이터를 지정할 수 있다. 여기서 값에 {{ }} 문법을 통해서 앞에서 payload로 불러온 값을 지정할 수 있다.
- capture : 응답으로 받은 데이터에서 다시 변수로 지정해서 뒤에 보내는 요청에 사용할 수 있다.
- match : 응답 데이터가 원하는 값이 오는지를 확인할 수 있다.
- weight : 시나리오에 대한 가중치이다. 이 값이 높으면 해당 시나리오는 더 많이 발생한다.
먼저 config 객체에서 target을 현재 서버로 잡고,
phases에서 60초 동안(duration), 매초 30명의 사용자(arrivalRate)가 들어오도록 했다. 즉, 1분동안 1800 명이 접속하는 상황이다.
defaults론 간단하게 헤더의 User-Agent를 설정해주었다.
payload는 임의의 데이터를 보내기 위해서 사용한다.
실제 사용자 시나리오처럼 테스트하려면 테스트마다 보내는 데이터가 달라져야 하는데 이런 테스트를 위해서 CSV 파일을 사용할 수 있다.
예를 들어 다음과 같은 data.csv 가 있다고 해보자.
payload의 path에서 이 파일을 지정하면 테스트를 처음 실행할 때 파일을 가져오고,
fields에서 순서대로 필드명을 지정하면,
flow 부분에서 {{email}} 같은 문법으로 변수로서 이 값을 바꿔가면서 사용할 수 있다. (넌적스의 변수 문법과 비슷하다)
이제 이 가상 사용자들이 어떠한 동작을 할지 scenarios 속성에 적는다.
먼저 첫 번째 flow로
- 메인 페이지 (GET /)에 접속하고,
- data.csv에 있는 데이터를 기반으로 로그인 (POST /auth/login) 을 한 후
- 해시태그 검색 (GET /hashtag?hashtag=test) 을 한다.
이제 명령어로 부하 테스트를 실행해보자.
$ npx artillery run loadtest.json
1800명의 접속자가 각각 네 번의 요청을 보내기 때문에 (한 번의 redirect를 포함) 총 7200번의 요청이 전송될 것이다.
각각의 요청이 모두 데이터베이스에 최소 한 번씩 접근하기 때문에 상당히 무리가 간다.
만일 서버가 못버틸경우 (최소 요청 시간이 40초 걸리고.. 등) 어디가 문제인지 알아봐야 한다.
예를 들어, 데이터베이스 문제라면, 데이터베이스 메모리 사용량, 트래픽 등을 체크해봐야 하고, 너무 느리다 하면 서버 오토스케일링을 설정 해야 한다.
꽤 준수한 수치가 나왔다.
이번에는 서버가 못버티는 결과 수치를 보자.
404는 해쉬태그 검색에서 아무 결과도 뜨지 않기 때문에 난 것이고, 나머지는 예상 가능한 숫자가 나왔다.
그런데 보면 min이 85.5ms더라도 max가 55583.7ms, 즉 55초가 나온 것을 볼 수 있다.
일단 median 값이 20.4초이며, 최대 55.6초, p95는 42.5초, p99는 50.9초가 나왔다.
즉, 어떠한 시나리오는 요청을 처리하는 데 49초나 걸렸다는 것이다.
또한 중간 로그를 살펴보면 테스트를 진행할수록 요청을 처리하는 속도가 점점 느려짐을 알 수 있다.
이는 서버가 지금 부하 테스트를 하는 정도의 요청을 감당하지 못한다는 뜻이다.
따라서 이 문제를 해결할 방법을 고민해봐야 한다.
서버의 사양을 업그레이드하거나, 서버를 여러 개 두거나, 코드를 더 효율적으로 개선하는 방법이 있다.
지금 상황에선 노드가 싱글 코어만 사용하고 있기 때문에, 클러스터링 같은 기법을 통해 서버를 여러 개 실행하는 것을 우선적으로 시도해볼 만하다.
일반적으론 요청-응답 시 데이터베이스에 접근할 때 가장 많은 시간이 소요된다.
서버는 여러대로 늘리기 쉽지만 데이터베이스는 늘리기 어려우므로 하나의 데이터베이스에 많은 요청이 몰리곤 한다.
따라서 최대한 데이터베이스에 접근하는 요청을 줄이면 좋다. 반복적으로 가져오는 데이터는 캐싱을 한다던지 하여 데이터베이스에 접근하는 일을 최대한 줄여주도록 한다.
서버의 성능과 네트워크 상황에 따라 arrivalRate를 줄이거나 늘려 자신의 서버가 어느 정도의 요청을 수용할 수 있을지 테스트 해보는 것이 좋다.
또한 한 번만 테스트하는 것이 아니라 여러 번 같은 설정값으로 테스트하여 평균치를 내보는 것이 좋다.
웹에서 그래프로 분석 하기
좀 더 상세하게 visualizing 된 내용을 확인하고 싶다면 report 를 사용해보자.
아래 커맨드를 통해 report 파일을 생성할 수 있다.
$ npx artillery run -o myReport loadtest.json
시나리오 테스트를 하고 결과 데이터를 담은 파일인 myReport 가 생성 되었다.
이제 이를 웹으로 변환하는 작업을 해주자
$ npx artillery report myReport
그러면 myReport.html 웹파일이 생성된다.
실행하면 그래프로 잘 디자인된 테스트 보고서 홈페이지를 볼 수 있을 것이다.
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.