HN 日本語サマリー

← 一覧へ戻る
プログラミング

Go 1.27におけるHTTP/1接続の再利用性の向上

Better HTTP/1 connection reuse in Go 1.27 (go.dev)

114 pointsby allddd20 コメント

要約

Go 1.27では、HTTP/1接続の再利用性が大幅に改善され、パフォーマンスが約5倍向上しました。これは、以前のバージョンではHTTP/1接続でレスポンスボディが完全に読み取られない場合に接続が再利用されず、毎回新しい接続が確立されていたためです。Go 1.27ではこの問題を解決し、接続のセットアップコストを削減することで、HTTPリクエストの処理速度を向上させています。

全文翻訳

Go Blog HTTP/1接続の再利用性 Nicholas Husin と Damien Neil 2026年10月14日 以下の関数を見てみましょう。 func getStatus(url string) (int, error) { resp, err := http.Get(url) if err != nil { return 0, err } defer resp.Body.Close() return resp.StatusCode, nil } これはurlにGETリクエストを送信し、HTTPステータスコードを返します。HTTP/1(GODEBUG=http2client=0)を使用した場合、私のマシンでGo 1.26でこの関数をindex.golang.orgに対して100回呼び出すと、約710ミリ秒かかります。Go 1.27では、同じプログラムは約150ミリ秒で実行されます。これは約5倍高速です!このような劇的なパフォーマンスの違いはどのようにして可能なのでしょうか? HTTP接続の確立コスト 違いを理解するために、HTTPリクエストを行う際にネットワークで何が起こるかを考えてみましょう。Goのnet/httpパッケージは、HTTP/1.0、HTTP/1.1、およびHTTP/2をサポートしています。これらすべてはTCP上で実行され、HTTPSエンドポイントと通信する場合は通常TLS上で実行されます。 新しい接続 over リクエストは、次のようなものになります。 TCPハンドシェイク(1 RTT):クライアントはSYNパケットを送信し、サーバーはSYN-ACKで応答し、クライアントはACKで応答します。クライアントは最初のTLSメッセージを最後のACKとともに送信できるため、TCP接続の確立には1ラウンドトリップ時間(RTT)がかかります。 TLSハンドシェイク(2 RTT):次に、TLSハンドシェイクで暗号化キーが交換され、サーバーのIDが検証されます。TLS 1.2を使用すると、これには2 RTTかかります。 HTTPリクエスト:キーが交換されると、合計3 RTT後にHTTPリクエストが最終的に送信できるようになります。 これらのラウンドトリップ(TLSハンドシェイクに費やされるCPU時間の一部を含む)は、すぐに積み重なります。クロスリージョン通信では、片道のレイテンシは、上記の図に示されている50ミリ秒(100ミリ秒RTT)に達するか、それを超えることがよくあります。接続をセットアップするだけで費やされる300ミリ秒と比較すると、HTTPリクエスト自体は、帯域幅が豊富であるか、HTTPレスポンスが小さい場合、わずか1 RTT強で提供できるため、安価になる可能性があります。 より有利な条件であっても、新しい接続を開くことは依然として高価になる可能性があります。私たちのオープニングベンチマークは、世界中に分散されており、TLS 1.3をサポートするindex.golang.orgにクエリを実行します。これは、レイテンシが比較的低く、TLSハンドシェイクが実際には1 RTTで完了できることを意味します。それでも、パフォーマンスの違いは依然として非常に顕著です。 接続の再利用 すべてのリクエストでこのセットアップコストを支払うことを避けるために、HTTPクライアントとサーバーは、可能な場合は既存の接続を複数のリクエストで再利用しようとします。このメカニズムは、HTTPのバージョンによって異なります。 HTTP/1.0:公式仕様の一部ではありませんが、クライアントとサーバーは、Connection: keep-aliveヘッダーを送信することで接続の再利用を選択することがよくあります。どちらかのエンドポイントがヘッダーを省略するか、Connection: close ヘッダーを送信するまで、再利用は続行されます。 HTTP/1.1:接続はデフォルトで永続的であり、どちらかの側がConnection: close ヘッダーを送信して明示しない限り、後続のリクエストとレスポンスで再利用できます。 HTTP/2:単一のTCP接続 over 複数のリクエストとレスポンスを多重化できます。 接続の再利用が失敗する場合 HTTP/1(HTTP/1.0およびHTTP/1.1の両方)では、同じ接続上の複数のリクエストとレスポンスは、ワイヤ上でシーケンシャルに送信されます。2つのGETリクエストが同じTCP接続 over index.golang.orgに送信された場合、レスポンスは単一のバイトストリームとして到着します。 図に示すように、net/http.Getはバイトストリームからステータスラインとヘッダーを読み取り、解析してnet/http.Responseを populat し、すぐに返します。レスポンスボディは接続上に未読のまま残され、呼び出し元がnet/http.Response.Bodyから読み取るまで消費されません。レスポンスは同じストリーム上でバックツーバックで送信されるため、net/httpは、現在のレスポンスボディがContent-LengthまたはTransfer-Encodingヘッダーによって決定されるように最後まで読み取られた後でなければ、次のレスポンスの開始を見つけることができません。 ここで、resp.Bodyをまったく読み取らない例のプログラムに戻りましょう。 func getStatus(url string) (int, error) { resp, err := http.Get(url) if err != nil { return 0, err } defer resp.Body.Close() return resp.StatusCode, nil } Go 1.26まで、resp.Body.Closeが呼び出されたとき、net/httpはまだ未読のレスポンスボディがあることに気づきます。接続を再利用できないと判断し、既存の接続を破棄して、次のリクエストのために新しい接続を作成します。これにより、新しい接続を確立するコストが繰り返し発生します。(HTTP/2にはこの問題はありません。フレームのストリームを単一の接続 over 多重化するため、厳密にバックツーバックでレスポンスを送信するのではなく、あるストリーム上の未読レスポンスボディが他のリクエストが接続を共有することをブロックしません。) 接続の再利用が失敗する場所 HTTP/1接続の再利用のこの微妙な点は、無害に見えるコードでサイレントなパフォーマンス低下を引き起こす可能性があります。オープニング例のように、クライアントはAPIレスポンスのステータスコードのみを気にするかもしれません。しかし、それが通信しているAPIエンドポイントは、{