프로젝트에 부하 테스트 적용하기
작성된 글의 프로젝트
https://github.com/Fingoo-org/Fingoo
GitHub - Fingoo-org/Fingoo
Contribute to Fingoo-org/Fingoo development by creating an account on GitHub.
github.com
들어가기 전
Fingoo라는 프로젝트의 기능적 개발이 거의 끝나가는 시점이 왔습니다..!(드디어..) 개발을 본격적으로 시작한지 약 4개월이 지났고, 기획 및 마케팅 전략 수립 단계까지하면 약 9개월의 시간이 지난 시점에서 만족스러운 결과물이 나오니 신기하기도 하고, 법인까지 세워서 실제 사용자들에게 피드백을 받게 되는 프로젝트이기 때문에 많이 떨리기도 합니다.
핵심 기능 개발이 거의 끝나가는 시기가 다가왔고, 클라우드 펀딩 받는 시기에 함께 프로젝트를 진행한 사람들과 베타 테스트를 진행하게 되었습니다. 베타 테스트 기간에는 실제 런칭 전에 여러 문제에 대응하는 시기를 가지게 되었고, 이번 글에서는 Artillery 부하 테스트를 적용하는 과정을 기록해보고자 합니다.
이 글에서는 Artillery 부하 테스트의 flow는 기본적인 것만 추가할 것이고 프로젝트에 적용하는 것을 목표로 작성했습니다.
추가로 성능테스트를 진행하다보면 실제 서비스에 비용적으로 부담이 생길 수 있기 때문에 완전히 똑같은 환경으로 EC2 서버 및 clould front로 서버를 올린 후에 진행했습니다.
Artillery 부하 테스트란?
먼저 부하 테스트(stress test)란 서버가 얼마만큼의 요청을 견딜 수 있는지 테스트하는 방법입니다.
즉, 작성한 API 에 병목 현상과 얼마 만큼의 트래픽을 수용할 수 있는지에 대한 여부를 확인하고자 스트레스 테스트를 작성하는 것입니다.
코드에 문법적, 논리적인 문제가 있을 때는 유닛 테스트, 통합 테스트를 통해 확인할 수 있고, 현재 80% 이상의 커버리지를 가졌기 때문에 논리적 문법적인 문제보다는 서버의 메모리 문제(OOM(out of memory))가 발생하는 문제가 발생할 수 있습니다.
이를 예측하기 위해 진행하는 것이 부하 테스트이고, Spring/Java를 사용할 때는 jmeter, ngrinder, grinder 등이 사용되지만, 이 프로젝트에서는 NestJS를 사용했기 때문에 node 환경에서 가볍고 편리하게 사용할 수 있는 Artillery를 사용해보고자 합니다.
Artillery를 살펴보면, Spike tests와 Soak tests를 설명하고 있습니다.
Spike tests
- flash tests 라고도 합니다.
- 짧은 시간(<30분) 동안 실행되며 트래픽을 빠르게 증가시켜 큰 트래픽 폭증을 일으킵니다.
이 유형의 부하 테스트는 특히 다음과 같은 문제를 발견하는 데 유용합니다:
- 오토스케일링 구성
- 서비스 시작 시간 문제
- 서비스 생존성/준비성 체크의 잘못된 구성
- 애플리케이션 코드의 CPU 병목 현상
Soak tests
- 오랜 시간 동안 실행되는 테스트로, 일반적으로 6-12시간 동안 실행되며 (24시간 이상 실행되는 경우도 흔함) 보통 기준 부하보다 10-20% 이상 높지 않습니다.
이 유형의 부하 테스트는 다음과 같은 수명 주기 문제를 찾는 데 사용됩니다:
- 잘못 구성된 리소스 풀
- 메모리 누수 및 기타 리소스 누수
- 리소스 포화 문제
저는 우선 Spike tests 를 CI/CD 파이프라인까지 적용해보고자 합니다. (요구사항에 따라 변경될 가능성이 있을거 같습니당)
사용 방법
Artillery를 설치하고, 각 서버의 실행 스크립트를 실행하면 됩니다.
저의 경우 mono repo를 사용하고 turbo를 적용하고 있기 때문에 turbo run start:dev --filter=api 를 실행한 것이고 보통은 npm start 입니다.
> npm i -D artillery
> turbo run start:dev --filter=api
서버가 실행되었다면 artillery가 적용되었는지 빠르게 확인해보겠습니다.
더 많은 옵션을 원하시면 아래의 링크를 확인해보시면 됩니다.
https://www.artillery.io/docs/reference/cli/quick
> npx artillery quick --count 100 -n 50 <http://localhost:8000>
해당 스크립트는 http://localhost:8000를 빠르게 테스트해보는 방법으로 옵션은 다음과 같습니다.
--count: 가상의 사용자 수
-n: 요청 횟수
--rate: 초당 요청
위의 스크립트는 100명의 가상 사용자가 50번의 요청을 각각 보내는 것입니다. 5000번의 GET / http://localhost:8000 요청이 발생하게 됩니다.
스크립트를 진행하면 위와 같은 summary가 나오게 되는데 이때, median, p95, p99의 차이가 적을수록 성능이 좋다고 볼 수 있습니다.
차이가 적다는 것은 요청이 비슷한 속도로 처리됐다는 것을 의미합니다.
이제 본격적으로 유저의 사용 경로를 설정해서 부하 테스트를 설정해보겠습니다!
Artillery 테스트 스크립트 정리(공식문서 정리)
Artillery 테스트 스크립트는 주로 두 개의 주요 섹션으로 구성된 YAML 파일입니다. config 섹션과 scenarios 섹션입니다.
1. config 섹션
config 섹션은 테스트의 런타임 설정을 정의합니다.
여기에는 테스트 대상 시스템의 URI, 로드 단계 설정, 플러그인, 그리고 HTTP 응답 시간 초과와 같은 프로토콜 별 설정이 포함됩니다.
- target: 테스트 대상 시스템의 엔드포인트를 설정합니다.
- phases: 로드 단계는 지정된 시간 동안 새로운 가상 사용자(VU)를 생성하는 방법을 정의합니다. 일반적인 성능 테스트는 점진적인 웜업 단계, 램프업 단계, 그리고 최대 부하 단계로 구성됩니다.
- 로드 단계 유형:
- 일정한 속도로 VU를 생성하는 단계
- VU 도착 속도가 선형적으로 증가하는 램프업 단계
- 일정 시간 동안 고정된 수의 VU를 생성하는 단계
- 일정 시간 동안 VU를 생성하지 않는 휴지 단계
- 추가 옵션:
- maxVusers: 단계에서 최대 VU 수를 제한합니다.
- name: 단계에 이름을 부여하여 CLI 로그와 Artillery Cloud 대시보드에서 쉽게 식별할 수 있습니다.
- duration: 단계의 지속 시간을 초 또는 사람이 읽을 수 있는 형식으로 지정할 수 있습니다.
- 로드 단계 유형:
- defaults : 뒤에서 테스트할 시나리오의 기본값을 설정할 수 있습니다.
- payload : 임의의 데이터를 보내기 위해서 사용.
- 실제 사용자 시나리오처럼 테스트하려면 테스트마다 보내는 데이터가 달라져야 하는데, 이런 테스트를 위해서 CSV 파일을 사용할 수 있습니다.
- socketio : 소켓을 테스트 할 수 있습니다.
- query : socket 들이 들어오는 주소 끝에 roomID로 쿼리를 달고 있어서 쿼리 부분을 작성해줘야한다. 방 만들때마다 roomID가 매번 달라져서 테스트할때마다 이 부분을 바꿔주는게 번거로울수 있습니다.
- plugins : 플로그인 설정- tls: configure how Artillery handles self-signed certificates
- ensure : 에러나 지연시간에 대해 성공 조건을 셋팅
- processor : 커스텀 js 코드를 load한다.
config:
target: '<http://service1.acme.corp:3003>'
phases:
- duration: 10
arrivalRate: 1
environments:
production:
target: '<http://service1.prod.acme.corp:44321>'
phases:
- duration: 1200
arrivalRate: 10
local:
target: ''
phases:
- duration: 1200
arrivalRate: 20
2. scenarios 섹션
scenarios 섹션은 VU가 수행할 일련의 작업을 정의합니다. 각 시나리오는 애플리케이션 사용자의 일반적인 요청 또는 메시지 시퀀스를 나타냅니다.
- flow: VU가 수행하는 작업 배열입니다. 테스트 동작을 순서대로 적으면 됩니다.
- name: 시나리오에 설명적인 이름을 지정합니다.
- weight: 새로운 VU가 특정 시나리오를 선택할 확률을 다른 시나리오에 비해 상대적으로 설정할 수 있습니다. 값을 높일 수록 해당 시나리오를 더 많이 발생시킵니다.
- capture : 응답으로 받은 데이터에서 다시 변수로 지정해서 뒤에 보내는 요청에 사용할 수 있습니다.
- match : 응답 데이터가 원하는 값이 오는지를 확인할 수 있습니다.
scenarios:
- flow:
- get:
url: '/data'
headers:
authorization: 'Bearer {{ token }}'
3. 플러그인 및 사용자 정의 코드
Artillery는 테스트 수명 주기의 다양한 지점에서 사용자 정의 코드를 실행할 수 있는 "훅"을 제공합니다. 예를 들어, 동적 페이로드 생성, 사용자 정의 검사 실행 또는 사용자 정의 메트릭 추적을 위해 사용할 수 있습니다.
config:
processor: './my-functions.js'
4. CSV 파일에서 데이터 로드
Artillery는 CSV 파일에서 동적 데이터를 로드하여 테스트 스크립트에서 사용할 수 있습니다.
config:
payload:
path: 'users.csv'
fields:
- 'username'
- 'password'
scenarios:
- flow:
- post:
url: '/auth'
json:
username: '{{ username }}'
password: '{{ password }}'
5. 환경 변수 사용
환경 변수를 통해 동적으로 값을 설정할 수 있습니다. 이는 테스트 정의를 수정하지 않고도 다양한 구성 값을 설정할 수 있게 하며, 소스 코드에 비밀 정보를 포함하지 않도록 합니다.
config:
target: <https://service.acme.corp>
phases:
- duration: 600
arrivalRate: 10
scenarios:
- flow:
- get:
url: '/'
headers:
x-api-key: '{{ $env.SERVICE_API_KEY }}'
저는 scenarios 를 추후 비지니스 요구에 따라 추가할 예정이기 때문에 기본 api 만 추가해두었습니다.
추후에 scenarios 를 추가할 때 위의 자료를 참고하면 될 듯합니다.
{
"config": {
"target": "https://*****.cloudfront.net",
"phases": [
{
"duration": 60,
"arrivalRate": 30
}
],
"payload" : {
"path": "./vusers.csv",
"fields": ["email", "password"]
}
},
"scenarios": [
{
"name": "init",
"flow": [
{
"get": {
"url": "/api"
}
}
]
}
]
}
CI (github actions)
저는 pull request에 해당 결과를 올리고 싶었기 때문에 아래와 같이 CI에 추가해보았습니다.
전체
name: Artillery Test
on:
pull_request:
branches: [main]
jobs:
artillery_test:
name: artillery_test
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_REMOTE_ONLY: true
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Cache dependencies
id: cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci
- name: Setting api .env
run: |
echo "SERVICE_KEY=${{ secrets.KRX_SERVICE_KEY }}" >> .env
echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> .env
echo "SUPABASE_BUCKET_NAME=${{ secrets.SUPABASE_BUCKET_NAME }}" >> .env
echo "SUPABASE_KEY=${{ secrets.SUPABASE_KEY }}" >> .env
echo "TWELVE_KEY=${{ secrets.TWELVE_KEY }}" >> .env
- name: Run Artillery Test
run: |
npx artillery run --output artillery-report.json --record --key ${{ secrets.ARTILLERY_KEY }} artillery/load-test.json
- name: Publish Artillery Report to PR
uses: thollander/actions-comment-pull-request@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
message: '🚀 Artillery Test Report: <https://app.artillery.io/load-tests/t34zz_dw5bpe3twxq473cpxd954k8n7zmdk_9x8d>'
Artillery 부하 테스트 부분
- name: Run Artillery Test
run: |
npx artillery run --output artillery-report.json --record --key ${{ secrets.ARTILLERY_KEY }} artillery/load-test.json
위 코드를 추가하면 artillery/load-test.json 에 있는 값을 CI에서 실행시키고 해당 결과를 아래의 사진과 같이 올려주도록 했습니다.
PR 댓글에 결과 추가 부분
- name: Publish Artillery Report to PR
uses: thollander/actions-comment-pull-request@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
message: '🚀 Artillery Test Report: <https://app.artillery.io/load-tests/t34zz_dw5bpe3twxq473cpxd954k8n7zmdk_9x8d>'
아래의 사진처럼 올라오게 됩니다.
참고로 위의 결과를 들여다보면 부하테스트에 대한 정보를 그래프로 보여주게 됩니다.
위의 내용을 바탕으로 pull request 시에 artillery 부하 테스트를 진행하도록 적용했고, scearios는 추후에 비지니스 회의 이후 추가할 예정입니다.
CI 파이프라인에 적용하는 부분 참고하실 분은 해당 링크 확인해보시면 좋을거 같아요!
관련 pr 링크: https://github.com/Fingoo-org/Fingoo/pull/218
[EI-251] [infra] chore: artillery test by Hoya324 · Pull Request #218 · Fingoo-org/Fingoo
✅ 작업 내용 Artillery 부하 테스트를 적용했습니다. 추가적인 작업(test flow 추가)은 fred 기능 추가 이후에 진행할 예정입니다. 🤔 고민 했던 부분 pr에 올라올 때 테스트를 진행할지 merge 후에 진
github.com
Reference
'프로젝트 삽질 🧯' 카테고리의 다른 글
프로젝트 | 텍스트 유사도 검색 어떻게 구현할까?(2) - Cosine Similarity과 TF-IDF (0) | 2024.07.07 |
---|---|
프로젝트 & DB | 텍스트 유사도 검색 어떻게 구현할까?(1) - Elasticsearch를 안 쓴 이유와 MySQL Full Text Index (0) | 2024.07.07 |
프로젝트 | redis 적용 및 최적화 도전기 (2) | 2024.06.19 |