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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける重要なバグ修正を目的としています。具体的には、HTTPサーバーがクライアントへのレスポンス書き込み中にエラー(例えば、書き込みタイムアウト)が発生した場合に、サーバーがその接続からの読み込みを永久にブロックしてしまう問題を解決します。この修正により、書き込みエラーが発生した際には即座に接続が閉じられ、サーバーのリソースが不必要に占有されることを防ぎます。

コミット

commit d1e16d06b4df98930b5c6b0775cdd414dfdebd50
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Feb 4 20:26:25 2013 -0800

    net/http: fix Server blocking after a Handler's Write fails
    
    If a Handle's Write to a ResponseWriter fails (e.g. via a
    net.Conn WriteDeadline via WriteTimeout on the Server), the
    Server was blocking forever waiting for reads on that
    net.Conn, even after a Write failed.
    
    Instead, once we see a Write fail, close the connection,
    since it's then dead to us anyway.
    
    Fixes #4741
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/7301043

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

https://github.com/golang/go/commit/d1e16d06b4df98930b5c6b0775cdd414dfdebd50

元コミット内容

このコミットは、Goの net/http パッケージにおけるHTTPサーバーの動作に関するバグを修正します。元の問題は、HTTPハンドラが ResponseWriter を介してクライアントにデータを書き込もうとした際に、何らかの理由で書き込みが失敗した場合に発生していました。例えば、サーバー側で設定された WriteTimeout が発生し、基盤となる net.Conn への書き込みが期限切れになった場合などです。このような状況下で、サーバーは書き込みが失敗したにもかかわらず、その net.Conn からの次のリクエスト(読み込み)を永久に待ち続けてしまい、結果として接続がブロックされ、リソースが解放されないという問題がありました。

変更の背景

この変更の背景には、HTTPサーバーの堅牢性とリソース管理の改善があります。HTTPサーバーは多数の同時接続を処理するため、各接続が適切に管理され、不要なリソースの占有を防ぐことが極めて重要です。

元の実装では、書き込みエラーが発生しても、サーバーは接続がまだ有効であると誤解し、その接続からのさらなるデータ(次のHTTPリクエストなど)を待ち続けていました。これは、以下のような問題を引き起こす可能性がありました。

  1. リソースリーク: サーバーがブロックされた接続を解放しないため、その接続に関連するメモリやファイルディスクリプタなどのリソースが不必要に占有され続け、サーバー全体のパフォーマンス低下やリソース枯渇につながる可能性がありました。
  2. デッドロック/ハングアップ: 特定の条件下では、サーバーが応答しなくなり、クライアントからの新しい接続を受け付けられなくなる可能性がありました。
  3. ユーザーエクスペリエンスの低下: クライアント側から見ると、サーバーが応答しない、またはタイムアウトするなどの問題が発生し、アプリケーションの信頼性が損なわれました。

この修正は、書き込みエラーが発生した時点でその接続は「死んでいる」と判断し、即座に接続を閉じることで、これらの問題を根本的に解決し、サーバーの安定性と効率性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の net/http パッケージとネットワークプログラミングに関する基本的な概念を理解しておく必要があります。

  • net/http パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。Webアプリケーション開発の基盤となります。
  • http.Server: HTTPサーバーを構成および実行するための構造体です。ListenAndServe メソッドなどを使用してリクエストを待ち受け、処理します。
  • http.Handlerhttp.ResponseWriter:
    • http.Handler インターフェースは、HTTPリクエストを処理するための ServeHTTP(ResponseWriter, *Request) メソッドを定義します。
    • http.ResponseWriter インターフェースは、HTTPレスポンスをクライアントに書き込むためのメソッド(Write, WriteHeader, Header など)を提供します。ハンドラはこのインターフェースを通じてクライアントに応答を送信します。
  • net.Conn: ネットワーク接続を表すインターフェースです。Read メソッドでデータを受信し、Write メソッドでデータを送信します。TCP/IPソケットのような低レベルの接続を抽象化します。
  • WriteTimeout: http.Server の設定オプションの一つで、レスポンスの書き込み操作が完了するまでの最大時間を設定します。この時間を超えると、書き込み操作はエラー(通常は net.OpError)を返します。
  • ブロッキングI/O: ネットワークプログラミングにおけるI/O操作(読み込みや書き込み)の一般的なモデルです。ブロッキングI/Oでは、操作が完了するまで(データが読み込まれるか、書き込まれるか、またはエラーが発生するまで)プログラムの実行が一時停止します。この問題では、書き込みが失敗したにもかかわらず、サーバーが読み込み操作でブロッキングし続けていました。
  • chunkWriter: HTTP/1.1 のチャンク転送エンコーディングを処理するための内部的なライターです。大きなレスポンスボディを送信する際に、ボディ全体を事前にバッファリングすることなく、チャンク単位で送信するために使用されます。各チャンクは、そのサイズを示すヘッダとデータ本体、そして終端を示すゼロサイズのチャンクで構成されます。

技術的詳細

このバグは、net/http パッケージの内部実装、特に chunkWriterResponseWriterWrite メソッドの背後でどのように動作するかに関連していました。

  1. 問題の発生メカニズム:

    • HTTPハンドラが ResponseWriter.Write を呼び出し、大量のデータを送信しようとします。
    • http.ServerWriteTimeout が設定されており、ネットワークの遅延やクライアントの受信速度が遅いなどの理由で、書き込み操作がタイムアウトします。
    • chunkWriter は、チャンクのサイズや実際のデータ、そしてチャンクの終端を示すCRLF (\r\n) を基盤となる net.Conn に書き込みます。
    • タイムアウトにより、これらの書き込み操作の途中でエラーが発生します。
    • しかし、エラーが発生したにもかかわらず、chunkWriter やその上位のロジックは、基盤となる net.Conn を明示的に閉じませんでした。
    • 結果として、http.Server はその接続がまだ有効であると判断し、次のHTTPリクエストを読み込むために、その net.ConnRead 操作を待ち続けました。この Read 操作は、クライアントが新しいデータを送信しない限り、永久にブロッキング状態に陥りました。
  2. 修正のアプローチ:

    • 修正は、chunkWriter.Write メソッド内に、書き込みエラーが発生した場合に即座に基盤となる net.Conn を閉じるロジックを追加することによって行われました。
    • chunkWritercw.res.conn.rwc を通じて net.Conn にアクセスできます。rwc は "read-write closer" の略で、読み書き可能な接続を表し、Close() メソッドを持っています。
    • チャンクサイズを書き込む際、またはチャンクデータとCRLFを書き込む際にエラーが発生した場合、cw.res.conn.rwc.Close() が呼び出され、接続が強制的に閉じられます。
    • 接続が閉じられると、サーバーがその接続でブロッキングし続けることはなくなり、関連するリソースも解放されます。

この修正により、書き込みエラーが発生した接続は速やかにクリーンアップされ、サーバーの安定性とリソース効率が向上します。

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

変更は主に src/pkg/net/http/server.gochunkWriter 構造体の Write メソッドに集中しています。また、このバグを再現し、修正を検証するための新しいテストケースが src/pkg/net/http/serve_test.go に追加されています。

src/pkg/net/http/server.go の変更点:

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -223,6 +223,7 @@ func (cw *chunkWriter) Write(p []byte) (n int, err error) {
 	if cw.chunking {
 		_, err = fmt.Fprintf(cw.res.conn.buf, "%x\r\n", len(p))
 		if err != nil {
+			cw.res.conn.rwc.Close()
 			return
 		}
 	}
@@ -230,6 +231,9 @@ func (cw *chunkWriter) Write(p []byte) (n int, err error) {
 	if cw.chunking && err == nil {
 		_, err = cw.res.conn.buf.Write(crlf)
 	}\n+	if err != nil {
+		cw.res.conn.rwc.Close()
+	}
 	return
 }

src/pkg/net/http/serve_test.go の変更点:

新しいテスト関数 TestOnlyWriteTimeout とヘルパー構造体 trackLastConnListener が追加されています。

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -326,6 +326,66 @@ func TestServerTimeouts(t *testing.T) {
 	}\n }\n \n+// golang.org/issue/4741 -- setting only a write timeout that triggers\n+// shouldn't cause a handler to block forever on reads (next HTTP\n+// request) that will never happen.\n+func TestOnlyWriteTimeout(t *testing.T) {\n+\tvar conn net.Conn\n+\tvar afterTimeoutErrc = make(chan error, 1)\n+\tts := httptest.NewUnstartedServer(HandlerFunc(func(w ResponseWriter, req *Request) {\n+\t\tbuf := make([]byte, 512<<10)\n+\t\t_, err := w.Write(buf)\n+\t\tif err != nil {\n+\t\t\tt.Errorf("handler Write error: %v", err)\n+\t\t\treturn\n+\t\t}\n+\t\tconn.SetWriteDeadline(time.Now().Add(-30 * time.Second))\n+\t\t_, err = w.Write(buf)\n+\t\tafterTimeoutErrc <- err\n+\t}))\n+\tts.Listener = trackLastConnListener{ts.Listener, &conn}\n+\tts.Start()\n+\tdefer ts.Close()\n+\n+\ttr := &Transport{DisableKeepAlives: false}\n+\tdefer tr.CloseIdleConnections()\n+\tc := &Client{Transport: tr}\n+\n+\terrc := make(chan error)\n+\tgo func() {\n+\t\tres, err := c.Get(ts.URL)\n+\t\tif err != nil {\n+\t\t\terrc <- err\n+\t\t\treturn\n+\t\t}\n+\t\t_, err = io.Copy(ioutil.Discard, res.Body)\n+\t\terrc <- err\n+\t}()\n+\tselect {\n+\tcase err := <-errc:\n+\t\tif err == nil {\n+\t\t\tt.Errorf("expected an error from Get request")\n+\t\t}\n+\tcase <-time.After(5 * time.Second):\n+\t\tt.Fatal("timeout waiting for Get error")\n+\t}\n+\tif err := <-afterTimeoutErrc; err == nil {\n+\t\tt.Error("expected write error after timeout")\n+\t}\n+}\n+\n+// trackLastConnListener tracks the last net.Conn that was accepted.\n+type trackLastConnListener struct {\n+\tnet.Listener\n+\tlast *net.Conn // destination\n+}\n+\n+func (l trackLastConnListener) Accept() (c net.Conn, err error) {\n+\tc, err = l.Listener.Accept()\n+\t*l.last = c\n+\treturn\n+}\n+\n // TestIdentityResponse verifies that a handler can unset\n func TestIdentityResponse(t *testing.T) {\n \thandler := HandlerFunc(func(rw ResponseWriter, req *Request) {\n```

## コアとなるコードの解説

`src/pkg/net/http/server.go` の変更は、`chunkWriter.Write` メソッド内で、書き込み操作がエラーを返した場合に、そのエラーをチェックし、即座に基盤となるネットワーク接続 (`cw.res.conn.rwc`) を閉じるようにしています。

1.  **最初の変更箇所 (行 223-226)**:
    ```go
    		_, err = fmt.Fprintf(cw.res.conn.buf, "%x\r\n", len(p))
    		if err != nil {
    			cw.res.conn.rwc.Close()
    			return
    		}
    ```
    これは、チャンクのサイズ(例: `512000\r\n`)をバッファに書き込む際にエラーが発生した場合の処理です。もしこの書き込みが失敗した場合、それは通常、基盤となる接続に問題があることを意味します。したがって、`cw.res.conn.rwc.Close()` を呼び出して接続を閉じ、それ以上の操作を停止します。

2.  **二番目の変更箇所 (行 230-234)**:
    ```go
    	if cw.chunking && err == nil {
    		_, err = cw.res.conn.buf.Write(crlf)
    	}
    	if err != nil {
    		cw.res.conn.rwc.Close()
    	}
    ```
    これは、チャンクデータ本体とそれに続くCRLFを書き込んだ後の処理です。`cw.res.conn.buf.Write(crlf)` の呼び出し後、またはそれ以前のチャンクデータ書き込みで `err` が `nil` でなかった場合(つまり、何らかの書き込みエラーが発生していた場合)、ここでも `cw.res.conn.rwc.Close()` を呼び出して接続を閉じます。

これらの変更により、`chunkWriter` は書き込みエラーを検知した瞬間に、その接続がもはや有効ではないと判断し、サーバーがその「死んだ」接続でブロッキングし続けることを防ぎます。これにより、サーバーのリソースが適切に解放され、全体的な堅牢性が向上します。

`src/pkg/net/http/serve_test.go` に追加された `TestOnlyWriteTimeout` テストは、この修正が正しく機能することを検証します。このテストは、意図的に `WriteTimeout` を発生させ、その後に `ResponseWriter.Write` がエラーを返すことを確認し、さらにサーバーがその接続でブロッキングしないことをアサートします。`trackLastConnListener` は、テスト中にサーバーが受け入れた最後の接続を追跡するために使用され、その接続に対して `SetWriteDeadline` を設定することで、書き込みタイムアウトをシミュレートしています。

## 関連リンク

*   **Go Issue #4741**: [https://code.google.com/p/go/issues/detail?id=4741](https://code.google.com/p/go/issues/detail?id=4741) (元の問題報告)
*   **Go Change List 7301043**: [https://golang.org/cl/7301043](https://golang.org/cl/7301043) (このコミットに対応するGoの変更リスト)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント: `net/http` パッケージ
*   Go言語の公式ドキュメント: `net` パッケージ
*   Go言語のIssue Tracker (上記Issue #4741)
*   Go言語のCode Reviewツール (上記Change List 7301043)