System.Net.Http.HttpClient クラスを使用して HTTP サーバーと会話を行った際のソケットの使用と DNS サーバーの A レコードを変更した時の振る舞いの調査を .NET Core 3.1 及び .NET Framework 4.5.2 で行ってみた結果です。
HTTP で会話を行うプログラムを作るために、HttpClient クラスについてちょっと調べていたら、「一つのHttpClientオブジェクトで一つのソケットなので、異なるホストにもリクエストを投げる場合は、別のオブジェクトを生成しておく方がよい」という話と「StaticにしてるとDNS変更が反映されないという問題がある」という話があり、ほんとうにそうなのか疑問があったので、(当時のことは分からないが)現在の実装でもそのような振る舞いになるのかを調査してみた。
なお、現在の HttpClient のドキュメントには「static で実装して再利用する」こととスレッドセーフとして実装しているメソッドが明示されている(以下に再利用の説明部分を引用しておく)。
HttpClient は、一度インスタンス化し、アプリケーションの実行中に再利用することを目的としています。 すべての要求に対して HttpClient クラスをインスタンス化すると、大量の読み込みで使用可能なソケットの数が枯渇します。 これにより、SocketException エラーが発生します。 HttpClient を正しく使用する例を次に示します。
HttpClient クラス
調査及び考察は、HTTP クライアント側で HttpClient クラスを利用し稼働実績のある HTTP サーバーへ接続を行うことを前提とし、非同期メソッドを利用して行った。結果は次のとおり。
- インスタンスを1つ生成し、それを使いまわすことで複数の HTTP サーバーと会話を行うことができる。
- 異なる HTTP サーバーとの会話を始める際には、新しいソケットが開かれる(同時並行で会話が行われることも確認した)。
- アイドル状態が一定時間継続すると、ソケットは閉じられる。
- .NET Core 2.1 以降で HttpClient が利用する System.Net.Http.SocketsHttpHandler では、System.Net.ServicePointManager は使われていない。
- 「StaticにしてるとDNS変更が反映されない」 については、特に問題となるような振る舞いはない。
「一つのHttpClientオブジェクトで一つのソケットなので、異なるホストにもリクエストを投げる場合は、別のオブジェクトを生成しておく方がよい」については、まったくそのようなことはない。この説は明らかな誤りであろう。
(2020/03/31 追記)
記事を投稿後に、この説が書かれている Qiita の記事へコメントを書いておこうとしたが、コメントには Qiita のユーザー登録が必要であることがわかり、面倒なのでコメントはやめた。かわりにこの説が書かれていた記事へのリンクを貼っておく。「.NET(Framework)のHttpClientの取り扱いには要注意という話」
(2020/03/31 追記ここまで)
「DNSの変更が反映されないという問題がある」については、これを考察してみた結果を次に示す。
結論としては、特に問題となることはないであろうということになる。
問題とされている振る舞いは「HttpClient のインスタンスを使いまわすと DNS の変更が反映されない」ということ。
それでは、この状態はどのような際に問題となるのか。まずは確認した HttpClient の動作を次に示す(2点目3点目については HttpClient クラスの話ではなくなっているが)。
- アイドル状態が一定時間継続すると、ソケットは閉じられる。次にソケットが開かれる際には、DNS リゾルバから IP アドレスが引かれるが、 A あるいは AAAA レコードの TTL 設定時間が経過していれば、権威 DNS サーバーへの問い合わせが行われ、更新された A あるいは AAAA レコードから IP アドレスが引かれる。👈 当たり前の動作で全く問題ない
- ソケットは、「ESTABLISHED」状態が継続している間は閉じられない。したがって、DNS に IP アドレスを問い合わせることもない。
- 「ESTABLISHED」状態であっても、サーバー側でシャットダウン操作が行われれば、サーバーから 「TCP FIN」パケットが送られてくる。クライアント側は「CLOSE_WAIT」状態に移行し、以降順次「LAST_ACK」、「CLOSED」に移行しソケットは閉じられる。
問題となることが考えられるのは、次の条件がすべて当てはまった場合であろう。
- 権威 DNS サーバーで A レコードあるいは AAAA レコードの変更が行われる。
- 同一サーバーへの HTTP 要求メッセージの送信が「アイドル状態継続でのソケットクローズまでの時間」よりも短い間隔で行われ、ソケットのクローズが発生しない。
- HTTP サーバー上で状態情報を保持している。
- この保持している状態は、永続されるべきものである。
- 上記状態情報の保持が HTTP サーバーのローカル環境に閉じていて、変更前後の HTTP サーバーで状態情報の共有ができない。
HTTP 要求メッセージが頻繁に投げられることでアイドル状態継続でのソケットクローズが発生しないことは考えられるが、3点目から5点目までの条件を満たすような構成で HTTP サーバーが構築されることは、次の1点を除き、ないであろうことから、特に問題は発生しないであろうと考える。
上記の条件が当てはまる状態として考えられる1点とは、「サーバ事業者間での DB サーバーを含むサイトの引っ越し」であるが、この場合には引っ越し作業・動作確認終了後、DNS 更新情報の伝播に必要となると想定される時間が経過したのちに、旧サーバーの停止あるいはコンテンツ削除が行われるであろうことから、これについても特段問題となることはないと思われる。
なお、[開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント]で、HttpClient のインスタンスを使いまわすことによる弊害とされている「DNSの変更が反映されない」ことへの解決策として示されている方法の有効性についても検証したので、その結果を示しておく。
- .NET Framework: 現在でも ServicePoint.ConnectionLeaseTimeout で設定した時間が経過した次の HTTP 要求でリクエストヘッダに「Keep-Alive の無効(Connection: close)」が設定される(ただし、この方法はクライアント側の一連の HTTP 要求の終了時にソケットを閉じるという本来の使い方ではないことから問題がないとは言えない)。
- .NET Core 2.1 以降: 設定した時間が経過した次の HTTP 要求でリクエストヘッダに「Keep-Alive の無効(Connection: close)」を設定する方法を見つけることができなかった。
以下、調査で得られた主なデータを示す。なお、調査は netstat コマンド及び Wireshark を利用して行った。
複数の接続については、非同期メソッドを使用して、窓の杜(IPv4: 118.238.19.173)の 4MB 程度の zip ファイルと ITmedia(IPv4: 13.112.89.167, 54.64.158.206)の RSS ファイルに GET 要求を行って調査した。先に zip ファイルの GET 要求を行い、その転送中に RSS ファイルの GET 要求を行った。転送処理は次のように行われたことを確認した。
- 51114番ポートを開いて GET 要求を行い zip ファイルの転送開始
- 51115番ポートを開いて GET 要求を行い RSS ファイルの転送開始
- RSS ファイルの転送終了
- zip ファイルの転送終了
次の画像が zip ファイルの GET 要求の際の Wireshark の表示。
次の画像は RSS ファイルの GET 要求時の Wireshark の表示
次の画像は両ファイルの転送中の Wireshark の表示
次の画像は両ファイル転送終了後ソケットが閉じられた時の Wireshark の表示
DNS の A あるいは AAAA レコードの変更時の動作検証は、内部ネットワーク内で2つの仮想マシンを構築し、内部 DNS サーバーで A レコードを更新することで行った。調査で得られたデータについては、ごく当たり前の結果であることから掲載は行わない。