ElixirのHttpoisonでAPI負荷検証ツールをつくった

次世代Web言語として名高く並列処理が得意なElixir、そしてElixirのHTTPクライアントライブラリのHTTPoisonを使って、開発しているAPIの負荷検証ツールを作ってみました。

なお、作成にあたって、こちらの記事を大変参考にさせていただきました。
[ElixirのHTTPoisonライブラリの使い方](http://qiita.com/ColdFreak/items/4beb6cacfbb39baa955f)
[Phoenix で並列処理を使ったメタサーチ API を作ってみる](http://qiita.com/mserizawa/items/96f9d14e5ba24ccc8078)

要件はこんな感じです。
・テスト用に作成したAPIを並列で叩きまくる
・秒間リクエスト数を指定できるようにしたい
・処理継続時間を指定できるようにしたい

早速やってみましょう。（Elixirはインストール済みのものとします）

#プロジェクトの作成
プロジェクトの作成にはmixコマンドを使用します。

```
$ mix new performance_tool
```

すると、こんな感じのディレクトリ構成が自動で出来上がります。mix便利！

```
.
-- performance_tool
    |-- config
    |-- config.exs
    |-- lib
      `-- performance_tool.ex
    |-- mix.exs
    |-- README.md
    `- test
      |-- performance_tool_test.exs
      `-- test_helper.exs
```

#依存関係の整理のため、mix.exsを編集
HTTPoisonを使用するため、./performance_tool/mix.exsを編集します。

```
defmodule PerformanceTest.Mixfile do
  ...
  def application do
    [applications: [:logger, :httpoison]]
  end

  def deps do
    [
      { :httpoison, "~> 0.7.4"}
    ]
  end
end
```

applicationのところに:httpoisonと追記しておくことで、実際のコードでHTTPoison.startの記述を省略できます。

#実装
実際のコードは./performance_tool/lib/performance_tool.exに記述していきます。
ちなみにずっとJava一本で生きてきた私は「returnがないけどどうやって戻り値返すんだ？」と戸惑いましたが、どうやら関数の最終行の結果がそのまま関数の戻り値になるんだそうです。
また、Elixirでは実行結果を次に呼ぶ関数の第一引数に入れてくれるパイプ演算子(|>)というものが用意されているのでそれも使ってみます。

```
defmodule PerformanceTest do
  def run(process_num,seconds) do
    run(process_num,seconds,0)
  end

  #ここを再帰的に呼び出している
  def run(process_num,max_count,count) do 
    Task.async(fn -> send_requests_parallel(process_num,count) end)
    :timer.sleep(1000)
    run(process_num,max_count,count+1)
  end

  #run呼び出し時、max_countとcountが一致してた時だけこっちが動く
  def run(process_num,max_count,count) when max_count == count do 
    IO.puts("process end. count:#{count}")
  end

  def send_requests_parallel(process_num,count) do
    time_total = Enum.map(1..process_num, &Task.async(fn -> #関数の中身をprocess_numの回数分だけ非同期実行(戻り値=実行するtask)
      &1
      send_request("http://検証するAPIのURL")
    end))
    |> Enum.map(fn(task) -> Task.await(task,1000_000) end) #Task.asyncの内容が全部終わるまで待つ(戻り値=渡したtaskの実行結果)
    |> Enum.reduce(0, fn x,total -> total + x end) #実行時間を合計（ここの結果がtime_totalに格納される）

    #結果出力
    IO.inspect "#{count}, averave_time: #{time_total / process_num / 1000} ms, time_total: #{time_total / 1000} ms" 
  end

  def send_request(url) do
    #APIを叩いてかかった時間を返却する（今回はレスポンスは不要のためそのまま捨てているが、2つ目の戻り値にレスポンスが入っている）
    {time, _} = :timer.tc(fn -> HTTPoison.get!(url,[],[{:timeout, 10000000}]) end) 
    time
  end
end
```

#実行
では、実際に動かしてみましょう。
performance_toolに記述したrun関数に1秒あたりの並行処理数と実行秒数を渡して実行します。

```
$ cd performance_tool
$ iex -S mix
iex(1)> PerformanceTest.run(10,10)
"0, averave_time: 98.9 ms, time_total: 989.0 ms"
"1, averave_time: 70.3 ms, time_total: 703.0 ms"
"2, averave_time: 64.3 ms, time_total: 643.0 ms"
"3, averave_time: 65.9 ms, time_total: 659.0 ms"
"4, averave_time: 62.8 ms, time_total: 628.0 ms"
"5, averave_time: 50.0 ms, time_total: 500.0 ms"
"6, averave_time: 62.3 ms, time_total: 623.0 ms"
"7, averave_time: 51.6 ms, time_total: 516.0 ms"
"8, averave_time: 120.2 ms, time_total: 1202.0 ms"
"9, averave_time: 50.0 ms, time_total: 500.0 ms"
process end. count:10
:ok
```


#まとめ
・Elixirの関数では最後の行の結果が戻り値として返る
・Task.asyncで新しいプロセスを作成して非同期実行
・Task.awaitでTask.asyncで作られたプロセスの終了を待つ
・パイプ演算子（|>）は便利で楽しい