grep

Engineering

쉽고 빠른 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            │
├────────────┼──────────────┤
│ 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을 한 번 쯤은 사용해보시길 추천드립니다.