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

Kay Hwang
직방 기술 블로그
25 min readOct 12, 2023

--

개발단계에서 부하를 발생시켜 성능테스트를 해야 할 경우가 간혹 있습니다. 이 경우 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 │
├────────────┼──────────────┤
│ 50 │ 0 │
├────────────┼──────────────┤
│ 75 │ 0 │
├────────────┼──────────────┤
│ 90 │ 1 │
├────────────┼──────────────┤
│ 97.5 │ 5 │
├────────────┼──────────────┤
│ 99 │ 10 │
├────────────┼──────────────┤
│ 99.9 │ 44 │
├────────────┼──────────────┤
│ 99.99 │ 181 │
├────────────┼──────────────┤
│ 99.999 │ 330 │
└────────────┴──────────────┘

551k requests in 60.03s, 135 MB read

2. /distance2

Running 60s test @ http://localhost:3000/distance2
10 connections


┌─────────┬──────┬──────┬───────┬───────┬─────────┬─────────┬────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼──────┼──────┼───────┼───────┼─────────┼─────────┼────────┤
│ Latency │ 0 ms │ 0 ms │ 5 ms │ 10 ms │ 0.54 ms │ 3.81 ms │ 289 ms │
└─────────┴──────┴──────┴───────┴───────┴─────────┴─────────┴────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 252 │ 337 │ 11311 │ 14191 │ 9314.02 │ 4482.55 │ 252 │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 61.9 kB │ 82.8 kB │ 2.78 MB │ 3.48 MB │ 2.29 MB │ 1.1 MB │ 61.9 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 │
├────────────┼──────────────┤
│ 50 │ 0 │
├────────────┼──────────────┤
│ 75 │ 0 │
├────────────┼──────────────┤
│ 90 │ 1 │
├────────────┼──────────────┤
│ 97.5 │ 5 │
├────────────┼──────────────┤
│ 99 │ 10 │
├────────────┼──────────────┤
│ 99.9 │ 45 │
├────────────┼──────────────┤
│ 99.99 │ 164 │
├────────────┼──────────────┤
│ 99.999 │ 276 │
└────────────┴──────────────┘

559k requests in 60.03s, 137 MB read

3. /distance3

Running 60s test @ http://localhost:3000/distance3
10 connections


┌─────────┬──────┬──────┬───────┬───────┬─────────┬─────────┬────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼──────┼──────┼───────┼───────┼─────────┼─────────┼────────┤
│ Latency │ 0 ms │ 0 ms │ 5 ms │ 10 ms │ 0.55 ms │ 3.67 ms │ 290 ms │
└─────────┴──────┴──────┴───────┴───────┴─────────┴─────────┴────────┘
┌───────────┬───────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼───────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 289 │ 293 │ 11543 │ 14255 │ 9292.22 │ 4581.79 │ 289 │
├───────────┼───────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 71 kB │ 71.9 kB │ 2.83 MB │ 3.5 MB │ 2.28 MB │ 1.12 MB │ 70.9 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 │
├────────────┼──────────────┤
│ 50 │ 0 │
├────────────┼──────────────┤
│ 75 │ 0 │
├────────────┼──────────────┤
│ 90 │ 1 │
├────────────┼──────────────┤
│ 97.5 │ 5 │
├────────────┼──────────────┤
│ 99 │ 10 │
├────────────┼──────────────┤
│ 99.9 │ 45 │
├────────────┼──────────────┤
│ 99.99 │ 142 │
├────────────┼──────────────┤
│ 99.999 │ 254 │
└────────────┴──────────────┘

558k requests in 60.03s, 137 MB read

60초 동안 1, 2, 3 경우 각 551k, 559k, 558k가 처리되었고 request latency는 평균 0.56ms, 0.54ms, 0.55ms로 큰 차이가 없습니다. 결론적으로 3 개의 라이브러리 모두 크게 성능의 차이는 없는 것 같습니다.

그렇다면 오픈소스 커뮤니티의 크기와 활동정도, 정확도, 사용성 등에 따라 선호하는 것을 선택하면 될 것 같습니다.

결론

3가지 오픈소스 라이브러리의 처리 속도를 비교하기 위해 NodeJS 부하테스트 툴인 autocannon을 사용해보았고 각 요청마다 파라미터를 세팅하는 방법, 세부적인 테스트 옵션, 결과 테이블에 대한 내용을 소개해 드렸습니다.

기회가 된다면 autocannon을 한 번 쯤은 사용해보시길 추천드립니다.

--

--

Kay Hwang
직방 기술 블로그

직방 부동산팀에서 백엔드 개발을 하고 있습니다.