Go 1.27、未読のレスポンスボディに対するHTTP/1接続再利用を修正
Go 1.27では、HTTP/1クライアントのパフォーマンスが向上し、未読のレスポンスボディを自動的に処理することで、コードの変更なしに接続の再利用が可能になりました。
英語の原文から自動翻訳されました。
Goチームはバージョン1.27をリリースし、レスポンスボディを完全に読み取らないHTTP/1クライアントにとって重要なパフォーマンス改善を導入しました。index.golang.orgを対象としたベンチマークテストでは、新しいランタイムは約150ミリ秒で100回の連続GETリクエストを完了し、Go 1.26の710ミリ秒と比較して大幅な高速化を実現しています。この5倍の速度向上は、使用されなかったレスポンスデータによりTCP接続の再利用が妨げられていた長年の非効率性を解決します。
何が起きたのか
Goでバックエンドサービスを開発するエンジニアは、標準のnet/httpパッケージとHTTP/1を使用する際に、サイレントなペナルティとして長年パフォーマンス低下に直面してきました。この問題は、クライアントがリクエストを送信したものの、クローズ前にレスポンスボディを完全に消費しない場合に発生します。Go 1.26以前では、HTTPクライアントは未読データを検知すると、再利用を試みず基盤となるTCP接続を破棄していました。これにより、以降のリクエストごとに新しい接続を作成する必要が生じ、そのたびにTCPおよびTLSハンドシェイクの全コストが発生していました。
Go 1.27での改善により、この挙動が変更されました。ランタイムは現在、resp.Body.Close()が呼び出された際に未読のレスポンスボディを自動的にドレイン(読み捨て)するため、接続が生きたまま維持され、将来のリクエストに利用可能になります。この変更は特にHTTP/1トラフィックを対象としており、HTTP/2が普及しているにもかかわらず依然として広く使用されています。開発者が手動でボディをドレインする必要をなくすことで、ステータスコードのみを確認したり、部分的なJSONレスポンスを解析したりするサービスにおいて、レイテンシとリソース消費を削減します。
仕組み
この修正を理解するには、HTTP/1がデータ送信をどのように処理するかを見る必要があります。単一の接続上で複数のストリームを多重化するHTTP/2とは異なり、HTTP/1はレスポンスをバイト列の連続ストリームとして順次送信します。クライアントはまずステータスラインとヘッダーを読み取り、その後ボディを読み取ります。接続を再利用するためには、クライアントは現在のレスポンスボディのすべてのバイトを読み取り、次のレスポンスがどこから始まるかを正確に把握する必要があります。もし未読のバイトが残っている場合、クライアントは次のメッセージの開始位置を安全に判断できず、接続を再利用するとプロトコルエラーが発生します。
以前のバージョンでは、開発者がボディ全体を読み取らずにresp.Body.Close()を呼び出すと、net/httpパッケージはその接続を利用不可としてマークしていました。その後、ソケットを閉じ、次のリクエストのために新しいソケットを開きます。このプロセスには、1往復時間(RTT)かかるTCPハンドシェイクと、TLS 1.2では2 RTT、TLS 1.3では1 RTTかかる可能性のあるTLSハンドシェイクが含まれます。高レイテンシネットワークでは、これらのハンドシェイクが総リクエスト時間を支配します。
Go 1.27では、Close()メソッドが修正され、接続をプールに戻す前にレスポンスボディバッファ内の残りのデータを自動的に読み取り・破棄します。これにより、次のリクエストに対してストリームの位置が正しく保たれます。ランタイムはこの処理を効率的に行い、プロトコルの正確性を確保しつつ不要なブロッキングを回避します。このメカニズムは、経験豊富なGo開発者が以前io.Discardへボディをコピーすることで手動で実装していたものに類似しています。
主要な詳細
- Go 1.27では、ベンチマークテストにおける100回の連続HTTP/1 GETリクエストの所要時間が約710msから約150msに短縮されます。
- この改善は特にHTTP/1接続に適用されます。多重化アーキテクチャを持つHTTP/2は、この問題の影響を受けませんでした。
- 従来のワークアラウンドでは、クローズ前に
io.Copy(io.Discard, resp.Body)を使用してレスポンスボディを手動でドレインする必要がありました。 - この修正により、
resp.Body.Close()が呼び出された際に未読バイトが自動的に消費され、接続の再利用性が保持されます。 - 接続再利用の失敗は、多くの場合
resp.StatusCodeのみを確認したり、末尾の改行を読み取らずにJSONを解析したりするコードで発生していました。 - この変更は下位互換性があり、既存のアプリケーションがこの恩恵を受けるためにコードを変更する必要はありません。
なぜ重要なのか
マイクロサービスやAPIクライアントを構築するソフトウェアエンジニアにとって、このアップデートは予期せぬレイテンシの一般的な原因を取り除きます。多くのアプリケーションは、HTTPステータスコードによって示されるAPI呼び出しの成否のみに関心を持ち、レスポンスボディを無視します。この修正がない場合、そのようなアプリケーションは絶え間ない接続の切り替え(churn)によるパフォーマンス低下に苦しむことになります。新しいTCPおよびTLS接続を確立するオーバーヘッドは、より高いネットワークレイテンシの中でサービスが地域間で通信する分散システムにおいて特に顕著です。
さらに、この変更は開発者の認知的負荷を軽減します。以前は、すべてのレスポンスボディがドレインされることを保証するために厳格なコーディング基準を強制する必要があり、カスタムラッパー関数やリンタルールが必要になることもありました。見逃されたケースは、コードが正しく機能しているように見えるため診断が難しい微妙なパフォーマンス低下を引き起こしていました。Go 1.27では、標準ライブラリがこのエッジケースを内部で処理するため、安全かつ効率的なHTTPクライアントの使用がデフォルトの挙動となります。
できること
- Goツールチェーンをバージョン1.27にアップグレードし、自動接続再利用の改善の恩恵を受けてください。
- 既存のHTTPクライアントコードを見直し、レスポンスボディが無視されているか部分的にしか読み取られていない箇所を特定してください。
- 接続再利用のためだけに追加された
io.Copy(io.Discard, resp.Body)などの手動ボディドレインロジックを削除してください。 - 負荷下でのサービスのレイテンシをテストし、接続確立オーバーヘッドの削減を観察してください。
- モニタリングツールで接続再利用率を追跡し、本番環境での改善を検証してください。
- 可能な限りHTTP/2を使用し続けてその inherent な多重化メリットを活かしますが、レガシーなHTTP/1エンドポイントに対してはこの修正に頼ってください。



