Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Artillery를 이용한 스트레스 테스트

API 서버나 웹 애플리케이션 서버를 만들면 성능테스트가 필요해진다. 다 만들고 나면 병목 지점을 찾아야 할 필요도 있고 서버가 얼마나 트래픽을 받을 수 있는지 알아야 사용자 증가에 따라 언제 서버를 늘려야 할지도 예상할 수 있기 때문이다. ab같은 간단한 스트레스 테스트 도구도 있고 nGrinder(혹은 Grinder)나 Gatling, Tsung, JMeter처럼 더 정교한 테스트를 할 수 있는 도구도 있다.

성능이 엄청 중요하거나 성능 테스트 작업이 내가 주로 하는 일이라면 위의 도구를 잘 배워서(제대로 배운다면 아마 nGrinder?) 사용하겠지만, 성능 테스트해봐야지 하면서도 다른 할 일도 많아서 매번 제대로 해보지 못했다. 그리고 위의 도구들이 내가 주로 사용하는 언어를 쓰지 않다 보니 나는 좀 덜 정교하더라도 가볍고 편하게 쓸 수 있는 스트레스 테스트 도구가 필요했다.

Artillery

artillery 1. 대포 2. 포병

Artillery는 Node.js로 작성된 스트레스 테스트 도구이다. 최근에 스트레스 테스트를 할 일이 있어서 찾아보다가 발견해서 사용해 봤는데 내가 필요한 목적으로는 딱 맞았다. Node.js라서 사용하기도 편했고 쉽게 설정해서 다양한 상황에 스트레스 테스트를 할 수 있었다. 아직 GitHub에 star도 많지 않고 완성도가 뛰어나다고 할 순 없지만, 회사에서 만드는 프로젝트라서인지 개발도 빠르게 잘 되고 있다.(물론 이런 신생도구는 또 언제 사라질지 모르지만...)

홈페이지에서 설명하는 몇 가지 특징을 정리하면

  • HTTP(S), Socket.io, Websocket 등 다양한 프로토콜을 지원한다.
  • 시나리오 테스트를 할 수 있다.
  • JavaScript로 로직을 작성해서 추가할 수 있다.
  • statsd를 지원해서 Datadog이나 InfluxDB 등에 실시간으로 결과를 등록할 수 있다.

설치

npm install artillery 명령어로 설치한다. 홈페이지에서는 -g 옵션으로 전역 설치하라고 안내되어 있지만 난 요즘은 웬만하면 전역 설치를 잘 사용하지 않는다. 전역설치는 다른 사람에게 안내하기가 어려우므로 로컬에 설치하고 다음과 같이 경로를 주어서 사용하는 게 더 낫다. 긴 경로는 npm scripts에 단축명령어를 등록하면 되고 package.json에 의존성을 명시하면 누구나 npm install만으로 관련 의존성이 모두 설치되어 편하다.

$ ./node_modules/artillery/bin/artillery --version
1.5.0-12

$ ./node_modules/artillery/bin/artillery dino
 ____________
< artillery! >
 ------------
          \
           \
               __
              / _)
     _/\/\/\_/ /
   _|         /
 _|  (  | (  |
/__.-'|_|--|_|


$ ./node_modules/artillery/bin/artillery

  Usage: artillery [options] [command]


  Commands:

    run [options] <script>   Run a test script. Example: `artillery run benchmark.json`
    quick [options] <url>    Run a quick test without writing a test script
    report [options] <file>  Create a report from a JSON file created by "artillery run"
    convert <file>           Convert JSON to YAML and vice versa
    dino [options]           Show dinosaur of the day

  Options:

    -h, --help     output usage information
    -V, --version  output the version number

위에 보면 사용할 수 있는 간단한 명령어가 나온다.(dino는 공룡을 보여주는 기능이다.)

간단한 스트레스 테스트

quick은 ab처럼 특정 URL에 간단하게 스트레스를 보내고 싶을 때 사용하는 기능이다. 이 블로그의 속도를 확인하기 위해 다음과 같이 간단하게 테스트할 수 있다.

$ ./node_modules/artillery/bin/artillery quick --duration 60 --rate 10 -n 20 https://blog.outsider.ne.kr
Log file: artillery_report_20160908_024547.json
Phase 0 started - duration: 60s

Report for the previous 10s @ 2016-09-07T17:45:39.794Z
  Scenarios launched:  99
  Scenarios completed: 7
  Requests completed:  922
  Concurrent users:    92
  RPS sent: 99.29
  Request latency:
    min: 151.7
    max: 1362.5
    median: 236.3
    p95: 805.7
    p99: 953.4
  Scenario duration:
    min: 6323.1
    max: 9127.4
    median: 7530.9
    p95: NaN
    p99: NaN
  Codes:
    200: 922

.... 중략 ...

all scenarios completed
Complete report @ 2016-09-07T17:46:49.908Z
  Scenarios launched:  600
  Scenarios completed: 600
  Requests completed:  12000
  RPS sent: 149.91
  Request latency:
    min: 151.7
    max: 3690.3
    median: 690.1
    p95: 1340
    p99: 1671.9
  Scenario duration:
    min: 5689.6
    max: 32588.1
    median: 20959.4
    p95: 29898.1
    p99: 31901.8
  Codes:
    200: 12000

여기선 artillery quick --duration 60 --rate 10 -n 20 https://blog.outsider.ne.kr의 명령어를 사용했는데(앞의 경로는 생략) 이는 **60초 동안 테스트를 하는데 초당 10 요청을 보내고 동시 접속은 20으로 한다는 얘기다. 그래서 마지막의 결과 리포트를 보면 총 600개의 시나리오를 보내고 총 요청 수가 12,000인 것을 볼 수 있다. 사용할 수 있는 옵션의 자세한 내용은artillery quick -h`로 확인할 수 있다.

테스트를 실행하면 지난 10초간의 결과를 모아서 보여주고 테스트가 끝나면 마지막에 전체 결과를 보여준다. 그래서 마지막에 나온 최종 결과를 보면 요청당 평균 690ms이지만 min과 max의 차이가 꽤 커서 오래 걸리는 경우는 요청에만 3.6초나 걸리는 것을 볼 수 있다. p95p99는 퍼센타일(Percentile)값으로 느린 요청 중 95%에 있는 요청이 1.34초가 걸렸고 99%에 있는 요청이 1.67초가 걸려서 느린 요청에 분포를 볼 수 있다. 마지막으로 Codes는 모든 요청이 200 OK를 받았음을 보여준다.

좀 더 복잡한 스트레스 테스트

위의 테스트는 요청시간을 측정하는 정도지만 실제로 성능을 테스트하려면 좀 더 사용자 시나리오에 가까운 테스트가 필요하다.

간단한 Express 앱으로 테스트를 해보자. 데모를 위한 앱으로 POST /users 요청을 받은 id를 응답으로 돌려주고 GET /users/:id로 들어온 id를 다시 반환해 준다. 실제로는 회원가입을 시켜서 데이터베이스에 넣고 이를 반환해 줘야겠지만 여기서는 간단하게 받은 값을 그대로 돌려주도록 했다.

var express = require('express');
var router = express.Router();

router.post('/users', function(req, res, next) {
  res.json({id: req.body.id});
});

router.get('/users/:id', function(req, res, next) {
  res.json({id: req.params.id});
});

module.exports = router;

이를 테스트하기 위해서 Artillery가 사용할 설정 파일을 JSON으로 만들 수 있다.

{
  "config": {
    "target": "http://localhost:3000",
    "phases": [
      {"duration": 60, "arrivalRate": 100}
    ],
    "defaults": {
      "headers": {
        "User-Agent": "Artillery"
      }
    },
    "payload": {
      "path": "./data.csv",
      "fields": ["id", "password"]
    }
  },
  "scenarios": [
    {
      "name": "Joining user",
      "flow": [
        { "get": { "url": "/" } },
        {"post":
          {
            "url": "/users",
            "json": {"id": "{{id}}", "password": "{{password}}" },
            "capture": {"json": "$.id", "as": "username"}
          }
        },
        {"get":
          {
            "url": "/users/{{username}}",
            "match": {"json": "$.id", "value": "{{username}}"}
          }
        }
      ]
    }
  ]
}

먼저 config 부분을 살펴보자.

  • target은 서버의 주소를 지정한다.
  • phases는 테스트 요청 시간과 비율을 정한다. {"duration": 60, "arrivalRate": 100}로 지정했으니 60초 동안 매초 100개의 요청을 보낸다.
  • defaults에서는 뒤에서 테스트할 시나리오의 기본값을 설정할 수 있는데 여기서는 헤더에 User-Agent를 설정했다.

payload는 임의의 데이터를 보내기 위해서 사용한다. 실제 사용자 시나리오처럼 테스트하려면 테스트마다 보내는 데이터가 달라져야 하는데 이런 테스트를 위해서 CSV 파일을 사용할 수 있다. 예를 들어 다음과 같은 data.csv가 있다고 해보자.

jane,asdf
jhon,asdf
bob,qwer
greg,1234
ive,1234
tim,qwer

payloadpath에서 이 파일을 지정하면 테스트를 처음 실행할 때 파일을 가져오고 fields에서 순서대로 필드명을 지정하면 뒤에서 {{id}}같은 문법으로 이 값을 바꿔가면서 사용할 수 있다.

"scenarios": [
  {
    "name": "Joining user",
    "flow": [
      { "get": { "url": "/" } },
      {"post":
        {
          "url": "/users",
          "json": {"id": "{{id}}", "password": "{{password}}" },
          "capture": {"json": "$.id", "as": "username"}
        }
      },
      {"get":
        {
          "url": "/users/{{username}}",
          "match": {"json": "$.id", "value": "{{username}}"}
        }
      }
    ]
  }
]

앞에서 본 JSON 파일의 scenarios 부분을 다시 가져왔다. 여기서 실제 테스트할 시나리오를 정한다. 예를 들어 홈페이지에 접속하고 회원 가입을 적용하고 회원 페이지를 본다같은 시나리오를 만들 수 있고 이 값이 배열이므로 여러 시나리오를 만들어 넣을 수 있다.

  • name은 테스트 시나리오의 이름으로 테스트 결과에서 시나리오의 이름을 확인할 수 있다.
  • flow에서는 시나리오에서 진행하는 테스트를 순서대로 적으면 된다. 이 flow에서는 GET, POST, GET 요청을 순서대로 보낸다.
  • 각 요청에서는 url로 호스트 명을 제외한 패스를 지정하면 되고 json 필드에서 보낼 데이터를 지정할 수 있다. 여기서 값에 {{ }} 문법을 통해서 앞에서 payload로 불러온 값을 지정할 수 있다.
  • capture는 응답으로 받은 데이터에서 다시 변수로 지정해서 뒤에 보내는 요청에 사용할 수 있다. 여기서는 JSON으로 받은 응답의 id값을 username으로 지정했고 이 값을 다시 뒤의 요청에서 변수로 사용할 수 있다. 즉 가입한 사용자 아이디로 다음 요청을 보낼 수 있다.
  • match에서는 응답 데이터가 원하는 값이 오는지를 확인할 수 있다.

이 테스트는 run명령어를 사용해서 다음과 같이 실행할 수 있다.

$ artillery run test.json

all scenarios completed
Complete report @ 2016-09-10T13:29:12.757Z
  Scenarios launched:  6000
  Scenarios completed: 6000
  Requests completed:  18000
  RPS sent: 297.27
  Request latency:
    min: 0.6
    max: 566.4
    median: 0.9
    p95: 48
    p99: 334.9
  Scenario duration:
    min: 7.2
    max: 1115.8
    median: 8.6
    p95: 233.8
    p99: 867.4
  Codes:
    200: 18000

테스트 결과는 앞에서 설명한 부분과 동일하다.

테스트 결과 보고서

run으로 테스트를 하면 테스트 결과로 artillery_report_20160910_222866.json와 같은 파일이 만들어진다. 이 파일에는 테스트의 상세 결과가 JSON으로 담겨있는데 사람이 보기에는 쉽지 않다.

그래서 report 명령어를 사용하면 이 보고서를 보기 쉬운 HTML로 만들 수 있다.

$ artillery report artillery_report_20160910_222866.json
Report generated: artillery_report_20160910_222866.json.html

이렇게 만들어진 보고서는 테스트 결과를 보기 쉽게 그래프로 잘 표현이 된다.

Artillery HTML 보고서

2016/09/10 23:59 2016/09/10 23:59