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만으로 관련 의존성이 모두 설치되어 편하다.

 1$ ./node_modules/artillery/bin/artillery --version
 21.5.0-12
 3
 4$ ./node_modules/artillery/bin/artillery dino
 5 ____________
 6< artillery! >
 7 ------------
 8          \
 9           \
10               __
11              / _)
12     _/\/\/\_/ /
13   _|         /
14 _|  (  | (  |
15/__.-'|_|--|_|
16
17
18$ ./node_modules/artillery/bin/artillery
19
20  Usage: artillery [options] [command]
21
22
23  Commands:
24
25    run [options] <script>   Run a test script. Example: `artillery run benchmark.json`
26    quick [options] <url>    Run a quick test without writing a test script
27    report [options] <file>  Create a report from a JSON file created by "artillery run"
28    convert <file>           Convert JSON to YAML and vice versa
29    dino [options]           Show dinosaur of the day
30
31  Options:
32
33    -h, --help     output usage information
34    -V, --version  output the version number

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

간단한 스트레스 테스트

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

 1$ ./node_modules/artillery/bin/artillery quick --duration 60 --rate 10 -n 20 https://blog.outsider.ne.kr
 2Log file: artillery_report_20160908_024547.json
 3Phase 0 started - duration: 60s
 4
 5Report for the previous 10s @ 2016-09-07T17:45:39.794Z
 6  Scenarios launched:  99
 7  Scenarios completed: 7
 8  Requests completed:  922
 9  Concurrent users:    92
10  RPS sent: 99.29
11  Request latency:
12    min: 151.7
13    max: 1362.5
14    median: 236.3
15    p95: 805.7
16    p99: 953.4
17  Scenario duration:
18    min: 6323.1
19    max: 9127.4
20    median: 7530.9
21    p95: NaN
22    p99: NaN
23  Codes:
24    200: 922
25
26.... 중략 ...
27
28all scenarios completed
29Complete report @ 2016-09-07T17:46:49.908Z
30  Scenarios launched:  600
31  Scenarios completed: 600
32  Requests completed:  12000
33  RPS sent: 149.91
34  Request latency:
35    min: 151.7
36    max: 3690.3
37    median: 690.1
38    p95: 1340
39    p99: 1671.9
40  Scenario duration:
41    min: 5689.6
42    max: 32588.1
43    median: 20959.4
44    p95: 29898.1
45    p99: 31901.8
46  Codes:
47    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를 다시 반환해 준다. 실제로는 회원가입을 시켜서 데이터베이스에 넣고 이를 반환해 줘야겠지만 여기서는 간단하게 받은 값을 그대로 돌려주도록 했다.

 1var express = require('express');
 2var router = express.Router();
 3
 4router.post('/users', function(req, res, next) {
 5  res.json({id: req.body.id});
 6});
 7
 8router.get('/users/:id', function(req, res, next) {
 9  res.json({id: req.params.id});
10});
11
12module.exports = router;

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

 1{
 2  "config": {
 3    "target": "http://localhost:3000",
 4    "phases": [
 5      {"duration": 60, "arrivalRate": 100}
 6    ],
 7    "defaults": {
 8      "headers": {
 9        "User-Agent": "Artillery"
10      }
11    },
12    "payload": {
13      "path": "./data.csv",
14      "fields": ["id", "password"]
15    }
16  },
17  "scenarios": [
18    {
19      "name": "Joining user",
20      "flow": [
21        { "get": { "url": "/" } },
22        {"post":
23          {
24            "url": "/users",
25            "json": {"id": "{{id}}", "password": "{{password}}" },
26            "capture": {"json": "$.id", "as": "username"}
27          }
28        },
29        {"get":
30          {
31            "url": "/users/{{username}}",
32            "match": {"json": "$.id", "value": "{{username}}"}
33          }
34        }
35      ]
36    }
37  ]
38}

먼저 config 부분을 살펴보자.

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

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

1jane,asdf
2jhon,asdf
3bob,qwer
4greg,1234
5ive,1234
6tim,qwer

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

 1"scenarios": [
 2  {
 3    "name": "Joining user",
 4    "flow": [
 5      { "get": { "url": "/" } },
 6      {"post":
 7        {
 8          "url": "/users",
 9          "json": {"id": "{{id}}", "password": "{{password}}" },
10          "capture": {"json": "$.id", "as": "username"}
11        }
12      },
13      {"get":
14        {
15          "url": "/users/{{username}}",
16          "match": {"json": "$.id", "value": "{{username}}"}
17        }
18      }
19    ]
20  }
21]

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

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

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

 1$ artillery run test.json
 2
 3all scenarios completed
 4Complete report @ 2016-09-10T13:29:12.757Z
 5  Scenarios launched:  6000
 6  Scenarios completed: 6000
 7  Requests completed:  18000
 8  RPS sent: 297.27
 9  Request latency:
10    min: 0.6
11    max: 566.4
12    median: 0.9
13    p95: 48
14    p99: 334.9
15  Scenario duration:
16    min: 7.2
17    max: 1115.8
18    median: 8.6
19    p95: 233.8
20    p99: 867.4
21  Codes:
22    200: 18000

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

테스트 결과 보고서

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

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

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

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

Artillery HTML 보고서

Valid HTML5 Valid CSS WCAG 2.1 AA tested