次世代Web言語として名高く並列処理が得意なElixir、そしてElixirのHTTPクライアントライブラリのHTTPoisonを使って、開発しているAPIの負荷検証ツールを作ってみました。
なお、作成にあたって、こちらの記事を大変参考にさせていただきました。
ElixirのHTTPoisonライブラリの使い方
Phoenix で並列処理を使ったメタサーチ API を作ってみる
要件はこんな感じです。
・テスト用に作成した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で作られたプロセスの終了を待つ
・パイプ演算子(|>)は便利で楽しい