성능 테스트의 필요성

catchx2 프로젝트에서 가장 중요한 문제집 작성과 문제집 풀기 로직을 작성하면서, api 호출에 대한 부하 테스트의 필요성을 느꼈다. 특히 하나의 문제집에 10개 내의 문제, 그리고 각 문제에 대해 4개 내의 정답지가 존재하므로 이를 반복함수로 구현해서 한번에 요청을 처리할 것인지 혹은 매 번 요청으로 처리할 것인지를 결정해야 했다.

현재 문제 작성은 사용자 이탈의 경우 프론트에서 캐싱으로 작성중이었던 문제의 정보를 불러올 수 있다. 그게 가능하기에 문제집 발행시에 모든 문제 저장을 백엔드에서 처리하게 하는, 막연하게 본다면 무식하거나 느릴 수 있는 방법을 채택하게 되었다. 하지만 이는 테스트로 검증되지 않았기에 방법을 찾아서 성능에 대한 검증을 진행해야 한다. 그래서, 오늘은 이를 위한 테스트를 구성하는 방법을 알아보기로 한다.

테스트 방법

어떤 테스트가 적합할까?

포스트를 작성하기 전에, 코드 로직의 정합성을 평가하는 것은 '부하 테스트'의 일종이 아닐까라고 막연하게 생각했다. 로직이 맞는 지 확인하기 위해서는 '부하'를 주어야 한다는 생각이었다. 하지만 역시 검색해보니 수많은 테스트가 각자의 역할이 분명하게 나뉘어 수행되어야 했다. 이 중에서 내가 수행해야 하는 것은 두 가지 정도였다.

스트레스 테스트

개별적인 요청의 처리는 문제 없다고 가정했을 시에 한번에 수많은 요청이 전달되었을 때 반복 함수 로직이 얼마나 버틸 수 있는 지 확인하고자 한다.

부하 테스트

한번에 수많은 요청이 아닌, 꾸준하게 상당한 트래픽을 발생 시켜본다. 로직의 수행 시간이 지연될 수 있는 부분이 있어 실패한다면 로직을 변경해야 할 것이다.

이 밖에도 지금 당장 필요할 만한 테스트로 성능테스트가 있었다. 하지만 목표치를 수행할 수 있는 지에 대한 검증인 성능테스트는, 내가 작성한 로직의 능력을 가늠할 수 없었기에 앞의 두 가지를 먼저 진행해보려고 한다. (참고)

Artillery 도입하기

aws lambda 테스트를 위해 검색해보다가, aws 블로그에서 artillery를 사용하여 load testing 하는 방법을 포스팅해둔 것을 보고 artillery를 막연하게 사용해보고자 했다. Jmeter와 같은 테스트 툴이 있는 것을 알고 있었지만, JS와 혼용해서 사용하기엔 역시 artillery가 편해보였다.

배포 환경 확인

테스트를 진행하기 앞서 우리 프로젝트가 배포되어 있는 환경을 확인해봐야 했다. 현재 lambda + api gateway를 사용하여 vpc 밖의 모든 요청에 대해 열려있는 상황이다. 추후에는 api gateway 의 /dev,/stage 구분에 따라 각 vpc를 적당히 나누어 위치시켜서 접근을 구체화해야 한다.

하지만 이는 릴리즈 전까지 개발이 진행되고 고려하고자 한다. 현재 해결하고자 하는 문제에 집중하자면, /dev로 배포된 엔드포인트를 사용하여 테스트를 진행하는 정도면 충분할 것 같다. 배포해둔 lambda의 end point를 사용하여 테스트를 진행해보자.

테스트 문서 작성

artillery가 문서상에는 http 요청에 대한 테스트 시나리오만 가능한 것으로 나와 있었다. 문서를 참고하여 다음과 같은 시나리오 yml 파일을 작성할 수 있었다. (api 명세를 작성해 둔게 없고... 기능 별로 상세가 나와있어 찾기 어려웠다..)

# TODO: 배포 단계 구분에 따라 target 명시해주기, aws 와 연계하여 iam 부여 후 arn 접근 권한 설정 가능한지 알아보기.

config:
  processor: "./createProbSet.js"
  target: "https://tatjyz1mvj.execute-api.ap-northeast-2.amazonaws.com/dev"
  # target: "http://127.0.0.1:5000"
  phases:
  # 초당 {arrivalRate}명의 유저를 {duration}초 동안 생성
    - duration: 1
      arrivalRate: 50
      # 모든 테스트 중 {mavVusers}명 이상의 유저는 허용하지 않음
      maxVusers: 100
      # 테스트 종료까지 점진적으로 {arrivalRate}부터 {rampTo}까지 유저 증가시킴
      # rampTo: 200
      # arrivalRate가 없을 시 arrivalCount만큼의 회원을 테스트동안 균등하게 생성
      # arrivalCount: 20000

  # staging, prod 환경 등 나뉘어서 타겟을 정할 수 있음.
  # 테스트 실행 시 -e 옵션을 사용하여 해당 환경 지정 가능.
  # environments:
  #   locals:
  #     해당 api 포인트를 따로 지정하여 사용
  #     target: "" 
  #     phases:
  #       - duration: 2000
  #         arrivalRate: 30
    
  #   stage:
  #     target: ""
  #     phases:
  #       - duration: 1000
  #         arrivalRate: 30

  # 그 밖에 payload로 csv의 데이터를 접근할 수도 있음
  # processor는 custom js code를 실행시킬 수 있음

# 각 flow를 실행하기 전의 동작 수행 지정
# before:
#   flow:
#     - log: "Get auth token"
#     - post:
#         url: "/auth"
#         json:
#           username: "myUsername"
#           password: "myPassword"
#         capture:
#           - json: $.id_token
#             as: token

scenarios:
  # 로깅에 좋은 이름 지정
  #- name: "로그인 동작 수행"
  # 새 가상 유저가 해당 동작을 실행할 확률 설정. 모든 시나리오의 weight를 합하여 지정 수 만큼의 비율로 실행. (default:1)
  #- weight: 3
  - flow:
    - function: "genCreateProbRequest"
    # environment 활용 시, 아래와 같이 yml상에서 변수 사용가능. 실행중인 environment 이름이 담김. (ex, local / stage)
    # console에 찍는 log 입니다..
    - post:
        url: "/rds" 
        json: 
          "{{ probset }}"

# 각 flow를 실행하고 난 후의 동작 수행
# after:
#   flow:
#     - log: "Invalidate token"
#     - post:
#         url: "/logout"
#         json:
#           token: "{{ token }}"

또, 이를 위한 custom js 파일인 createProbSet.js도 작성하였다.


const probSet= {
    "setTitle": "새로운 문제집",
    "userId": 1321,
    "problems":[]
}
const imgProb = {
    "problemTitle": "좋아하는 소주 브랜드는?",
    "choiceType": "img",
    "choices": ["2022-09-02_9kSEJvLmRWtT5NzoCfGD.png", "2022-09-02_G5JgGrvhrme5pCndvtic.png","2022-09-02_G5JgGrvhrme5pCndvtic.png","2022-09-02_G5JgGrvhrme5pCndvtic.png"],
    "correctIndex": 1
}

const genCreateProbRequest = (context, ee, next) => {
    for(let i =0;i<10;i++){
        probSet.problems.push(imgProb)
    }
    context.vars.probset = probSet;
    return next();
}

module.exports = {genCreateProbRequest}

aws 에서 예시로 든 custom js의 명세도 위 링크의 다음 화면에서 찾아볼 수 있다.

테스트 수행

현재 aws의 db저장 로직은 auth layer에 의해 인증절차를 거치게 구성되어 있다. 테스트하고자 하는 환경의 auth properties를 NONE으로 세팅해 주는 것으로 인증 과정을 거치지 않을 수 있다. (물론, 더 자세히는 카카오 인증까지 수행하는 스크립트를 짜야하겠지만 우선은 여기까지 작업한다.)artillery run ./{test 스크립트명}.yml 으로 테스트 코드를 수행하면 다음과 같은 결과를 얻을 수 있다.

시나리오 수행

10초간 1초마다 10명의 문제 생성 요청

위와 같이 구성한다음 테스트한 결과 지표는 아래와 같다.etimedout으로 처리된 요청과 함께 200이 65개로 db에 생성된 것을 확인할 수 있었다. 하지만, 실패한 것이 35개나 되며(500에러) 평균 수행 시간이 p95 수준에서 8520ms인 것을 확인할 수 있었다. 이는 일반적으로 이 상황에 놓인 유저가 8초는 기다려야 문제 저장이 완료된다는 것을 의미한다....

lambda의 상태를 지표로써 확인해보았다.throttles가 계속 발생하고 있는 것을 보고, concurrency 문제일 수 있겠다고 생각하여 설정된 개수를 확인했다.기본적으로 aws에서는 리전마다 동시성을 1000개 할당할 수 있게 해주는데, 예약되지 않은 계정 동시성이 50개로 고정되어 있어서 현재 100개가 작업된다고 가정한 현재 케이스에서는 작업이 지연되거나 실패하는 상황이 발생하고 있는 것이었다.

해결하기 위해 동시성 증가요청을 진행했다. Service Quotas에서는 aws 서비스에서 사용하는 할당에 관련된 수치를 조정할 수 있게해준다. 동시 실행 가능 수를 200 정도로 생각하고 신청을 해두었다.

50명의 문제 생성 동시 요청 처리

1초동안 50명이 문제 생성을 하게 스크립트를 수정한 다음, 테스트를 수행한 결과이다.동시성 갯수 안에서 처리해서, 실패하는 케이스는 없었지만 여전히 시간이 오래걸리는 것을 알 수 있었다.

DB 상태 확인

db 연결 수는 동시성인 50만큼으로 고정되어 조절되는 것을 알 수 있었다. cpu 사용률은 견딜만 해보이지만 사용 가능 메모리가 급격히 줄어드는 것은 조금 다시 생각해볼 필요가 있을 듯 했다. 전체적으로 DB의 부담은 그렇게 크지 않은 것 같아서 다행이었다.

결과 정리

일단, 두 가지의 실험을 통해서 다음과 같은 내용으로 정리가 가능했다.

  1. 10초간 1초동안 10명이 문제 생성 요청을 던지면 평균 8초가 걸림.
    1-1. 근데 그 중엔 35개는 실패함.
  2. 1초간 50개의 문제 생성 요청은 모두 성공함.
    2-1. 근데 8초 걸리는 것은 똑같음.
  3. 생각보다 DB쪽 부하는 이 테스트로는 안 걸리는 것으로 확인.

내일 요금이 부과되는 것을 확인한 다음, 로직을 수정할 지 결정할 수 있을 것으로 보인다.

그 밖에 알게된 것

  1. artillery test 스크립트의 작성법.
    • request body는 json으로도 줄 수 있는데, custom function을 사용하여 vars를 던지는 형태라면, json에서 받을 때는 quote 사이에 변수를 존재시켜야 한다.
    • custom js 파일 작성에서 현재는 ts를 지원하지 않는 것으로 보인다.
profile
기술로써 가치를 만들고 싶은 사람입니다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN