목록 보기
쉽고 빠른 NodeJS 부하테스트 툴, autocannon
기타

쉽고 빠른 NodeJS 부하테스트 툴, autocannon

직방
직방
2023년 10월 12일

개발단계에서 부하를 발생시켜 성능테스트를 해야 할 경우가 간혹 있습니다. 이 경우 NodeJS 부하테스트 툴인 autocannon을 주로 사용하는 편인데요. 오늘은 부하테스트가 필요한 실제와 가까운 사례를 한 가지 들어 보면서 autocannon을 사용하는 법을 소개해 드리도록하겠습니다. 비즈니스 로직을 만드는 과정에서 만약 두 위치 거리를 계산하는 것이 필요하다고 가정해 보겠습니다. 이 문제를 해결하기 위한 방법은 크게 두 가지로 직접 구현하거나, 검증된 오픈소스를 사용하거나 일 것같습니다. 직접 구현하는 것도 좋지만 구면에서 두 지점간거리를 구하는 공식이 생각보다 복잡하고 이 복잡한 수식을 코드로 옮기다가 실수가 발생할 수도 있을 것 같아 검증된 오픈소스 라이브러리를 사용하기로했습니다. 두 지점간 거리를 구하는 오픈소스 라이브리리를 검색해보니 다양하게 나옵니다.여러 오픈소스 라이브러리 중 geolib, haversine 그리고 cheap-ruler 이 셋이 괜찮아 보였습니다. 셋 중 계산결과의 정확도 보다는 좋은 성능을 가진 라이브러리를 선택하기로결정했습니다. NodeJS는 다수 클라이언트의 요청을 제한된 쓰레드로 처리하기 때문에 Event Loop를 블록시킨다면 다른 클라이언트 요청도 블록되기 때문에 연산을 할 때 리소스를 많이 사용할 것 같은 라이브러리를 사용할 때 성능을 검증할 필요가있습니다. 먼저 두 지점간거리를 구하는 NestJS HTTP API 3개를 만들어 보겠습니다. /distance1, /distance2, /distance3 3가지 path를 만들었고 /distance1은 haversine, /distance2는 geoLib 리고 /distance3은 CheapRuler를 사용합니다. controller import { Controller, Get, Query } from '@nestjs/common' import { AppService } from './app.service' class GetDistanceRequest { point1Lat: number point1Lng: number point2Lat: number point2Lng: number } @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get('/distance1') getDistance1(@Query() query: GetDistanceRequest) { return this.appService.getDistanceByHaversine( { lat: query.point1Lat, lng: query.point1Lng }, { lat: query.point2Lat, lng: query.point2Lng }, ) } @Get('/distance2') getDistance2(@Query() query: GetDistanceRequest) { return this.appService.getDistanceByGeolib( { lat: query.point1Lat, lng: query.point1Lng }, { lat: query.point2Lat, lng: query.point2Lng }, ) } @Get('/distance3') getDistance3(@Query() query: GetDistanceRequest) { return this.appService.getDistanceByCheapRuler( { lat: query.point1Lat, lng: query.point1Lng }, { lat: query.point2Lat, lng: query.point2Lng }, ) } } service import { Injectable } from '@nestjs/common' import * as geolib from 'geolib' import * as haversine from 'haversine' import * as CheapRuler from 'cheap-ruler' export class Point { lat: number lng: number } @Injectable() export class AppService { getDistanceByHaversine(point1: Point, point2: Point) { const distance = haversine( { latitude: point1.lat, longitude: point1.lng }, { latitude: point2.lat, longitude: point2.lng }, { unit: 'meter' }, ) return distance } getDistanceByGeolib(point1: Point, point2: Point): number { const distance = geolib.getDistance(point1, point2) return distance } getDistanceByCheapRuler(point1: Point, point2: Point): number { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error const ruler = new CheapRuler(point1.lat, 'meters') const distance = ruler.distance( [point1.lng, point1.lat], [point2.lng, point2.lat], ) return distance } } 앱을 실행시킨 후 3가지 경우를 테스트해 보니 각 결과는 아래와같았습니다. $ curl 'localhost:3000/distance1?point1Lat=37.497952&point1Lng=127.027619&point2Lat=37.508872&point2Lng=127.063186' 3364.237180878499 $ curl 'localhost:3000/distance2?point1Lat=37.497952&point1Lng=127.027619&point2Lat=37.508872&point2Lng=127.063186' 3368 curl 'localhost:3000/distance3?point1Lat=37.497952&point1Lng=127.027619&point2Lat=37.508872&point2Lng=127.063186' 3370.5534698980437 두 지점은 사실 지하철 2호선 강남역과 삼성역이었습니다. 네이버 지도에서 둘 간 거리는 3.4km 인것으로 확인돼 계산 결과에서 셋의 라이브러리 모두 가까운 거리를 계산하는데 있어서 크게 오차를 가지지는 않을 것같습니다. autocannon autocannon은 JavaScript로 만들어진 http API 부하테스트 툴입니다. 설치 후 cli 또는 Node.JS 라이브러리 형태로사용가능합니다. /distance1을 테스트 하기 위한 라이브러리로 형태의 코드는 다음과같습니다. import autocannon from "autocannon" async function main() { const instance = autocannon({ url: "http://localhost:3000/distance1?point1Lat=37.497952&point1Lng=127.027619&point2Lat=37.508872&point2Lng=127.063186", }, finishedBench) autocannon.track(instance, { renderProgressBar: true, renderLatencyTable: true, renderResultsTable: true, }) function finishedBench (err: any, res: any) { console.log('finished bench', err, res) } } main() 결과는 아래와 같이 출력되는데요. 첫 번째 표는 request latency, 두 번째 표는 request volume입니다. request latency 표는 요청에 대한 응답속도라고 볼 수 있습니다. 2.5%는 빠른 상위 latency 2.5%를, 50%는 latency의 중앙값을, 97.5%는 느린 하위 latency 그리고 99%는 가장 느린 백분위의 latency 를나타냅니다. request volumn 표는 초당 전송된 요청의 수, 다운로드된 byte 수를 보여줍니다. 그리고 매 초당 한 번씩 샘플링 됩니다. 숫자가 높을 수록 더 많이 처리가능한, 높은 성능을 가진다고 볼 수 있습니다. request latency와 달리 1%는 가장 느린 경우를 나타내며 %가 올라갈수록 상위로 빨라지는 경우라고 볼 수있습니다. Running 10s test @ http://localhost:3000/distance1?point1Lat=37.497952&point1Lng=127.027619&point2Lat=37.508872&point2Lng=127.063186 10 connections ┌─────────┬──────┬──────┬───────┬──────┬────────┬─────────┬───────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼──────┼──────┼───────┼──────┼────────┼─────────┼───────┤ │ Latency │ 0 ms │ 0 ms │ 2 ms │ 3 ms │ 0.2 ms │ 0.62 ms │ 30 ms │ └─────────┴──────┴──────┴───────┴──────┴────────┴─────────┴───────┘ ┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼────────┼─────────┤ │ Req/Sec │ 11527 │ 11527 │ 14495 │ 15087 │ 14170.4 │ 1131.5 │ 11526 │ ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼────────┼─────────┤ │ Bytes/Sec │ 2.82 MB │ 2.82 MB │ 3.55 MB │ 3.7 MB │ 3.47 MB │ 277 kB │ 2.82 MB │ └───────────┴─────────┴─────────┴─────────┴────────┴─────────┴────────┴─────────┘ Req/Bytes counts sampled once per second. # of samples: 10 ┌────────────┬──────────────┐ │ Percentile │ Latency (ms) │ ├────────────┼──────────────┤ │ 0.001 │ 0 │ ├────────────┼──────────────┤ │ 0.01 │ 0 │ ├────────────┼──────────────┤ │ 0.1 │ 0 │ ├────────────┼──────────────┤ │ 1 │ 0 │ ├────────────┼──────────────┤ │ 2.5 │ 0 │ ├────────────┼──────────────┤ │ 10 │ 0 │ ├────────────┼──────────────┤ │ 25 │ 0 │ ├────────────┼──────────────┤ │ 50 │ 0 │ ├────────────┼──────────────┤ │ 75 │ 0 │ ├────────────┼──────────────┤ │ 90 │ 1 │ ├────────────┼──────────────┤ │ 97.5 │ 2 │ ├────────────┼──────────────┤ │ 99 │ 3 │ ├────────────┼──────────────┤ │ 99.9 │ 4 │ ├────────────┼──────────────┤ │ 99.99 │ 12 │ ├────────────┼──────────────┤ │ 99.999 │ 29 │ └────────────┴──────────────┘ 142k requests in 10.01s, 34.7 MB read 위의 예에서는 요청파라미터를 쿼리스트링에 고정하여서 매번 요청하였는데, 매 번 요청 마다 값을 바꿔서 테스트 해 볼 수도 있습니다. 그렇게 하기 위해서는 아래 코드에서 볼 수 있듯이 requests에서 요청 전 쿼리스트링을 세팅하도록 설정하면 됩니다. 개인적으로 요청 파라미터를 요청마다 랜덤하게 바꿀 수 있다는 점 때문에 다른 툴에 비해 autocannon을 주로사용합니다. import autocannon from "autocannon" import qs from "node:querystring" import * as _ from "lodash" function randomLat() { return _.random(36.0, 37.5) } function randonLng() { return _.random(125.5, 128.5) } async function main() { const instance = autocannon({ url: "http://localhost:3000/distance1", requests: [ { path: "", method: "GET", // @ts-ignore setupRequest: (request, context) => { const params = { point1Lat: randomLat(), point1Lng: randonLng(), point2Lat: randomLat(), point2Lng: randonLng(), } const queryString = qs.encode(params) request.path = "http://localhost:3000/distance1?" + queryString return request }, }, ] }, finishedBench) autocannon.track(instance, { renderProgressBar: true, renderLatencyTable: true, renderResultsTable: true, }) function finishedBench (err: any, res: any) { console.log('finished bench', err, res) } } main() 그 외에 얼마나 테스트를 지속할 것인지(duration), 몇 회 테스트할 것인지(amount), 동시 연결을 몇 개 할 것인지(connections), 초당 요청 수를 몇 회로 제한할 것인지(connectionRate) 등 세부 설정도가능합니다. 예를 들어, 60초 동안 테스트를 하고 싶다면 아래와 같이 duration: 60을 추가해 주면됩니다. const instance = autocannon({ url: "http://localhost:3000/distance1", duration: 60, // 60초 동안 테스트 requests: [ ... 테스트 결과 /distance1, /distance2 그리고 /distance3을 60초 동안 위경도를 무작위로 바꿔가면서 테스트한결과입니다. 1. /distance1 Running 60s test @ http://localhost:3000/distance1 10 connections ┌─────────┬──────┬──────┬───────┬───────┬─────────┬─────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼──────┼──────┼───────┼───────┼─────────┼─────────┼────────┤ │ Latency │ 0 ms │ 0 ms │ 5 ms │ 10 ms │ 0.56 ms │ 4.14 ms │ 358 ms │ └─────────┴──────┴──────┴───────┴───────┴─────────┴─────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 243 │ 307 │ 10775 │ 14207 │ 9183.5 │ 4386.86 │ 243 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 59.7 kB │ 75.4 kB │ 2.64 MB │ 3.49 MB │ 2.25 MB │ 1.08 MB │ 59.7 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ Req/Bytes counts sampled once per second. # of samples: 60 ┌────────────┬──────────────┐ │ Percentile │ Latency (ms) │ ├────────────┼──────────────┤ │ 0.001 │ 0 │ ├────────────┼──────────────┤ │ 0.01 │ 0 │ ├────────────┼──────────────┤ │ 0.1 │ 0 │ ├────────────┼──────────────┤ │ 1 │ 0 │ ├────────────┼──────────────┤ │ 2.5 │ 0 │ ├────────────┼──────────────┤ │ 10 │ 0 │ ├────────────┼──────────────┤ │ 25 │ 0 │ ├────────────┼────────────

댓글 0

댓글을 작성하려면 로그인이 필요합니다.

댓글을 불러오는 중...