.NET
HttpClient

.NETのHttpClientの取り扱いには要注意という話

---2017/07/07 更新---
HttpClientのStatic化について注意点を記載。
クリスマスの記事を七夕に更新するという洒落乙な計らい。

  • 1つは、「開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント」リンクの一番下の方に 書いてあることですが、StaticにしてるとDNS変更が反映されないということも起きうるので、 リンク先に記載のあるように、HttpClientにコネクションを定期的にリサイクルするようにさせると。
  var sp = ServicePointManager.FindServicePoint(new Uri("http://foo.bar"));
  sp.ConnectionLeaseTimeout = 60*1000; // 1 minute
  • もう1つは、Cookieのやり取りをしている場合に、Cookieがキャッシュされてしまうという点。 これは下記のようにHttpClient生成時にUseCookiesをfalseにすれば回避が可能となる。 もしプロキシサーバを実装していて、Cookieを引き継がなければならないなら、単純にCookieヘッダーを追加してやればよい。
  var handler = new HttpClientHandler()
  {
      UseCookies = false,// trueにするとCookieがキャッシュされてしまうのでfalseにする`
  };
  var client = new HttpClient(handler);

はじめに

Advent Calendar 2016、クリスマスの投稿。
当初クリスマスにまつわる何かを、と言ってましたが、特に思い浮かばなかったので普通に技術ネタを。
(あえてクリスマスにつなげるとしたら、この記事を私からのクリスマスプレゼントと思っていただければ・・・)

ではアドベントカレンダー最後の投稿内容として「.NETのHttpClientの取り扱いには要注意」という話をしましょう。

HttpClientとは

アプリケーション内でHTTPリクエストを投げたい場合に使うクラスです。
.NET 4.5から提供されはじめた機能で、それまではHttpWebRequestWebClientがありましたが、簡単にHTTPリクエストを投げられるクラスとしてみなさんも使われているのではないでしょうか。

HttpClientをどう実装してますか?

みなさんはHttpClientクラスをどのようにアプリケーションに実装しているでしょうか。
(例えばどこかのURLに対してリクエストを投げてSomeResponseという戻り値を返すメソッドを用意する、と考えてみましょう)

こう?

public async Task<SomeResponse> CallAPI()
{
   var client = new HttpClient();
   client.PostAsync("{URL}");
   ...
}

それともこう?

public async Task<SomeResponse> CallAPI()
{
   using (var client = new HttpClient())
   {
       client.PostAsync("{URL}");
   }
   ...
}

HttpClientはIDisposableインターフェースを実装しているので、大抵はusingブロックで囲って使っているのではないのでしょうか。

ですが、実はこのどちらの実装にも問題があるのです。

HttpClientの仕様

まず、HttpClientをインスタンス生成した時、内部ではなにが起きているのでしょうか?
当たり前かもしれませんが、中では新しいソケットをopenしています。

つまり、上述のような実装でCallAPI()を何度も呼び出していると毎度新しいソケットをopenしてしまい、あっという間にリソースを食いつぶしてしまうのです。

では次にインスタンスをDispose()した場合はどうなるのでしょうか?
当然、openしたソケットを今度はcloseしようとします。
しかし、ソケットのcloseはすぐに済むものではありません。
ソケットをcloseする時、Windowsではまず状態がTIME_WAITに遷移して、そしてしばらくしてからようやく解放されるのです。

これはリクエストする回数が少ないのであれば問題は表出しませんが、大量にリクエストをする場合に大きなボトルネックとなる問題を孕んでいます。

ではどうすればよいのか

実はMicrosoftの公式ドキュメント「不適切なインスタンス化のアンチパターン」の中で、この問題について取り上げており、HttpClientを使った実装する時はそのインスタンスをstatic変数で持っておき、使いまわすのがよいとしています。
それをもとに先ほどのコードをちょっと書き換えてみましょう。

private static HttpClient client = new HttpClient();

public async Task<SomeResponse> CallAPI()
{
   client.PostAsync("{URL}");
   ...
}

このようにすればHttpClientのオブジェクトを使いまわす形になります。
(TimeOutの設定とかをするのならばコンストラクタ内でやりましょう)
心配になるのが、これが同時実行の時に大丈夫なのかどうかですが、どうやらこのHttpClientはそのような利用を想定した設計となっているようです。

また、もう一つ注意点をあげると、一つのHttpClientオブジェクトで一つのソケットなので、異なるホストにもリクエストを投げる場合は、別のオブジェクトを生成しておく方がよいでしょう。
(つまり、1ホストにつき1HttpClientオブジェクト)

おわりに

インスタンスは必要な時に生成し、不要になったら、破棄する。
これは開発者に染み付いた習慣のようなものです。
HttpClientの使い方を見てみると、必要な時にインスタンスを作成するものだと思うでしょう。
しかもそのクラスはIDisposableを実装しているのでusingで囲いたくもなります。
内部でいい感じに処理してくれているものかと思いきや、中身は意外とシンプルなことしかしておらず、公式レファレンスにもそのような仕様については書いていません。

そして、問題に気付くのは負荷が高い時だけで、パフォーマンス調査する場合も、まさかそこが問題とは思わないのではないでしょうか。

まあ、いずれにしてもHTTPリクエストが簡単にできるのは便利なので、このような罠に注意して開発していくしかないですね。
(正直このHttpClientの実装は変えた方がよいと思いますが)

最後に、HttpClientの問題について詳しく述べられているサイトをいくつか載せておきますので、より詳しく知りたい方はご参照ください。

ではでは、メリークリスマス。
そしてよいお年を。

開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント

YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE