Engineers2018-11-12Goのnet/httpクライアントで大量のリクエストを高速に行う方法Goこんにちは、門多です。Token形式)のプッシュ通知に対応しました。APIにも対応します。これらのプロトコルはどちらもHTTP/2に対応していますが、net/httpをデフォルトのまま使っても、想定していたほどの通知速度を出すことができません。この製品は速度をひとつの特徴としているので、HTTP/2でも一定の速度を出す必要がありました。改善の余地はあると思いますが、なぜデフォルトだと速度が出ないのか、と、速度を出すために行った実装を紹介します。1.11の頃に書かれたものです。)まずは、HTTP/2とnet/httpの動作について。サーバがHTTP/2に対応していれば優先的にHTTP/2を使って通信します。net/httpは、環境変数やその他いくつかの方法でHTTP/2を無効化できます。すればサーバが対応していてもHTTP/2を使いません。HTTP/2プロトコルネゴシエーション上の文章で「サーバがHTTP/2に対応していれば」と書きましたが、HTTP/1.xと同様に、HTTP/2も80または443ポートが使われます。そのためクライアントは、通信前にはHTTP/2またはHTTP/1.xのどちらの方法で通信すれば良いか判断ができません。プロトコルを判断するため、通信の最初に行うことがプロトコルネゴシエーションです。接続先のサーバでどのプロトコルが使えるのかは、以下のどれかで検出することが可能です。Negotiation)HTTPのConnectionとUpgradeヘッダネゴシエーションなし(ダイレクト)TLSが使える場合はALPNまたはNPNが使われます。TLSが使えない場合はConnectionやUpgradeヘッダが使われます。対応していないので、HTTPSが必須です。QuestionsALPNまたはNPNは、ALPNの方が効率がよく、他の慣習に沿っているので良いそうです。Prefaceというリクエストを送信します。これは誤ってHTTP/1.1サーバに接続してしまった場合であっても、間違ったまま通信をさせずエラーを引き起こさせるために送信しています。Prefaceの理由と経緯net/httpで大量のリクエストを実行するとどうなるかAPNsのHTTP/2プロトコルでは、1回のHTTP/2リクエストで1つの端末へ通知を行います。1回のリクエストで複数の端末に送る手段はないので、高速な配信を行うためには短時間で大量のリクエストを処理する必要があります。再接続のコストを抑えるため、HTTP/1.1にはコネクション再利用(Keep-Alive)のための仕様が含まれます。net/httpでは、Keep-Alive状態のコネクションをアイドルコネクションと呼んでいて、コネクションの管理はhttp.Transportが行います。http.Transportは内部をうまく隠蔽しているため、開発者はそれほど意識することなくKeep-Aliveの恩恵を受けることができます。逆に、隠蔽されていることにより、コネクションを個別に解放する方法がありません。そのため、http.Transportはグローバルに1つ用意して使いまわすことが推奨されます。理由がなければ、通常はhttp.DefaultTransportを使うのが良いでしょう。それぞれのコネクションは、次のようにアクティブとアイドル状態を遷移します。http.Response.Bodyをクローズするとアイドル状態へ遷移するただしhttp.Transport.MaxIdleConnsまたはhttp.Transport.MaxIdleConnsPerHostを超えた場合はすぐにクローズする一定時間(http.Transport.IdleConnTimeout)経過するまでアイドル状態を維持する期間内に同一ホストへ次のリクエストが行われればコネクションを再利用する期間内に使われなければコネクションをクローズするhttp.Transport.DisableKeepAliveがtrueの場合はそもそもKeep-AliveしないHTTP/1.1で並列リクエストまずはHTTP/1.1での話です。HTTP/1.1は上に書いたようにKeep-Aliveを行うため、スキーマドメインポート番号が同じ場合はアイドル状態のコネクションを再利用します。同一のコネクションに並行して別のリクエストを行うことができません。アイドル状態のものがなければnet/httpは新しく接続を行います。1.10までは最大接続数の制限がありません。1.11からはhttp.Transport.MaxConnsPerHostを設定すると、同一ホストへの同時接続上限を超えたらブロックするようになりました。リクエスト数と同等のTCPまたはTLSコネクションが作られます。OSのリソースも消費します。これらの動作は、文中にもいくつか挙げましたが、以下のパラメータで調整可能です。1.11以降)HTTP/2で並列リクエスト次にHTTP/2が使える場合です。アクセスするサーバがHTTP/2に対応しているかわからないので、net/httpでは、プロトコルネゴシエーションのため新しく接続するALPNまたはNPNでHTTP/2が利用可能か調べるHTTP/2として接続しているコネクションから同じホストのものを探す同じホストへのHTTP/2コネクションがあれば集約して片方を閉じる該当するものがなければそのまま使う以後HTTP/2として多重化のように動作します。クライアント側だけでみると、ALPN等で確認する時点でTCPとTLSのハンドシェイクは終わっています。集約したところで効率がどの程度速度が改善されるか分かりませんが、Goに限らずHTTP/2はそういうものらしいです。net/httpでは、HTTP/1.1とHTTP/2のコネクションは分けて管理しています。HTTP/1.1でアイドル状態のコネクションがあったとしても、HTTP/2では使われませんし、逆もありません。上記でみたように、短時間で大量のリクエストを行う場合においては、HTTP/2でも多くのコネクションが発生します。OSのリソースも消費しますし、TCPとTLSハンドシェイクを接続のたびに行うため、当然遅くなってしまいます。接続先のサーバが必ずHTTP/2に対応していると分かっているのであれば、接続の時点で確認を行う必要はありません。上で紹介したプロトコルネゴシエーションのうちネゴシエーションなしを利用すると、最初から既存のコネクションを探しに行くようになるため効率を上げることができます。このためには、golang.org/x/net/http2パッケージを使う必要があります。&http2.Transport{},}これで、OSのリソース消費量とハンドシェイクの遅さによる問題は改善します。このままでは、APNsの場合、秒間500リクエスト程度しか送ることができません。HTTP/2のmaxConcurrentStreamsなぜかというと、HTTP/2にはmaxConcurrentStreamsというパラメータがあって、これはSETTINGSフレームでサーバからクライアント、またはクライアントからサーバへ送られます。HTTP/2では1つのコネクションで複数のストリーム(リクエスト+レスポンス)を同時に行えますが、実際は同時にmaxConcurrentStreamsで指定された個数までしかリクエストを送ることができません。この値を超えて送ろうとした場合、net/httpはレスポンスが返ってくるまでリクエストをブロックします。秒間500リクエスト程度しか送れないのは、APNsの場合はこの値が500前後だった、ということです。日本語訳HTTP/2で並列リクエストのところでみた通り、HTTP/2のコネクションは基本的に1本へ集約されるので、どう頑張っても1コネクションでは秒間500リクエスト前後を超えることができません。一般的にはhttp.Transportを使い回すほうが良いとされますが、しかし通知対象が数十万件以上になる場合は500/s程度では全然足りませんので、速度を上げたい場合は複数のコネクションを扱わなければなりません。複数のコネクションを扱う場合は、単純に別のhttp2.Transportを使えば良いです。1.12から、ストリームの数がmaxConcurrentStreamsを超えた場合、ブロックせずに新しいコネクションを作成するようになりました。(以下http2.Transportも同じ)を複数扱うと決めた時点で、厄介なリソースの問題にも対処しなければならなくなります。http.Transportは内部でコネクションやゴルーチンを管理しており、これらのリソースはGCするだけでは回収されません。そのため、特に何も考慮せず使った場合はhttp.Transportを使うたびにリークします。これらのリソースを確実に解放するため、http.Response.Bodyを必ずクローズする全てのリクエストが終わったらhttp.Transport.CloseIdleConnectionsを必ず呼ぶの2点を忘れないようにしましょう。これを守ることでhttp.Transportの内部で管理していたリソースは解放されます。名前の通り、CloseIdleConnectionsはアイドル状態のコネクションしか対象としません。使われているものは対象外なので必ず全てのリクエストが終わってから呼びましょう。github.comHTTP/2での単体テスト少し本題からは外れますが、上記でネゴシエーションプロトコルを行わない代わりに必ずサーバはHTTP/2が必要、としました。現在のnet/httpでHTTP/2を使う場合は必ずHTTPSでの接続が必要です。GoでHTTPクライアントを使った単体テストを行う場合はnet/http/httptestをよく使いますが、これをHTTP/2対応させるためにはいくつか手続きが必要です。errorに与えます。s.Close()また、このままではクライアント側で証明書の検証が失敗するので、http.Transport.TLSClientConfig.InsecureSkipVerifyをセットして証明書の検証を無視する必要があります。明示的にHTTP/2を有効化する必要があります。t}GolangのhttptestでHTTP2を使うGoで証明書を無視するクライアントでHTTP2を使いたい終わりに細かい工夫は他にもありますが、誰かの参考になりそうなものを紹介しました。BoltzEngineは他にも速度制限やKindle対応など、実際の問題に対応していますし、体験版も用意していますので気軽にお問い合わせください。www.fenrir-inc.com最近は、無作為に通知登録を促したりするアプリやサイトが増えたことで、スパム扱いされることも多く見かけるようになりました。個人的な意見ですが、何が送られてくるかわからない通知を購読しようとは思わないし、特にWebPushではサイトを開くだけで通知ダイアログが表示されてブラウジングが妨げられるので不快だと感じます。通知という機能は正しく使えば便利なものなので、可能であればサービス提供者の伝えたいことではなくユーザの望むことを通知するために使って欲しいと思っています。HTTP/2では1つのグローバルIPアドレスから1分以内に3つ以上のトークンを使うと弾かれるなど、細かい話は色々あるので、機会があれば書くかもしれません。Modules(vgo)環境で利用する3つの方...こんにちは、アプリ部の門多です。アプリエンジニアの山口です。»このブログについてフェンリルのエンジニアが開発技術に関する様々なトピックをお送りします。(7)スターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。