이 글은 저번 포스팅 에서 날 괴롭히고 험난했던 도커 연동을 지나, 마지막으로 나의 서비스에 부하테스트를 진행해보면서 작성한 글이다.
MAC M1 PRO 로 진행할 것이고, Artillery 로 테스트를 진행해보고(인강을 들었기 때문에 우선..!) 시간이 된다면 추가로 k6로 테스트를 진행해 볼 예정이다.
나는 지금부터 부하테스트를 진행해볼 것이다.
근데 잠깐,,
"근데 그거 왜 하는지 알고 써요?"
부하테스트를 하는 것도 중요하지만 , 우선 간단하게 "내가 왜!? 무엇을 위해?!" 부하테스트를 진행해야하는지 알아보고 넘어가자
개발자는 뭘 하는 사람일까?
단순히 이거 동작하게 해주세요 하면 동작하게 만들어주고, "이거 느려요" 하면 기술들을 써서 개선만 해주면 될까?
아니라고 생각한다. 우리가 그렇게 구현한 소프트웨어는 분명 후에 장애가 발생한다.
일단 적어도 내가 만든건 그럴 것이다.
장애가 아예 발생하지 않는 소프트웨어는 존재하지 않는다. 하드웨어도 마찬가지고 세상이 그렇다.
때문에 개발자로써 다음과 같은 포인트를 가지고 개발해야한다.
- 예방 가능한 장애를 방지할 것.
- 장애가 발생했다면 "망했다.." 보다 빠르게 전파하고 해결할 것.
- 발생했던 장애는 다시는 발생하지 않도록 할 것.
나의 서비스는 수많은 유저가 몰리는 서비스도 아닐뿐더러 그러고 싶어도 그렇게 할 수 있는 방법은 없다.
만약 내가 열심히 누군가와 서비스를 개발했는데 예상하지 못한 장애 때문에 서비스가 터져버려 유저들이 떠나간다면 그거만큼 슬픈 일은 없을 것이다. 이러한 것을 방지하기 위해 부하테스트를 할 수 있는 툴을 사용하여 서비스에 어떤 예상치못한 문제가 발생할 수 있는지 확인할 필요가 있다.
📢 부하 테스트의 목적은 최소 이 3가지는 살펴보자 📢
장애를 유발할 수 있는 테스트 시나리오를 작성, 수행합
미리 서버의 성능에 대해 목표를 정하고 다음과 같은 사항들에 대해 점검하자
(1) 예상 TPS (Transaction Per Second)
(2) 평균/중간/최대 응답시간
(3) 다량의 트래픽 유입 시 동시성 이슈 발생 여부 (나의 서비스에서는 대기열 진입 등이 있겠다.)
나의 서비스에서 어디에 부하를 줄 수 있을까
POST 유저 생성 API
POST 유저 잔액 충전 API
POST 콘서트 생성 API
POST 콘서트 이벤트 생성 API
POST 콘서트 대기열 생성 API
POST 예약 생성 API
POST 콘서트 결제 API
GET 콘서트 좌석 조회 API
GET 콘서트 예약 가능 날짜 조회 API
GET 유저의 대기열 순서 조회 API
GET 유저의 잔액을 조회 API
---------------------------------------------------------------------------------------------------------------------------------
< 서비스 동작 흐름 >
POST 유저 생성 API -> 'uuid 생성'
( POST 콘서트 생성 API & POST 콘서트 이벤트 생성 API ) ->
POST 콘서트 대기열 생성 API -> 'waiting_token 이 redis 에 저장'
(POST 유저 잔액 충전 API & GET 콘서트 좌석 조회 API & GET 콘서트 예약 가능 날짜 조회 API & GET 유저의 대기열 순서 조회 API & GET 유저의 잔액을 조회 API ) ->
POST 예약 생성 API -> 'reservation_token 으로 redis 에 저장'
POST 콘서트 결제 API -> '결제가 되면 redis 에서 토큰 삭제'
1. 부하테스트 - 대규모 인원 동시 대기열 진입( + 대기 번호 조회 )
user
테이블: 3~400 명의 유저 데이터를 생성concert
테이블: 1개의 콘서트를 생성concertEvent
테이블: 해당 콘서트의 이벤트를 생성대기열 진입 -> 좌석 예약 -> 결제
Seat
테이블: 콘서트의 좌석 데이터를 생성(50개)concertEvent
테이블: 1개의 콘서트에 해당하는 콘서트 이벤트 생성user
테이블: 3~400 명의 유저 데이터를 사용3. 부하테스트 - 대기열 진입 + 잔액 충전 -> 잔액 조회 -> 좌석 예약 -> 결제 -> 잔액 조회
어느정도 알아봤으니 이제 본격적으로 테스트를 진행해보자
Artillery는 간편하게 사용할 수 있는 기능과 유연성을 제공하여 개발자가 애플리케이션을 효과적으로 테스트할 수 있는 오픈 소스 라이브러리
- HTTP, Socket.io, Websocket, gRPC 등 다양한 프로토콜 지원
- 클라우드 규모에서의 테스트 가능 (AWS Lambda 서버리스 로드 테스트)
- HTML, JSON 등 가독성이 좋은 처리량 및 통계 제공
- 시나리오 단위로 테스트 가능
테스트 도구로 Artillery 를 선택한 이유 중 가장 큰 이유는 인프런에서 테스트도구 사용법에 대한 강의를 찾다가 Artillery 에 대해 알려주는 강의가 있어 빠르게 배우고 접해볼 수 있다고 생각했다.
또한 커맨드 라인으로 간단한 테스트가 가능하고 결과를 html 로 받아서 시각화할 수 있어 이 점도 마음에 들었다.
k6 나 jmeter 도 있었지만 가장 큰 이유는 투자할 수 있는 시간적 비용을 고려했을 때 최선의 선택이라고 생각하여 Artillery 를 테스트 도구로 선택하게 되었다.
artillery run --output report.json test-config.yaml
artillery report report.json --output report.html
- http.codes.200: 이 지표에서 HTTP 응답 코드에 대해 볼 수 있는데 오른쪽 숫자는 해당 기간 동안 200인 HTTP 응답수가 몇 번이였는지를 알려준다.
- http.downloaded_bytes: 이 기간 동안 다운로드된 데이터의 총 크기(바이트)이다.
- http.request_rate: 이 기간 동안 초당 평균 HTTP 요청 속도이다.
- http.requests: 이 기간 동안 이루어진 총 HTTP 요청 수이다.
- http.response_time: 이 지표는 HTTP 요청의 응답 시간(밀리초)에 대한 통계를 제공한다.
- min: 가장 짧은 응답 시간
- max: 가장 긴 응답 시간
- mean: 평균 응답 시간
- median: 정렬된 응답 시간들의 중간 값
- p95: 95번째 백분위수 응답 시간으로 응답 시간의 95%가 이 값보다 낮음을 의미한다.
- p99: 99번째 백분위수 응답 시간으로 응답 시간의 99%가 이 값보다 낮음을 의미한다.
- vusers.completed: 실행을 완료한 가상 사용자 수
- vusers.created: 이 기간 동안 생성된 총 가상 사용자 수- vusers.session_length: 가상 사용자의 세션 길이(밀리초)에 대한 통계를 제공한다.
- min: 가장 짧은 세션 길이
- max: 가장 긴 세션 길이
- mean: 평균 세션 길이
- median: 정렬된 세션 길이 목록의 중간 값
- p95: 95번째 백분위수 세션 길이로, 세션의 95%가 이보다 짧음을 의미
- p99: 99번째 백분위수 세션 길이로, 세션의 99%가 이보다 짧음을 의미
본격적으로 테스트를 진행해보자. 여러 시나리오들 중 내가 선택한 시나리오는 다음과 같다.
2 . 부하테스트 - 대기열 진입 + 좌석 예약 + 결제
대기열 진입 -> 좌석 예약 -> 결제
- 목표 : 다수의 인원이 동시에 예약을 진행하고 결제하였을 때
- UV : 3-400명의 유저가 동시 예약 요청 후 결제 요청
- 더미 데이터 설정:
Seat
테이블: 콘서트의 좌석 데이터를 생성(50개)concertEvent
테이블: 1개의 콘서트에 해당하는 콘서트 이벤트 생성user
테이블: 3~400 명의 유저 데이터를 사용
위 시나리오는 아까 내가 발생가능하다고 생각했던 시나리오 중 하나이다. 좀 더 구체화해보자
목표 : 초당 10명의 사용자가 대기열에 진입 -> 예약 -> 결제 (반복)
목표 tps
대기열 진입: 10 TPS (400명의 유저가 초당 10명씩 진입 )
좌석 예약: 10 TPS (초기 50명만 예약 성공 -> 350명 실패)
결제: 10 TPS (50명 결제 성공)
합계: 약 30 TPS
성공률
90% 이상의 요청 성공을 목표로 설정
요청 실패(5xx/4xx 오류)가 발생한 경우 이를 로깅하고 병목 지점 분석하기
응답 시간
대기열 진입 응답 시간: 500ms 이내
좌석 예약 응답 시간: 1초 이내
결제 응답 시간: 1초 이내
위와 같이 목표를 삼고 진행해보았다.
config:
target: 'http://localhost:8080'
phases:
- duration: 60
arrivalRate: 10
payload:
path: 'user-name.csv'
fields:
- 'uuid'
scenarios:
- name: "Concert Ticket Reservation Flow"
flow:
- post:
url: '/api/queues'
qs:
uuid: '{{ uuid }}'
capture:
- json: "$.token"
as: "queueToken"
- post:
url: '/api/reservations/1'
qs:
token: "{{ queueToken }}"
json:
concertName: "Manuela Powlowski"
eventId: 1
seatNumber: "{{ $randomInt(1,50) }}"
capture:
- json: "$.reservationId"
as: "reservationId"
- post:
url: '/api/payments'
qs:
token: "{{ queueToken }}"
json:
reservationId: "{{ reservationId }}"
대기열에 진입한 후 받은 token 을 가지고 예약 및 결제를 시도하도록 하였다. 이는 내가 생각한 시나리오처럼 작성한 내용과 같다.
이전에 대기열 진입이 잘 되는지 Artillery 로 테스트 해본 결과 잘 진입하는 것을 확인했고, 발급받은 token 으로 postman 에서 예약도 잘되는것을 확인하였기에 문제없이 잘 되겠지? 했다.
하지만..
위 사진과 같이 이미 할당된 유저들이 모두 대기열에 진입하였음에도 불구하고
반복해서 유저가 대기열진입을 시도하고 있었다.
때문에 Artillery 에서 다음 시나리오인 예약API 와 결제API 로 넘어가고 있지 않던 것이었다.
이 문제를 해결해야한다... 너 왜그러니
현재 시간 금요일 새벽 01시 11분,, 해결하고 잘 수 있을까요 😂
현재 시간 금요일 새벽 02시 19분,,
현재 시간안에 테스트를 해보는 것에 실패했다.
원인은 알았지만(맞는지 확실하지도 않지만,,) 해결하는 과정에 문제가 있는 것 같다.