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

Elixirには、[HTTPoison](https://github.com/edgurgel/httpoison)と[HTTPotion](https://github.com/myfreeweb/httpotion)という名前の 
非常に紛らわしい
 HTTP clientライブラリがあります。
この２つの違いについて、大きいデータをDLする時にどちらが良いだろう？という観点から、すこし調べた結果をまとめてみました。

## 調べてわかること

まず、調べてすぐに出てくるのが、[elixirforumでの議論](https://elixirforum.com/t/httpoison-vs-httpotion/2221)です。
ここから得られる情報で、だいたい以下のことが分かります

- Github starの数で見るかぎりHTTPoisonの方が知名度が高い
- どちらも裏側でErlangのHTTP clientを使っているが、HTTPoisonは[hackney](https://github.com/benoitc/hackney)を使い、HTTPotionは[ibrowse](https://github.com/cmullaparthi/ibrowse)を使っている
- HTTPotionはHTTPS URLアクセス時にサーバ証明書の検証を行ってくれない
    - https://blog.voltone.net/post/7

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

続いて、自分の興味に近い話をしているのが、[ElixirでHTTP streamingする話](http://big-elephants.com/2016-12/http-streaming-in-elixir/) です。
ここからは、だいたい以下のことが分かります。

- 非同期DLは当然必須である
- HTTPoisonで使われるhackneyは、非同期DLのコードを書いても、時々全データをRAMに載せようとすることがあるらしい（！？）
    - 詳細は追って書くとの話だが、調べた範囲では見つからない
    - お名前から察するに、[ここ](https://github.com/benoitc/hackney/issues/379)とか[ここ](https://github.com/benoitc/hackney/issues/378)のあたりが関係しそうである（ちゃんと読んでいない）
- 非同期に加え、stream_next関数を呼ぶまで次のメッセージを受信しないonceオプションがあるが、Httpotionはこのオプションを直接指定できない。
    - ibrowseオプションとして与えるのは可能だが、その際はibrowse型のメッセージを受信する必要がある
        - 
 それだったらibrowse直接使うよね… 

    - master branchに[onceオプションを直接呼べるようにしようとしている形跡](https://github.com/myfreeweb/httpotion/blob/master/test/httpotion_test.exs#L135)ならある。

## 自分の目で確かめてみたこと
百聞は一見に如かずということで、[HTTPoisonまたはHTTPotionで指定URIをGETするescript](https://github.com/yaoshimax/dlixir)を書き、ファイルを非同期に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オプション有|
|---|---|---|
|<b>httpoison</b>|real: 44.66s <br> user: 41.69s <br> sys: 26.42s |real: 53.17s <br> user: 33.40s <br> sys: 20.08s |
|<b>httpotion</b>|real: 60.22s <br> user: 37.84s<br> sys: 9.08s|real: 82.66s <br> user:57.10s<br> sys:28.38s|

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

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

<details><summary>…が、追加実験をしたところ、目に見えて違いが見える程ではなさそうです</summary>
<div>
プロセッサ数1、メモリ4GBでhttpoison, httpotion(オプション無し）を２回位実行してみましたが、微妙でした。

```
$ 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
```

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


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

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

||onceオプション無|onceオプション有|
|---|---|---|
|<b>httpoison</b>|![httpoison.png](https://qiita-image-store.s3.amazonaws.com/0/146355/07c11f56-c199-49a5-37cb-f49992bb115d.png)|![httpoison_once.png](https://qiita-image-store.s3.amazonaws.com/0/146355/fc2ac89a-6a8f-8999-9ea0-1605cb96bf83.png)
|
|<b>httpotion</b>|![httpotion.png](https://qiita-image-store.s3.amazonaws.com/0/146355/7ba8cf91-69d9-978b-e8d1-35ca61eda4e1.png)|![httpotion_once.png](https://qiita-image-store.s3.amazonaws.com/0/146355/5a916d08-893a-1d84-b13a-1086e75af01e.png)|

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

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

||onceオプション無|onceオプション有|
|---|---|---|
|<b>httpoison</b>|![httpoison_mem.png](https://qiita-image-store.s3.amazonaws.com/0/146355/338d9b43-0455-9039-3f93-8f0f322034d2.png)| ![httpoison_once_mem.png](https://qiita-image-store.s3.amazonaws.com/0/146355/4358edb7-f091-164b-62d8-74833bc6e8dd.png)|
|<b>httpotion</b>|![httpotion_mem.png](https://qiita-image-store.s3.amazonaws.com/0/146355/3aed58e9-f579-d1b4-55f6-35aa55d4dea0.png)|![httpotion_once_mem.png](https://qiita-image-store.s3.amazonaws.com/0/146355/17285769-aa6b-debb-966e-0818ef76d7ba.png)|

onceオプション無しだと書き込み処理のスピードに描かわらずデータをメモリにおいてしまおうとするためか、基本的にかなりメモリも食うようです。
特にhttpoisonのonceオプション無しはデータのかなりの量（4GBの20％なので800MBくらい)をデータにのせようとしていそうです。
逆にhttpotionはonceオプション無しでもメモリの使用量はそれなりの値(5%前後なので200MBくらい)に収まっていました。
こう見ると、onceオプション無しで良さそうに見えますが…
<details><summary>httpotionをonceオプションなし使っていて不穏な挙動が見えるので、個人的には非推奨です</summary>
<div>
図を作っている際にはでてこなかったのですが、プロセッサ数１，メモリ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処理などなおざりにしてるせい、という可能性もあります。
</div></details>

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

- HTTPoison onceオプションなしが一番速いが、リソース食うのが許される時に限る
  - 許される時＝メモリに一度DLしても良い時なので、非同期DLが求められることは少なさそうだが…
- 上のような例を除いては、HTTPoisonでもHTTPotionどちらでも基本的にはonceオプションはつけた方が良さそう
  - でもHTTPotionは直接このオプション指定できないんだよなぁ…
  - [ElixirでHTTP streamingする話](http://big-elephants.com/2016-12/http-streaming-in-elixir/) 曰く、onceをつけた上でもHTTPoisonはメモリ上に全データを載せることがあるらしいが…今回の実験の範囲ではでてこなかった
- 素直に使う分には、HTTPoisonの方が性能は良さそうである
  - デフォルトのオプションでは、という話なので、チューニングの余地はありそう
  - [ibrowseはチューニングすると速い](https://github.com/ransomr/httpcbench)らしい（今回の試験には効かなさそうだが）

## おまけ

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

- 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値をいじれば制御可能。
        - [ここ](https://github.com/cmullaparthi/ibrowse/blob/master/src/ibrowse_http_client.erl#L76)でデフォルト値が1MBになっている模様
    - 後者は具体的に言うと1460B. [MSS](http://www.infraexpert.com/info/5adsl.htm)っぽさがある。
    - HTTPoisonがCPUガシガシ食う＆sysの時間が高いのはこのせいかと最初思っていたが、自信なし
- HTTPotionを使う時はtimeout時間がデフォルト５秒で設定されているので、今回のようなユースケースではオプションに `timeout: :infinity`を適宜入れる必要があった