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

[インデックス 16212] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける CloseNotifier インターフェースの実装に関する重要な修正です。具体的には、CloseNotifier が使用するチャネルをバッファリングすることで、ゴルーチンリーク(goroutine leak)の可能性を排除することを目的としています。

コミット

commit 61ed384a9651bbd57fd8cf5229c68ccc1ff2fca4
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Apr 22 10:32:10 2013 -0700

    net/http: make CloseNotifier channel buffered to not leak goroutines
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/8911044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/61ed384a9651bbd57fd8cf5229c68ccc1ff2fca4

元コミット内容

net/http: CloseNotifier チャネルをバッファリングしてゴルーチンをリークさせないようにする。

変更の背景

Goの net/http パッケージでは、HTTPハンドラがクライアント接続の切断を検知するための CloseNotifier インターフェースが提供されています。このインターフェースは、CloseNotify() メソッドを通じてチャネルを返し、クライアントが接続を閉じた際にそのチャネルに値が送信されることでハンドラに通知します。

このコミット以前の実装では、CloseNotifier が返すチャネルはバッファなしチャネル(unbuffered channel)として作成されていました。バッファなしチャネルは、送信側と受信側が同時に準備できていないと、どちらか一方がブロックされるという特性を持っています。

問題は、HTTPハンドラが CloseNotify() を呼び出してチャネルを受け取ったものの、そのチャネルから値を受け取らない(例えば、ハンドラがチャネルを無視してすぐに終了する)場合に発生しました。この状況では、クライアント接続が切断された際に CloseNotifier の内部でチャネルへの送信操作が行われますが、そのチャネルには受信側が存在しないため、送信操作が永久にブロックされてしまいます。結果として、チャネルへの送信を試みているゴルーチンが終了できなくなり、メモリ上に残り続ける「ゴルーチンリーク」が発生していました。これは、特に多数のHTTPリクエストを処理するサーバーにおいて、時間とともにシステムリソースを枯渇させる深刻な問題となります。

このコミットは、このゴルーチンリークの問題を解決するために導入されました。

前提知識の解説

Go言語のゴルーチンとチャネル

  • ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。OSのスレッドよりもはるかに少ないメモリで作成でき、数千、数万といった単位で同時に実行することが可能です。Goの並行処理の根幹をなす要素です。
  • チャネル (Channel): ゴルーチン間で安全にデータを送受信するための通信メカニズムです。チャネルは、データの受け渡しを通じてゴルーチン間の同期も行います。

チャネルの種類

チャネルには大きく分けて2種類あります。

  1. バッファなしチャネル (Unbuffered Channel):

    • make(chan Type) のように作成されます。
    • 送信操作 (ch <- value) は、別のゴルーチンがそのチャネルから値を受信するまでブロックされます。
    • 受信操作 (<- ch) は、別のゴルーチンがそのチャネルに値を送信するまでブロックされます。
    • 送信と受信が同期的に行われるため、「同期チャネル」とも呼ばれます。
  2. バッファありチャネル (Buffered Channel):

    • make(chan Type, capacity) のように、容量を指定して作成されます。
    • 送信操作は、チャネルのバッファに空きがある限りブロックされません。バッファが満杯の場合のみブロックされます。
    • 受信操作は、チャネルのバッファに値がある限りブロックされません。バッファが空の場合のみブロックされます。
    • バッファの容量分だけ、送信側が受信側を待たずに値を送信できます。

ゴルーチンリーク (Goroutine Leak)

ゴルーチンリークとは、不要になったゴルーチンが終了せずにメモリ上に残り続ける現象を指します。これは、以下のような状況で発生しやすいです。

  • チャネルのブロック: バッファなしチャネルへの送信操作が、対応する受信操作が行われないために永久にブロックされる場合。または、バッファありチャネルが満杯の状態で送信操作がブロックされ、受信操作が行われない場合。
  • 無限ループ: 終了条件のない無限ループを持つゴルーチン。
  • リソースの解放忘れ: ネットワーク接続やファイルディスクリプタなどのリソースを適切にクローズしない場合。

ゴルーチンリークは、メモリ使用量の増加、CPUリソースの無駄遣い、最終的にはアプリケーションのパフォーマンス低下やクラッシュにつながる可能性があります。

net/http パッケージの CloseNotifier インターフェース

net/http パッケージの CloseNotifier インターフェースは、HTTPハンドラがクライアント接続の切断を検知するためのメカニズムを提供します。

type CloseNotifier interface {
    CloseNotify() <-chan bool
}

CloseNotify() メソッドは、読み取り専用の bool 型チャネルを返します。このチャネルは、クライアントが接続を閉じたときに true の値が送信されます。これにより、ハンドラはクライアントがリクエストを途中でキャンセルした場合などに、それ以上の処理を中止してリソースを解放するなどの対応を取ることができます。

技術的詳細

このコミットの核心は、net/http パッケージ内部で CloseNotifier の通知に使用されるチャネルの作成方法を変更することです。

変更前は、conn 構造体の closeNotify() メソッド内で、c.closeNotifyc というチャネルが以下のように作成されていました。

c.closeNotifyc = make(chan bool) // バッファなしチャネル

このバッファなしチャネルは、クライアント接続が切断された際に、CloseNotifier の内部ロジックがこのチャネルに true を送信しようとすると、もしハンドラ側が CloseNotify() を呼び出したものの、そのチャネルから値を受け取らない(つまり、受信側がいない)場合、送信操作がブロックされてしまいます。このブロックされた送信操作を実行しているゴルーチンがリークの原因となっていました。

変更後は、チャネルの作成時にバッファサイズを 1 に指定しています。

c.closeNotifyc = make(chan bool, 1) // バッファありチャネル (容量1)

容量が 1 のバッファありチャネルに変更することで、以下のようになります。

  1. 送信の非ブロック化: クライアント接続が切断された際、CloseNotifier の内部ロジックがチャネルに true を送信しようとします。このとき、チャネルには少なくとも1つのバッファスロットがあるため、受信側がすぐに存在しなくても送信操作はブロックされずに完了します。
  2. ゴルーチンリークの防止: 送信操作がブロックされないため、チャネルへの送信を担当していたゴルーチンは、その役割を終えて正常に終了することができます。これにより、不要なゴルーチンがメモリ上に残り続けることがなくなり、ゴルーチンリークが防止されます。
  3. セマンティクスの維持: CloseNotifier のセマンティクス(クライアント切断時に一度だけ通知する)は維持されます。バッファサイズが 1 であれば、複数回通知が送られることはありませんし、ハンドラがチャネルから値を受け取った場合も期待通りに動作します。

この修正は、net/http サーバーの堅牢性とリソース効率を向上させる上で非常に重要です。特に、短命なHTTPリクエストや、クライアントが途中で接続を切断するようなシナリオにおいて、サーバーが安定して動作するために不可欠な変更と言えます。

コアとなるコードの変更箇所

src/pkg/net/http/server.go

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -146,7 +146,7 @@ func (c *conn) closeNotify() <-chan bool {
  	tc.mu.Lock()
  	defer tc.mu.Unlock()
  	if c.closeNotifyc == nil {
- 		tc.closeNotifyc = make(chan bool)
+ 		tc.closeNotifyc = make(chan bool, 1)
  		if c.hijackedv {
  			// to obey the function signature, even though
  			// it'll never receive a value.

src/pkg/net/http/serve_test.go

テストケース TestCloseNotifierChanLeak が追加され、この変更がゴルーチンリークを防止することを検証しています。このテストは、CloseNotify() から返されたチャネルを意図的に読み取らないシナリオを複数回実行し、ゴルーチンがリークしないことを確認します。

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -1370,6 +1370,7 @@ func TestContentLengthZero(t *testing.T) {
  }
  
  func TestCloseNotifier(t *testing.T) {
+ 	defer afterTest(t)
  	gotReq := make(chan bool, 1)
  	sawClose := make(chan bool, 1)
  	ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) {
@@ -1405,6 +1406,31 @@ For:
  	ts.Close()
  }
  
+func TestCloseNotifierChanLeak(t *testing.T) {
+ 	defer afterTest(t)
+ 	req := []byte(strings.Replace(`GET / HTTP/1.0
+Host: golang.org
+
+`, "\n", "\r\n", -1))
+ 	for i := 0; i < 20; i++ {
+ 		var output bytes.Buffer
+ 		conn := &rwTestConn{
+ 			Reader: bytes.NewReader(req),
+ 			Writer: &output,
+ 			closec: make(chan bool, 1),
+ 		}
+ 		ln := &oneConnListener{conn: conn}
+ 		handler := HandlerFunc(func(rw ResponseWriter, r *Request) {
+ 			// Ignore the return value and never read from
+ 			// it, testing that we don't leak goroutines
+ 			// on the sending side:
+ 			_ = rw.(CloseNotifier).CloseNotify()
+ 		})
+ 		go Serve(ln, handler)
+ 		<-conn.closec
+ 	}
+}
+
 func TestOptions(t *testing.T) {
  	uric := make(chan string, 2) // only expect 1, but leave space for 2
  	mux := NewServeMux()

src/pkg/net/http/z_last_test.go

テストユーティリティファイルに、ゴルーチンリーク検知のためのパターンが追加されています。

--- a/src/pkg/net/http/z_last_test.go
+++ b/src/pkg/net/http/z_last_test.go
@@ -76,6 +76,7 @@ func afterTest(t *testing.T) {
  		"created by net/http/httptest.(*Server).Start": "an httptest.Server",
  		"timeoutHandler":                               "a TimeoutHandler",
  		"net.(*netFD).connect(":                        "a timing out dial",
+ 		").noteClientGone(":                            "a closenotifier sender",
  	}
  	var stacks string
  	for i := 0; i < 4; i++ {

コアとなるコードの解説

このコミットの最も重要な変更は、src/pkg/net/http/server.go ファイルの conn 構造体内の closeNotify() メソッドにおけるチャネルの初期化部分です。

変更前:

c.closeNotifyc = make(chan bool)

これはバッファなしチャネルを作成します。このチャネルに値を送信しようとするゴルーチンは、別のゴルーチンがその値を読み取るまでブロックされます。もし読み取るゴルーチンが存在しない場合、送信ゴルーチンは永久にブロックされ、リークします。

変更後:

c.closeNotifyc = make(chan bool, 1)

これは容量が 1 のバッファありチャネルを作成します。これにより、CloseNotifier の内部ロジックがチャネルに true を送信する際に、受信側がすぐに存在しなくても、その値はバッファに格納されるため、送信操作はブロックされずに完了します。結果として、送信を担当するゴルーチンは正常に終了し、リークが防止されます。

TestCloseNotifierChanLeak テストの追加は、この修正が意図通りに機能することを保証するためのものです。このテストは、CloseNotify() が返すチャネルを意図的に消費しないシナリオをシミュレートし、それでもゴルーチンがリークしないことを確認します。これは、実際のアプリケーションでハンドラが CloseNotify() を呼び出すものの、その通知を処理しない場合に発生する可能性のある状況を再現しています。

z_last_test.go への変更は、テストフレームワークがゴルーチンリークを検知する際に、CloseNotifier の送信側に関連するスタックトレースを適切に識別できるようにするためのものです。これにより、将来的に同様のリークが発生した場合に、より容易にデバッグできるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のチャネルに関する一般的な解説記事
  • Go言語におけるゴルーチンリークに関する技術ブログやフォーラムの議論