[インデックス 18115] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおける、HTTPコネクションのハイジャック(Hijack)処理に関する改善とバグ修正を含んでいます。具体的には、コネクションがハイジャックされた際に chunkWriter
が使用していた bufio.Writer
の参照を適切に解放することで、リソースリークを防ぎ、パフォーマンスを向上させています。また、二重に Hijack
が呼び出された場合の挙動を検証するテストが追加されています。
コミット
commit 92b5e16147b26dcea216b48b380566b900b4916b
Author: John Newlin <jnewlin@google.com>
Date: Thu Dec 26 11:52:14 2013 -0800
net/http: Release reference to chunkWriter's bufio.Writer on hijack
When a connection is hijacked, release the reference to the bufio.Writer
that is used with the chunkWriter. The chunkWriter is not used after
the connection is hijacked.
Also add a test to check that double Hijack calls do something sensible.
benchmark old ns/op new ns/op delta
BenchmarkServerHijack 24137 20629 -14.53%
benchmark old allocs new allocs delta
BenchmarkServerHijack 21 19 -9.52%
benchmark old bytes new bytes delta
BenchmarkServerHijack 11774 9667 -17.90%
R=bradfitz, dave, chris.cahoon
CC=golang-codereviews
https://golang.org/cl/39440044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/92b5e16147b26dcea216b48b380566b900b4916b
元コミット内容
このコミットの目的は以下の2点です。
net/http
パッケージにおいて、HTTPコネクションがハイジャックされた際に、chunkWriter
が内部で使用しているbufio.Writer
の参照を適切に解放すること。これにより、ハイジャック後の不要なリソース保持を防ぎます。Hijack
メソッドが複数回呼び出された場合の挙動を検証するテストを追加すること。
ベンチマーク結果も示されており、BenchmarkServerHijack
において、処理時間(ns/op)、アロケーション数(allocs)、および割り当てられたバイト数(bytes)がそれぞれ改善していることが報告されています。
変更の背景
Goの net/http
パッケージでは、HTTPハンドラが基盤となるTCPコネクションを直接制御できるようにする Hijacker
インターフェースが提供されています。これは、WebSocketやServer-Sent Events (SSE) のようなプロトコルを実装する際に非常に有用です。
従来の net/http
の実装では、HTTPレスポンスのボディをチャンクエンコーディングで送信するために chunkWriter
という内部構造体が使用されていました。この chunkWriter
は、効率的な書き込みのために bufio.Writer
を内部に保持していました。
問題は、コネクションが Hijack
された後も、chunkWriter
が bufio.Writer
への参照を保持し続けていた点にありました。Hijack
が呼び出されると、HTTPサーバーは基盤となるコネクションの制御をハンドラに完全に委譲するため、chunkWriter
やその内部の bufio.Writer
はもはや必要なくなります。しかし、参照が解放されないままだと、bufio.Writer
が保持していたバッファメモリが不必要に占有され続け、ガベージコレクションの対象となりにくくなる可能性がありました。これは、特に多数のコネクションがハイジャックされるようなシナリオにおいて、メモリ使用量の増加やパフォーマンスの低下につながる可能性がありました。
このコミットは、このリソースリークの可能性を解消し、ハイジャック後のリソース管理を最適化することを目的としています。また、Hijack
が複数回呼び出された場合の堅牢性を確保するためのテストも追加されています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の net/http
パッケージに関する知識が必要です。
net/http
パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。Webアプリケーション開発の基盤となります。http.ResponseWriter
: HTTPハンドラがクライアントにHTTPレスポンスを書き込むために使用するインターフェースです。http.Request
: クライアントからのHTTPリクエストを表す構造体です。http.Hijacker
インターフェース:
このインターフェースは、type Hijacker interface { Hijack() (c net.Conn, rw *bufio.ReadWriter, err error) }
http.ResponseWriter
が実装することができ、基盤となるネットワークコネクションと、そのコネクションに関連付けられたbufio.Reader
およびbufio.Writer
を取得することを可能にします。Hijack
が呼び出されると、net/http
サーバーはコネクションの制御をハンドラに完全に委譲し、それ以降のHTTP処理(例えば、レスポンスの自動フラッシュやコネクションのクローズ)は行いません。これにより、ハンドラは任意のプロトコル(例: WebSocket)をそのコネクション上で直接実装できます。bufio.Reader
とbufio.Writer
:bufio
パッケージは、I/O操作をバッファリングするための機能を提供します。bufio.Reader
は読み込みをバッファリングし、bufio.Writer
は書き込みをバッファリングします。これにより、システムコールを減らし、I/Oパフォーマンスを向上させます。chunkWriter
:net/http
内部で使用される構造体で、HTTP/1.1 のチャンク転送エンコーディングを実装します。大きなレスポンスボディを送信する際に、全体のサイズを事前に知らなくてもストリームでデータを送信できるようにします。chunkWriter
は内部にbufio.Writer
を持ち、これを通じて実際のネットワークコネクションにデータを書き込みます。putBufioWriter
関数:net/http
パッケージ内部で、bufio.Writer
オブジェクトをプールに戻すために使用される関数です。Goの標準ライブラリでは、頻繁に生成・破棄されるオブジェクト(特にバッファを持つもの)のGC負荷を軽減するために、オブジェクトプール(sync.Pool
など)が利用されることがあります。putBufioWriter
は、使用済みbufio.Writer
を再利用可能な状態に戻す役割を担います。
技術的詳細
このコミットの核心は、net/http
サーバーの response
構造体(http.ResponseWriter
の内部実装)の Hijack
メソッドの変更にあります。
response
構造体は、クライアントへのレスポンスを管理し、内部に chunkWriter
(w.cw
) や bufio.Writer
(w.w
) への参照を持っています。
変更前の Hijack
メソッドは、単に基盤となるコネクションの hijack
メソッドを呼び出すだけでした。このとき、response
構造体が保持していた bufio.Writer
(w.w
) は、chunkWriter
を通じて使用されていましたが、Hijack
後は不要になります。しかし、この bufio.Writer
への参照が明示的に解放されていなかったため、ガベージコレクタがそのメモリを回収するまでに時間がかかる可能性がありました。
このコミットでは、Hijack
メソッドが成功した場合に、以下の処理が追加されました。
putBufioWriter(w.w)
:response
構造体が保持していたbufio.Writer
(w.w
) をputBufioWriter
関数に渡します。これにより、このbufio.Writer
は内部のオブジェクトプールに返却され、再利用可能になります。これは、不要になったオブジェクトを明示的にプールに戻すことで、ガベージコレクションの負担を軽減し、メモリの再利用を促進する一般的な最適化手法です。w.w = nil
:response
構造体のw.w
フィールドをnil
に設定します。これにより、response
構造体からbufio.Writer
への参照が完全に切断されます。これにより、bufio.Writer
オブジェクトがガベージコレクションの対象となりやすくなります。
これらの変更により、コネクションがハイジャックされた直後に、不要になった bufio.Writer
が適切に解放され、メモリ効率が向上します。
また、TestDoubleHijack
という新しいテストが追加されました。このテストは、Hijack
メソッドが一度成功した後、再度呼び出された場合にエラーを返すことを検証します。Hijack
はコネクションの制御を一度だけ委譲するべきであり、二度目の呼び出しは論理的に不正であるため、エラーを返すのが正しい挙動です。これにより、Hijack
メソッドの堅牢性が向上します。
ベンチマーク結果が示すように、この変更は BenchmarkServerHijack
のパフォーマンスを大幅に改善しています。これは、不要なメモリ割り当ての削減と、ガベージコレクションの負荷軽減によるものと考えられます。
コアとなるコードの変更箇所
src/pkg/net/http/server.go
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -1198,7 +1198,14 @@ func (w *response) Hijack() (rwc net.Conn, buf *bufio.ReadWriter, err error) {
if w.wroteHeader {
w.cw.flush()
}
- return w.conn.hijack()
+ // Release the bufioWriter that writes to the chunk writer, it is not
+ // used after a connection has been hijacked.
+ rwc, buf, err = w.conn.hijack()
+ if err == nil {
+ putBufioWriter(w.w)
+ w.w = nil
+ }
+ return rwc, buf, err
}
func (w *response) CloseNotify() <-chan bool {
src/pkg/net/http/serve_test.go
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -1934,6 +1934,31 @@ func TestWriteAfterHijack(t *testing.T) {
}\n}\n\n+func TestDoubleHijack(t *testing.T) {\n+\treq := reqBytes(\"GET / HTTP/1.1\\nHost: golang.org\")\n+\tvar buf bytes.Buffer\n+\tconn := &rwTestConn{\n+\t\tReader: bytes.NewReader(req),\n+\t\tWriter: &buf,\n+\t\tclosec: make(chan bool, 1),\n+\t}\n+\thandler := HandlerFunc(func(rw ResponseWriter, r *Request) {\n+\t\tconn, _, err := rw.(Hijacker).Hijack()\n+\t\tif err != nil {\n+\t\t\tt.Error(err)\n+\t\t\treturn\n+\t\t}\n+\t\t_, _, err = rw.(Hijacker).Hijack()\n+\t\tif err == nil {\n+\t\t\tt.Errorf(\"got err = nil; want err != nil\")\n+\t\t}\n+\t\tconn.Close()\n+\t})\n+\tln := &oneConnListener{conn: conn}\n+\tgo Serve(ln, handler)\n+\t<-conn.closec\n+}\n+\n // http://code.google.com/p/go/issues/detail?id=5955\n // Note that this does not test the \"request too large\"\n // exit path from the http server. This is intentional;\n```
## コアとなるコードの解説
### `src/pkg/net/http/server.go` の変更
`response` 構造体の `Hijack` メソッド内で、`w.conn.hijack()` の呼び出し後に以下のロジックが追加されています。
```go
rwc, buf, err = w.conn.hijack()
if err == nil {
putBufioWriter(w.w)
w.w = nil
}
rwc, buf, err = w.conn.hijack()
: まず、基盤となるコネクションのhijack
メソッドを呼び出し、ネットワークコネクション (rwc
)、bufio.ReadWriter
(buf
)、およびエラー (err
) を取得します。if err == nil
:hijack
処理が成功した場合にのみ、以下のリソース解放処理を実行します。putBufioWriter(w.w)
:response
構造体が保持していたbufio.Writer
(w.w
) をputBufioWriter
関数に渡します。この関数は、bufio.Writer
をオブジェクトプールに返却し、再利用可能にします。これにより、不要になったバッファメモリが効率的に解放され、ガベージコレクションの負担が軽減されます。w.w = nil
:response
構造体のw.w
フィールドをnil
に設定します。これにより、response
構造体からbufio.Writer
オブジェクトへの参照が切断され、bufio.Writer
オブジェクトがガベージコレクタによって回収される準備が整います。
この変更により、Hijack
後の不要なリソース保持が解消され、メモリ効率が向上します。
src/pkg/net/http/serve_test.go
の変更
TestDoubleHijack
という新しいテスト関数が追加されました。
func TestDoubleHijack(t *testing.T) {
req := reqBytes("GET / HTTP/1.1\nHost: golang.org")
var buf bytes.Buffer
conn := &rwTestConn{
Reader: bytes.NewReader(req),
Writer: &buf,
closec: make(chan bool, 1),
}
handler := HandlerFunc(func(rw ResponseWriter, r *Request) {
conn, _, err := rw.(Hijacker).Hijack() // 1回目のHijack
if err != nil {
t.Error(err)
return
}
_, _, err = rw.(Hijacker).Hijack() // 2回目のHijack
if err == nil {
t.Errorf("got err = nil; want err != nil")
}
conn.Close()
})
ln := &oneConnListener{conn: conn}
go Serve(ln, handler)
<-conn.closec
}
このテストの主要なポイントは以下の通りです。
- テスト用のHTTPリクエストとコネクション (
rwTestConn
) を作成します。 HandlerFunc
を定義し、その中でrw.(Hijacker).Hijack()
を2回呼び出します。- 1回目の
Hijack
呼び出しは成功することを期待します。 - 2回目の
Hijack
呼び出しはエラーを返すことを期待します。もしエラーが返されなかった場合 (err == nil
)、テストは失敗します。これは、コネクションが一度ハイジャックされたら、それ以上ハイジャックできないというHijack
メソッドの正しいセマンティクスを検証しています。 - 最後に、コネクションをクローズし、サーバーが正常に終了するのを待ちます。
このテストの追加により、Hijack
メソッドの複数回呼び出しに対する堅牢性が保証されます。
関連リンク
- Go言語
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go言語
bufio
パッケージのドキュメント: https://pkg.go.dev/bufio - Go言語の
sync.Pool
について(オブジェクトプーリングの概念理解に役立ちます): https://pkg.go.dev/sync#Pool
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/39440044
は、このGerritの変更リストへのリンクです。) - Go言語のIssueトラッカー (Go Issues): https://github.com/golang/go/issues
- Go言語の公式ブログやドキュメント (一般的なGoの概念理解のため)
- Go言語のソースコード (
src/pkg/net/http/server.go
,src/pkg/net/http/serve_test.go
) bufio.Writer
の内部実装に関する一般的な知識- HTTP/1.1 チャンク転送エンコーディングに関する一般的な知識
- ガベージコレクションとメモリ管理に関する一般的な知識
- Go言語のベンチマークに関する一般的な知識