Erlang
Elixir
ElixirDay 3

毒にも薬にもならないHTTPoisonとHTTPotionの話

More than 1 year has passed since last update.

Elixirには、HTTPoisonHTTPotionという名前の 非常に紛らわしい HTTP clientライブラリがあります。
この2つの違いについて、大きいデータをDLする時にどちらが良いだろう?という観点から、すこし調べた結果をまとめてみました。

調べてわかること

まず、調べてすぐに出てくるのが、elixirforumでの議論です。
ここから得られる情報で、だいたい以下のことが分かります

  • Github starの数で見るかぎりHTTPoisonの方が知名度が高い
  • どちらも裏側でErlangのHTTP clientを使っているが、HTTPoisonはhackneyを使い、HTTPotionはibrowseを使っている
  • HTTPotionはHTTPS URLアクセス時にサーバ証明書の検証を行ってくれない

ここだけ見るとHTTPoisonの方が良さそうな気持ちになりますが、
冒頭で記載したとおり、今回は大きいデータをHTTP GETで取ってくる際の性能に興味があったので、その観点でもう少し調べます。

続いて、自分の興味に近い話をしているのが、ElixirでHTTP streamingする話 です。
ここからは、だいたい以下のことが分かります。

  • 非同期DLは当然必須である
  • HTTPoisonで使われるhackneyは、非同期DLのコードを書いても、時々全データをRAMに載せようとすることがあるらしい(!?)
    • 詳細は追って書くとの話だが、調べた範囲では見つからない
    • お名前から察するに、こことかここのあたりが関係しそうである(ちゃんと読んでいない)
  • 非同期に加え、stream_next関数を呼ぶまで次のメッセージを受信しないonceオプションがあるが、Httpotionはこのオプションを直接指定できない。

自分の目で確かめてみたこと

百聞は一見に如かずということで、HTTPoisonまたはHTTPotionで指定URIをGETするescriptを書き、ファイルを非同期にDLするときの挙動を調べることにしました。

./dlixir -l httpoison|httpotion [--once] -u http://my-server/bbb1G.mp4 

みたいな形で、使うライブラリとonceオプションの有無を指定できるようにしています。

実験概要

  • escriptの事項は自宅メイン機のvirtual box上で動かしているUbutu 16.04 (特筆しない限りメモリ4GB, プロセッサ数 4)
  • ダウンロード対象は自宅サーバ上の1GBくらいのmp4ファイル
  • httpoison,httpotionのどちらを使うか / onceオプションの有無 の4パターンを実行し、以下を調べて見る
    • timeコマンドにより実行時間を調べてみる
    • topコマンドによりCPU・メモリ使用率を調べてみる

実行コマンドはおよそ以下の様になります。

top -d 1 -bc | grep beam > httpoison$i.txt &
(time ./dlixir -l httpoison -u http://my-server/bbb1G.mp4 -o test.mp4) >> log_httpoison.txt 2>&1
kill $(jobs -p)

実験結果と考察

timeコマンドによる実行時間

25回の試行のreal, user, sysの値の平均値を取った結果が以下です。

onceオプション無 onceオプション有
httpoison real: 44.66s
user: 41.69s
sys: 26.42s
real: 53.17s
user: 33.40s
sys: 20.08s
httpotion real: 60.22s
user: 37.84s
sys: 9.08s
real: 82.66s
user:57.10s
sys:28.38s

実時間(real)の比較だけで単純に見ると、httpoisonの方がhttpotionよりも全般的に速いようです。
しかし、user実行CPU時間とsystem実行CPU時間の和(user+sys)で見ると、httpotion (onceオプション無) → httpoison(onceオプション有) → httpoison(onceオプション無し) → httpotion(onceオプション有) の順で速い、という不思議な結果が得られました。

user+sysがrealよりも大きくなる場合と言うのは、CPUコアが複数ある時に処理を分散してると起こり得る症状です。
プロセッサ数が1つだった場合は実行時間の逆転があり得そうです。

…が、追加実験をしたところ、目に見えて違いが見える程ではなさそうです

プロセッサ数1、メモリ4GBでhttpoison, httpotion(オプション無し)を2回位実行してみましたが、微妙でした。
$ time ./dlixir -l httpotion -u http://my-server/bbb1G.mp4 -o test.mp4
finished!
real    1m7.841s
user    0m35.780s
sys 0m5.348s
$ time ./dlixir -l httpoison -u http://my-server/bbb1G.mp4 -o test.mp4
finished!
real    1m0.106s
user    0m28.716s
sys 0m9.208s
$ time ./dlixir -l httpotion -u http://my-server/bbb1G.mp4 -o test.mp4
finished!
real    1m5.822s
user    0m35.872s
sys 0m5.788s
$ time ./dlixir -l httpoison -u http://my-server/bbb1G.mp4 -o test.mp4
finished!
real    1m16.552s
user    0m36.992s
sys 0m9.060s

と言うか、プロセッサ数が4であった時に比べて、httpoison側のsysの時間が大分短くなっています。
コンテキストスイッチか何かで時間を使っているとかかもしれません。


topコマンドによる負荷状況

次に、各パターンで25回ずつ行った試行それぞれについて、topコマンドから得られるcpu使用率・メモリ使用率をマッピングしていきます

CPU負荷の比較(横軸は時間、縦軸は使用率)

onceオプション無 onceオプション有
httpoison httpoison.png httpoison_once.png
httpotion httpotion.png httpotion_once.png

httpoison(onceオプション無)が他に比べてCPUを酷使してるようです。大体2コアくらい使って処理してるように見えます。
httpotion(onceオプション無)は、何故か安定してCPU使用率が100%あたりまでです。
onceオプションをつけるとhttpoisonもhttpotionも使用率が150%あたりでどちらも変わらなくなるようです。

メモリ使用率の比較(横軸は時間、縦軸は使用率)

onceオプション無 onceオプション有
httpoison httpoison_mem.png httpoison_once_mem.png
httpotion httpotion_mem.png httpotion_once_mem.png

onceオプション無しだと書き込み処理のスピードに描かわらずデータをメモリにおいてしまおうとするためか、基本的にかなりメモリも食うようです。
特にhttpoisonのonceオプション無しはデータのかなりの量(4GBの20%なので800MBくらい)をデータにのせようとしていそうです。
逆にhttpotionはonceオプション無しでもメモリの使用量はそれなりの値(5%前後なので200MBくらい)に収まっていました。
こう見ると、onceオプション無しで良さそうに見えますが…

httpotionをonceオプションなし使っていて不穏な挙動が見えるので、個人的には非推奨です

図を作っている際にはでてこなかったのですが、プロセッサ数1,メモリ4GBで実行を行うときに、メモリアロケーションで落ちる現象を見ました。
$ time ./dlixir -l httpotion -u http://my-server/bbb1G.mp4 -o test.mp4
eheap_alloc: Cannot allocate 3280272216 bytes of memory (of type "heap").

httpoisonとhttpotionのメモリの図が逆…というわけでもないですし、
そもそも1GBのファイルDLで3GBくらいのメモリを割り当てようとしているようで、メモリリークしてそうでだいぶ不穏です。
現在の自分の実装がエラーケース等あまり見てない&close処理などなおざりにしてるせい、という可能性もあります。

まとめ

HTTPoisonとHTTPotionそれぞれを使って大きめのファイルをHTTP GETしてみた際の挙動を調べてみました

  • HTTPoison onceオプションなしが一番速いが、リソース食うのが許される時に限る
    • 許される時=メモリに一度DLしても良い時なので、非同期DLが求められることは少なさそうだが…
  • 上のような例を除いては、HTTPoisonでもHTTPotionどちらでも基本的にはonceオプションはつけた方が良さそう
    • でもHTTPotionは直接このオプション指定できないんだよなぁ…
    • ElixirでHTTP streamingする話 曰く、onceをつけた上でもHTTPoisonはメモリ上に全データを載せることがあるらしいが…今回の実験の範囲ではでてこなかった
  • 素直に使う分には、HTTPoisonの方が性能は良さそうである

おまけ

蛇足ですが、実装したり色々調べたりしてるうちに見つけた話をいくつかおまけとして書いておきます。

  • onceオプションをつけた時のstream_next関数の呼び方はHTTPotion(と言うかibrowse)とHTTPoisonで微妙に違う
    • ibrowseにonceオプションをつけた場合は初回からstream_next関数を呼ぶ必要がある。
    • HTTPoisonでonceオプションをつけると初回メッセージはstream_next関数は不要
    • そもそも初回メッセージが違う。ibrowseはヘッダ(Status Code付き)を受信し、httpoisonはStatus(ステータスコードとか)を受信する
  • HTTPotionのメッセージはデフォルトだと1MBくらいの単位で送られるが、HTTPoisonは1KBくらいの単位
    • 前者はibrowseオプションのstream_chunk_size値をいじれば制御可能。
      • ここでデフォルト値が1MBになっている模様
    • 後者は具体的に言うと1460B. MSSっぽさがある。
    • HTTPoisonがCPUガシガシ食う&sysの時間が高いのはこのせいかと最初思っていたが、自信なし
  • HTTPotionを使う時はtimeout時間がデフォルト5秒で設定されているので、今回のようなユースケースではオプションに timeout: :infinityを適宜入れる必要があった