Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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点です。

  1. net/http パッケージにおいて、HTTPコネクションがハイジャックされた際に、chunkWriter が内部で使用している bufio.Writer の参照を適切に解放すること。これにより、ハイジャック後の不要なリソース保持を防ぎます。
  2. 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 された後も、chunkWriterbufio.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.Readerbufio.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 メソッドが成功した場合に、以下の処理が追加されました。

  1. putBufioWriter(w.w): response 構造体が保持していた bufio.Writer (w.w) を putBufioWriter 関数に渡します。これにより、この bufio.Writer は内部のオブジェクトプールに返却され、再利用可能になります。これは、不要になったオブジェクトを明示的にプールに戻すことで、ガベージコレクションの負担を軽減し、メモリの再利用を促進する一般的な最適化手法です。
  2. 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言語のコミット履歴 (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言語のベンチマークに関する一般的な知識