我々の時代における、未解決のプログラミングの問題とは何でしょうか。新しい HTTP クライアントライブラリが絶えず発生するという事実から考えれば、HTTP の呼び出しが挙げられます。一般的に、新しい機能や非同期 API が注目されがちですが、実際の IO の部分、特にパフォーマンスに関してはどうでしょう?

どちらかといえば詳細なケース

比較する側面が多すぎるので、総合的なパフォーマンス比較は行いません。私のユースケースは、潜在的に大きなファイルを早いネットワークからダウンロードするという、若干細かい内容です。リモートサーバーからダウンロードする際は、ネットワーク接続がネックになりがちです。では、ローカルネットワークにサーバーがあって、バンド幅が問題で無くなった場合はどうでしょうか? IO vs NIO の関係に変化はあるでしょうか?この点に関しては「NIO の方が遥かに早い」といったものから「IO の方が早いから、これが新しい NIO だ」といった噂や、時代遅れの内容がインターネット上にあふれています。これにはベンチマーク測定が必要です。

測定対象

Client Version Builds on Language
Apache httpcomponents-client 4.2.5 Java IO Java
Apache commons-httpclient (discontinued) 3.1 Java IO Java
Apache HttpAsyncClient (dev) 4.0-beta3 Java NIO, async Java
Bee 0.21.0 Java HTTP Scala
soke-http 3.0.0 Finagle, Netty Scala
Dispatch 0.10.0 Ning Async HTTP Client, Netty Scala
cURL 7.24.0 x86_64-apple-darwin10.8.0 C

テスト

私は隔離されたネットワーク内の専用ハードウェアにおいてではなく、自分のワークステーションでテストを実行するという、少々非科学的な方法をとっています。これを補うために、私は経時的に何度もテストランを実行しており、その平均化によって有効な傾向が得られます。結果を見ると、記録された時間にはほとんど差異がありません。

私は、ローカルネットワークの Nexus サーバーからバイナリーファイルを 3 つダウンロードしています。

File Size
small ~ 80 KB
medium ~ 7 MB
large ~ 30 MB

各テストは、Java 仮想マシンを起動してファイルを一つダウンロードします。それぞれの API が許可するのと同じく、テストはクライアントインスタンスの作成と起動をくくり出し、ディスク速度に対する純粋なリクエストを測定しようとします。OSX で JDK 1.6 を実行し、全ての書込みは SSD へ。

小ファイル

http_io_perf_small

cURL はネイティブであるため、ほとんどの JVM ベースのソリューションより遥かに優れています。 commons-httpclient が 2 番目です。ファイルのサイズから考えると、ここでの決定要素は JVM の起動、接続の確立とリクエスト生成のオーバーヘッドです。

中ファイル

http_io_perf_med

Bee はかなりのペースでその地歩を失っています。Apache の非同期クライアントは既に倍の早さで、他のソリューションは中程度、しかし依然として commons-httpclient が最速です。

大ファイル

http_io_perf_large

面白くなってくるのはここからです。Apache の非同期クライアントですら cURL に勝るように、NIO の低水準の IO 操作には変化が現れてきています。Bee は遥か彼方にあり、また興味深い事に、古典的な Apache が今度は Netty ベースのライブラリを破りました。しかし、総合的なダウンロード時間における勝者は依然として、古典的な IO を利用している、開発終了となった commons-httpclient です。

NIO について一言

中身を取り上げる前に、NIO について一言。NIO とは「ノンブロッキング IO」の略称だとよくいわれていますが、これは明らかに「新しい IO API」の略でしょう。ちなみに、NIO.2はどうやら「もっと新しい IO API」のようです。マーケティングはこれで決まりですね。

既に知られているノンブロッキング/非同期の側面以外にも、NIO には OS のネイティブ実装によく似たマッピングを行う、非常に効率的かつ低水準の IO 操作が大量に備わっています。「古典的」な IO が、メモリーのデータをカーネル/ネットワークソケットから、ユーザー/アプリケーションバッファ、そして再びカーネル/ファイルへとコピーしている中、NIO ならば複数のバッファコピーやカーネル/ユーザー間のコンテキストスイッチを回避しながらこれを大量に行えます。

NIO の「非同期」性の側面は、ロー・スループット向けというよりは、スケーラビリティ的な機能です。Netty や Grizzly に秘められているイベント駆動型の設定は、あまり忙しくない接続を大量に扱わなくてはならないサーバーに重宝されます。

その中身

まず、大ファイルテストで最も遅かった Bee クライアントから考察します。大半の時間は JDK クラスでバイトのコピーに費やされている他、最も劣悪な GC profile も有しており、これは Apache の非同期の倍のピークメモリを利用しています。 (20MB vs 10MB):

http_io_bee

次は soke-http ですが、これは Twitter の Finagle のスリムラッパーであり、中身は Netty を利用しています。これは NIO を利用していますが、それでもかなりの量のバッファのコピーが行われています。

http_io_soke

すぐ後ろの 3 番目に控えている Dispatch は、Scala の事実上の「標準的」HTTP クライアントです。これは Twitter のライブラリよりも効率的に Netty を利用していますが、Ning はターゲットファイルへの書込みに優れていません。

http_io_dispatch

かなり意外だったのが Apache の古典的な HTTP クライアントであり、そのプロファイルは非常に似通っているものの、これは普通のストリームを使用しています。一つ考えられる理由として、イベント駆動型ソリューションである soke-http と Dispatch には、両方とも GC サイクルが寄与している一方で、Apache は二つとも閾値未満を維持していたという点です。

http_io_ahc

最も効率的な IO は、Apache の非同期クライアントのゼロコピー実装によるものです。これはファイルの書込みに NIO を利用する事で、C 言語によって書込まれたツールを負かしています。

IBM は、Apache の非同期が利用している「ゼロコピー」の発想をよくまとめています。

http_io_ahc_async

しかしそれでも、最終的には commons-httpclient が僅かながらのリードでトップに立ちます。どういう事でしょうか?まず、IO のパフォーマンスがその後任である Apache HttpClient 4.0 と同水準である点は、同じ機能を利用しているため驚くに足りません。Apache の非同期クライアントにおいては IO で劣りますが、アプリケーションレベルではより早い実行能力でこれを補っています。

http_io_ahc3

このテストでは、単一のファイルをダウンロードするために単一のスレッドを利用している事から、イベント駆動型の、非同期性ライブラリに対する偏りがはっきりと見られます。ですが、ここでのポイントは IO の部分であり、この結果からは NIO の低水準の実装が古典的な Java IO よりもかなり早い事が明確に分かります。一方、この結果からは大ファイルを取り扱っている場合でも、パフォーマンスの向上が効率性に劣るクライアントモードの影に潜んでしまうという事が分かります。

なぜこれが重要なのか?

概ね、30MB のコピーには 500ms を要しますが、これ自体は取るに足らない数値です。しかし、例えばビルドツールに関して検討してみましょう。仮に 50 ファイルをダウンロードする必要がある場合、その違いは歴然としています。しかも、それは「煩わしい程の遅さ」と「精神的なコンテキストスイッチを必要とする程ではない遅さ」の丁度中間に位置する悩みです。

既に述べたように、これはかなり具体的なユースケースであり、いかなる一般的な HTTP クライアントライブラリに対する甲乙をつけるものではありません。しかし、これらの点は見過ごされる傾向にあります、あるいは、当然の事と思われたり、スタックの奥深くにしまい込まれてしまうのです。

*本ブログは Atlassian Blogs の翻訳です。本文中の日時などは投稿当時のものですのでご了承ください。
*原文 : 2013 年 7 月 11 日 “HTTP Client Performance – IO