WebSocket の負荷テストは Artillery でシュッと簡単に実行しよう

[Artillery](https://github.com/artilleryio/artillery) は yaml ファイルに宣言的にシナリオを記述し、シンプルなインタフェースで負荷をかけることができる Nodejs 製の負荷テストツールです。
本記事では Artillery を使用して簡単に WebSocket サーバの負荷テストを実行する方法を紹介します。

# 最小構成の WebSocket サーバ

まずはじめに WebSocket サーバを実装しましょう。今回は Node.js を使用します。
必要最小限の機能だけを提供します。[ws](https://github.com/websockets/ws) ライブラリを使用して簡単に実装しましょう。

```server.js
const WebSocket = require("ws");

const wss = new WebSocket.Server({ port: 3000 }, () => {
  console.log("server is now listening localhost:3000");
});

let connections = [];

wss.on("connection", ws => {
  connections.push(ws);
  console.log(`new connection established. connections: ${connections.length}`);
  ws.on("close", () => {
    console.log(`connection closeed: ${connections.length}`);
    connections = connections.filter((conn, i) => conn !== ws);
  });
  ws.on("message", message => {
    broadcast(JSON.stringify(message));
  });
});

const broadcast = message => {
  connections.forEach((con, i) => {
    con.send(message);
  });
};
```

起動して以下のメッセージが表示されれば準備完了です。

```bash
$ node server.js
server is now listening localhost:3000
```

# wscat で接続を確認する

サーバを起動したらまず [wscat](https://github.com/websockets/wscat) を使用して動作の確認をしましょう。wscat は npm でインストールできます。

```bash
$ npm install -g wscat
```

WebSocket サーバを起動し、wscat で接続したら任意のメッセージを送信してみましょう。１つのクライアントからの送信を受けて、他のクライアントへ broadcast していることがわかります。

![wscat](https://user-images.githubusercontent.com/20736455/77222169-36fb6b80-6b94-11ea-99e4-b5ed6d7307e7.gif)

# Artillery を使用して負荷テストを実行する

さて、ようやく本題です。Artillery を使用して負荷テストをかけてみましょう。
最小限のシナリオファイルのサンプルです。シナリオファイルは `senario.yml` のような名前をつけておきます。

```senario.yaml
config:
  target: "ws://localhost:3000"
  phases:
    - duration: 20
      arrivalRate: 10
scenarios:
  - engine: "ws"
    flow:
      - send: "hello"
```

以下、コマンドで実行します。

```bash
$ artillery run senario.yml
```

![run](https://user-images.githubusercontent.com/20736455/77239939-a10f2180-6c23-11ea-8943-a84eac8a9e19.gif)

## アクティブなコネクション数を調整する

実際に WebSocket を用いたアプリケーションでは、常に多くのコネクションが張られていることが一般的です。上記のシナリオでは hello というメッセージを送ったらすぐにコネクションを切断してしまうので、常時アクティブなコネクションが少ない状態であまり現実的ではありません。まずは、`think` を指定してアクティブなコネクション数が増えるように調整しましょう。また、同じメッセージを繰り返し送信する `loop` も指定できます。

```senario.yaml
scenarios:
  - engine: "ws"
    flow:
      - send: "hello"
      - think: 1 # pause for 1 second
      - loop:
          - send: "world"
        count: 5
```

## オブジェクトの送信に対応する

さて先ほどまでは string 形式のデータだけに限定していましたが、`{"name":"john", "age":24}` のようにオブジェクト形式で送信する方が良いこともあるでしょう。以下のように記述すれば、送信時に Stringify してくれます。

```senario.yaml
scenarios:
  - engine: "ws"
    flow:
      # the following will be stringified and sent as '{"name":"john","age":24}'
      - send:
          name: "john"
          age: 24
```

## カスタムコードを使用してタイムスタンプを付与する

また、送信したタイムスタンプを付与したいこともあります。このようなケースに対応するためにはカスタムコードを使用して柔軟に値を差し込むことができます。

```senario.yaml
config:
  target: "ws://localhost:3000"
  processor: "./custom.js"
scenarios:
  - engine: "ws"
    flow:
      # custom code for timestanp
      - function: "createTimestampedObject"
      - send: "{{ data }}"
```

非常にシンプルに記述し、シュッと実装することができました。Artillery は WebSocket だけではなく、HTTPのプロトコルもサポートしています。本記事は[公式ドキュメント](https://artillery.io/docs/)から参照していますので、詳細に理解されたい方は一度ドキュメントに目を通してみるのも良いでしょう。

また、CircleCIで継続的に負荷テストを実行するTipsを[こちらの記事](https://qiita.com/G-awa/items/6dd7d67eb569879ae3d1)で紹介していますのでぜひご参照ください。