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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるサーバーのタイムアウト処理に関する修正です。具体的には、HTTPサーバーが接続の確立時ではなく、各リクエストの開始時にデッドライン(タイムアウト期限)を設定するように変更することで、サーバーのタイムアウト動作をより正確かつ意図通りに機能させることを目的としています。

コミット

commit 022504b3ab62a4d35aad13c58382bd0a7168805b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Feb 4 13:52:45 2013 -0800

    net/http: fix when server deadlines get extended
    
    Deadlines should be extended at the beginning of
    a request, not at the beginning of a connection.
    
    Fixes #4676
    
    R=golang-dev, fullung, patrick.allen.higgins, adg
    CC=golang-dev
    https://golang.org/cl/7220076

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

https://github.com/golang/go/commit/022504b3ab62a4d35aad13c58382bd0a7168805b

元コミット内容

net/http: fix when server deadlines get extended Deadlines should be extended at the beginning of a request, not at the beginning of a connection. Fixes #4676

変更の背景

Goの net/http パッケージにおけるHTTPサーバーは、ReadTimeoutWriteTimeout という設定を通じて、クライアントからの読み込みおよびクライアントへの書き込み操作に対するタイムアウトを制御します。このコミット以前は、これらのデッドラインがTCP接続が確立された直後に設定されていました。

この挙動には問題がありました。例えば、クライアントが接続を確立したものの、すぐにリクエストを送信しない場合、サーバーは接続確立時からタイムアウトのカウントを開始してしまいます。これにより、クライアントがリクエストを送信する前に、まだ有効であるべき接続がタイムアウトしてしまう可能性がありました。特に、Keep-Alive接続のように複数のリクエストが同じ接続上で処理される場合、最初の接続確立時に設定されたデッドラインが後続のリクエストに不適切に適用され、予期せぬタイムアウトを引き起こす可能性がありました。

このコミットは、この問題を解決し、タイムアウトが各HTTPリクエストの処理開始時に適切にリセットまたは延長されるようにすることで、より堅牢で予測可能なサーバー動作を実現することを目的としています。コミットメッセージにある Fixes #4676 は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。

前提知識の解説

  • HTTPサーバーのタイムアウト: HTTPサーバーは、クライアントからのリクエストの読み込みやレスポンスの書き込みに時間がかかりすぎた場合に、接続を切断するためのタイムアウト機構を持つことが一般的です。これにより、リソースの枯渇を防ぎ、悪意のあるクライアントや応答しないクライアントからサーバーを保護します。
    • ReadTimeout: クライアントからのリクエストヘッダやボディの読み込みにかかる最大時間。
    • WriteTimeout: クライアントへのレスポンスの書き込みにかかる最大時間。
  • TCP接続とHTTPリクエスト:
    • TCP接続: クライアントとサーバー間で確立される低レベルの通信チャネル。一度確立されると、複数のHTTPリクエスト/レスポンスのやり取りに再利用されることがあります(Keep-Alive)。
    • HTTPリクエスト: TCP接続上で送信されるアプリケーション層のメッセージ。クライアントがサーバーに何かを要求する際に使用されます。
  • net.Conn.SetReadDeadline() / SetWriteDeadline(): Goの net パッケージが提供する機能で、ネットワーク接続に対する読み込み/書き込み操作のデッドラインを設定します。指定された時刻までに操作が完了しない場合、エラーが発生します。
  • httptest.NewUnstartedServer: net/http/httptest パッケージで提供されるテストユーティリティ。HTTPサーバーを起動せずに設定し、テストコード内で明示的に起動・停止を制御できるため、サーバーのライフサイクルを細かくテストするのに非常に便利です。

技術的詳細

このコミットの核心は、net/http/server.go 内の (*conn).serve() メソッドと (*conn).readRequest() メソッドにおけるデッドライン設定ロジックの変更です。

以前は、(*Server).Serve() メソッド内で新しいTCP接続が受け入れられた直後、つまり srv.newConn(rw) が呼び出される前に rw.SetReadDeadline()rw.SetWriteDeadline() が呼び出されていました。これは、接続が確立された時点でタイムアウトが開始されることを意味します。

このコミットでは、このデッドライン設定が (*conn).readRequest() メソッドの冒頭に移動されました。readRequest() は、各HTTPリクエストのヘッダを読み込む直前に呼び出されるため、デッドラインがリクエストの開始時に設定されるようになります。

具体的には、以下の変更が行われました。

  1. (*Server).Serve() からデッドライン設定の削除: net.Listener から新しい接続 (rw) が受け入れられた直後に設定されていた rw.SetReadDeadline()rw.SetWriteDeadline() の呼び出しが削除されました。
  2. (*conn).readRequest() へのデッドライン設定の移動: (*conn).readRequest() メソッドの冒頭で、c.server.ReadTimeoutc.server.WriteTimeout の値に基づいて c.rwc.SetReadDeadline()c.rwc.SetWriteDeadline() が呼び出されるようになりました。これにより、各リクエストの読み込みが開始される直前に読み込みデッドラインが設定され、リクエストの処理が完了するまで書き込みデッドラインが有効になるように defer を使って設定されます。
  3. TLS接続のハンドシェイク時のデッドライン設定: (*conn).serve() メソッド内でTLS接続のハンドシェイクが行われる際にも、ReadTimeoutWriteTimeout が設定されるようになりました。これは、TLSハンドシェイク自体もタイムアウトの対象となるべき重要なフェーズであるためです。

これらの変更により、HTTPサーバーは、接続がアイドル状態であっても、実際にリクエストの読み込みや書き込みが行われるフェーズでのみタイムアウトを適用するようになります。これにより、Keep-Alive接続の効率が向上し、不必要なタイムアウトが減少します。

また、テストコード src/pkg/net/http/serve_test.go も大幅に改善されています。

  • 以前は手動で net.Listen を使ってサーバーを起動していましたが、httptest.NewUnstartedServer を使用するように変更されました。これにより、テストのセットアップが簡潔になり、サーバーのライフサイクル管理が容易になりました。
  • time.Since(t1) を使用して経過時間をより正確に測定するようになりました。
  • testing.Short() を使って、短時間テストと長時間テストの切り替えに対応するようになりました。これにより、CI/CD環境などでのテスト実行時間を最適化できます。
  • 複数のリクエストを送信し、タイムアウトが適切に機能しているか、そしてハンドラが意図せず実行されていないかを確認するテストロジックが追加・改善されました。

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

src/pkg/net/http/server.go

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -416,6 +416,16 @@ func (c *conn) readRequest() (w *response, err error) {
 	if c.hijacked() {
 		return nil, ErrHijacked
 	}
+
+	if d := c.server.ReadTimeout; d != 0 {
+		c.rwc.SetReadDeadline(time.Now().Add(d))
+	}
+	if d := c.server.WriteTimeout; d != 0 {
+		defer func() {
+			c.rwc.SetWriteDeadline(time.Now().Add(d))
+		}()
+	}
+
 	c.lr.N = int64(c.server.maxHeaderBytes()) + 4096 /* bufio slop */
 	var req *Request
 	if req, err = ReadRequest(c.buf.Reader); err != nil {
@@ -779,6 +789,12 @@ func (c *conn) serve() {
 	}()
 
 	if tlsConn, ok := c.rwc.(*tls.Conn); ok {
+		if d := c.server.ReadTimeout; d != 0 {
+			c.rwc.SetReadDeadline(time.Now().Add(d))
+		}
+		if d := c.server.WriteTimeout; d != 0 {
+			c.rwc.SetWriteDeadline(time.Now().Add(d))
+		}
 		if err := tlsConn.Handshake(); err != nil {
 			return
 		}
@@ -1274,12 +1290,6 @@ func (srv *Server) Serve(l net.Listener) error {
 			return e
 		}
 		tempDelay = 0
-		if srv.ReadTimeout != 0 {
-			rw.SetReadDeadline(time.Now().Add(srv.ReadTimeout))
-		}
-		if srv.WriteTimeout != 0 {
-			rw.SetWriteDeadline(time.Now().Add(srv.WriteTimeout))
-		}
 		c, err := srv.newConn(rw)
 		if err != nil {
 			continue

src/pkg/net/http/serve_test.go

テストコード全体が httptest.NewUnstartedServer を使用するようにリファクタリングされ、タイムアウトテストのロジックが改善されています。

コアとなるコードの解説

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

  • (*Server).Serve() からの削除: Serve メソッドは、net.Listener から新しいネットワーク接続を受け入れるループを担当します。以前はここで SetReadDeadlineSetWriteDeadline が呼び出されていましたが、これは接続が確立された時点(リクエストがまだ来ていない可能性のある時点)でタイムアウトが開始されることを意味していました。このコミットでは、このロジックが削除され、より適切なタイミングでデッドラインが設定されるようになりました。

  • (*conn).readRequest() への追加: readRequest メソッドは、クライアントからのHTTPリクエスト(ヘッダとボディ)を実際に読み込む処理を担当します。このメソッドの冒頭で c.rwc.SetReadDeadline(time.Now().Add(d)) が呼び出されることで、読み込みタイムアウトはリクエストの読み込みが開始される直前に設定されるようになります。これにより、接続がアイドル状態であっても、リクエストの読み込みが開始されるまではタイムアウトがカウントされません。 defer func() { c.rwc.SetWriteDeadline(time.Now().Add(d)) }() は、readRequest が終了する際に書き込みデッドラインを設定します。これは、リクエストの処理が完了し、レスポンスの書き込みが始まる準備ができた時点で書き込みタイムアウトが有効になるようにするためです。

  • TLSハンドシェイク時のデッドライン設定: (*conn).serve() メソッド内でTLS接続 (*tls.Conn) のハンドシェイクが行われるブロックにも、同様に SetReadDeadlineSetWriteDeadline が追加されました。TLSハンドシェイクは、HTTPリクエストの読み込みに先立って行われる重要なステップであり、このステップ自体もタイムアウトの対象となるべきです。これにより、TLSハンドシェイクが長時間応答しない場合にも適切にタイムアウトするようになります。

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

  • httptest.NewUnstartedServer の導入: テストサーバーのセットアップが httptest.NewUnstartedServer を使用するように変更されました。これにより、テストコード内でサーバーの起動 (ts.Start()) と停止 (ts.Close()) を明示的に制御できるようになり、テストの信頼性と再現性が向上します。以前の手動での net.Listengo server.Serve(l) の組み合わせよりも、テスト環境の管理が容易になります。

  • タイムアウトテストの改善: スロークライアントのシミュレーションにおいて、time.Since(t1) を使用して経過時間を測定することで、より正確なタイムアウト時間の検証が可能になりました。また、testing.Short() を利用して、開発中の高速なテスト実行と、CI/CDなどでのより網羅的なテスト実行を切り替えられるようになりました。これにより、テストの柔軟性が向上します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • Go言語のIssueトラッカー (Issue #4676) - ※Web検索では直接的な情報が見つかりませんでしたが、コミットメッセージに記載されているため、過去に存在したIssueであると推測されます。
  • Go言語のコードレビューシステム (https://golang.org/cl/7220076)