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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージにおける、クライアント側の接続(ファイルディスクリプタ)のクローズ処理を改善するものです。具体的には、HTTPレスポンスの読み込み中にエラー(特に予期せぬEOF)が発生した場合に、クライアントのファイルディスクリプタ(FD)をより早期に、かつ確実にクローズするように修正されています。これにより、リソースリークの可能性を減らし、特定のテストシナリオ(TestStressSurpriseServerCloses)での不安定性を解消します。

コミット

commit c0ecfb072b02d5764e387af560bfedb1cadcac1c
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Jan 31 09:45:13 2012 -0800

    net/http: close client fd sooner on response read error
    
    This fixes some test noise in TestStressSurpriseServerCloses when
    ulimit -n something low, like 256 on a Mac.
    
    Previously, when the server closed on us and we were expecting more
    responses (like we are in that test), we'd read an "Unexpected EOF"
    and just forget about the client's net.Conn.  Now it's closed,
    rather than waiting on the finalizer to release the fd.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5602043

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

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

元コミット内容

net/http: レスポンス読み込みエラー時にクライアントのファイルディスクリプタをより早くクローズする。

この変更は、Mac上で ulimit -n の値が低い(例: 256)場合に TestStressSurpriseServerCloses テストで発生するノイズ(テストの不安定性)を修正します。

以前は、サーバーが予期せず接続をクローズし、クライアントがさらなるレスポンスを期待している場合(上記のテストのような状況)、"Unexpected EOF" エラーを読み取り、クライアントの net.Conn を単に放置していました。この修正により、ファイナライザがファイルディスクリプタを解放するのを待つのではなく、直ちにクローズされるようになります。

変更の背景

このコミットの主な背景は、Goの net/http パッケージのクライアント側で発生していたリソースリークの可能性と、それによって引き起こされるテストの不安定性です。

具体的には、TestStressSurpriseServerCloses というテストシナリオにおいて問題が顕在化していました。このテストは、サーバーが予期せず接続をクローズする状況をシミュレートし、クライアントがその状況に適切に対応できるかを検証するものです。

問題の根源は、クライアントがサーバーからのレスポンスを読み込んでいる最中に、サーバー側が突然接続を切断した場合にありました。この際、クライアントは「Unexpected EOF」(予期せぬファイルの終端)エラーを受け取ります。従来のコードでは、このエラーが発生しても、クライアント側の net.Conn(ネットワーク接続を表すオブジェクト)に関連付けられたファイルディスクリプタ(FD)が即座にクローズされず、ガベージコレクタによるファイナライザが実行されるまで解放されない状態になっていました。

特に、ulimit -n コマンドで設定される「プロセスが同時に開くことができるファイルディスクリプタの最大数」が低い環境(例: Macで256など)では、このFDの解放遅延が深刻な問題となります。テストが多数の接続を短期間に開閉するようなストレスシナリオでは、FDがすぐに解放されないために利用可能なFDが枯渇し、新たな接続を開けなくなり、結果としてテストが失敗したり、不安定になったりする「ノイズ」が発生していました。

このコミットは、このFDリークの可能性を排除し、テストの信頼性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念について知っておく必要があります。

  1. ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、ファイルやソケットなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。ネットワーク接続(TCPソケットなど)もFDとして扱われます。プロセスが同時に開けるFDの数には上限があり、この上限は ulimit -n コマンドで確認・設定できます。FDが適切にクローズされないと、利用可能なFDが枯渇し、新たな接続やファイル操作ができなくなる「FDリーク」が発生します。

  2. ulimit -n: Unix系OSのシェルコマンドで、現在のユーザーが実行するプロセスが同時に開くことができるファイルディスクリプタの最大数を表示または設定します。この値が低いと、多数のネットワーク接続を扱うアプリケーション(HTTPクライアントなど)は、FDリークが発生した場合にすぐにリソース不足に陥ります。

  3. Go言語の net/http パッケージ: Go言語の標準ライブラリで、HTTPクライアントおよびサーバーの実装を提供します。このパッケージは、HTTPプロトコルに準拠した通信を容易に行うための高レベルなAPIを提供します。

  4. net.Conn インターフェース: Go言語の net パッケージで定義されているインターフェースで、汎用的なネットワーク接続を表します。TCP接続やUDP接続など、様々な種類のネットワーク接続がこのインターフェースを実装します。ReadWriteClose などのメソッドを持ちます。

  5. persistConn 構造体: net/http パッケージの内部で使われる構造体で、HTTPクライアントがサーバーとの間で確立する永続的な(Keep-Alive)TCP接続を管理します。この構造体は、基盤となる net.Conn を保持し、複数のHTTPリクエスト/レスポンスのやり取りで再利用されることがあります。readLoop メソッドは、この永続接続上でサーバーからのレスポンスを継続的に読み取るためのゴルーチンで実行されます。

  6. ReadResponse 関数: net/http パッケージの内部関数で、bufio.Reader からHTTPレスポンスを解析して *http.Response オブジェクトを生成します。この関数は、レスポンスのヘッダやボディを読み込む際に、ネットワークI/Oエラーが発生する可能性があります。

  7. "Unexpected EOF" エラー: ネットワークプログラミングにおいてよく遭遇するエラーの一つです。これは、データストリームの途中で予期せず接続が切断されたり、データの終端に達したりした場合に発生します。HTTP通信においては、クライアントがサーバーからのレスポンスを読み込んでいる最中にサーバーが接続をクローズした場合などに発生します。

  8. ファイナライザ (Finalizer): Go言語の runtime パッケージで提供される機能で、オブジェクトがガベージコレクタによってメモリから解放される直前に実行される関数を登録できます。このコミットの文脈では、net.Conn オブジェクトがガベージコレクションされる際に、関連するFDを解放するファイナライザが設定されていることを指します。しかし、ガベージコレクションのタイミングは不定であるため、FDの解放が遅れる可能性があります。

技術的詳細

このコミットは、net/http パッケージ内の transport.go ファイルにある persistConn 構造体の readLoop メソッドの挙動を変更します。

persistConn.readLoop() は、HTTPクライアントがサーバーとの間に確立した永続的なTCP接続(Keep-Alive接続)上で、サーバーからのHTTPレスポンスを継続的に読み取るためのゴルーチンです。このループは、新しいリクエストが来るたびに ReadResponse を呼び出し、レスポンスを解析します。

変更前のコードでは、ReadResponse がエラーを返した場合(特に "Unexpected EOF" のようなネットワークI/Oエラー)、そのエラーが nil でない限り、if err == nil の条件が偽となり、エラー処理ブロックに入りませんでした。この場合、persistConn オブジェクト自体はまだ参照されている可能性があり、その内部で保持している net.Conn もクローズされずに残ってしまいます。net.Conn に関連付けられたファイルディスクリプタは、最終的にはGoのランタイムが設定したファイナライザによって解放されますが、そのタイミングはガベージコレクタの実行に依存するため、即時ではありません。

この遅延が、ulimit -n が低い環境でのFD枯渇問題を引き起こしていました。特に、TestStressSurpriseServerCloses のように、サーバーが頻繁に接続をクローズするシナリオでは、クライアント側で大量のFDが「宙ぶらりん」の状態になり、利用可能なFDがすぐに上限に達してしまいます。

このコミットによる修正は、ReadResponse がエラーを返した場合に、即座に pc.close() メソッドを呼び出すように変更することで、この問題を解決します。pc.close()persistConn に関連付けられた基盤の net.Conn を明示的にクローズし、それによってファイルディスクリプタも即座にOSに返却されます。これにより、ファイナライザの実行を待つ必要がなくなり、FDリークの可能性が大幅に減少します。

この変更は、エラー発生時のリソース管理をより堅牢にし、特に高負荷な環境やリソース制限のある環境での net/http クライアントの安定性を向上させます。

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

--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -535,7 +535,9 @@ func (pc *persistConn) readLoop() {
 		}
 		resp, err := ReadResponse(pc.br, rc.req)
 
-		if err == nil {
+		if err != nil {
+			pc.close()
+		} else {
 			hasBody := rc.req.Method != "HEAD" && resp.ContentLength != 0
 			if rc.addedGzip && hasBody && resp.Header.Get("Content-Encoding") == "gzip" {
 				resp.Header.Del("Content-Encoding")

コアとなるコードの解説

変更は src/pkg/net/http/transport.go ファイルの persistConn.readLoop() メソッド内で行われています。

元のコード:

		resp, err := ReadResponse(pc.br, rc.req)

		if err == nil {
			// ... レスポンスが正常に読み込まれた場合の処理 ...
		}

このコードでは、ReadResponse 関数がHTTPレスポンスの読み込みを試み、エラーが発生しなかった場合(err == nil)にのみ、レスポンスの処理に進んでいました。エラーが発生した場合(err != nil)、if ブロックの内部には入らず、そのままループの次のイテレーションに進むか、あるいは readLoop を抜けることになります。この際、pc (persistConn) に関連付けられた net.Conn は明示的にクローズされず、ファイナライザによる解放を待つ状態でした。

変更後のコード:

		resp, err := ReadResponse(pc.br, rc.req)

		if err != nil {
			pc.close()
		} else {
			hasBody := rc.req.Method != "HEAD" && resp.ContentLength != 0
			if rc.addedGzip && hasBody && resp.Header.Get("Content-Encoding") == "gzip" {
				resp.Header.Del("Content-Encoding")
				// ... レスポンスが正常に読み込まれた場合の処理 ...
			}
		}

この修正では、if err == nil の条件が if err != nil に反転され、エラーが発生した場合の処理が追加されました。

  • if err != nil ブロック: ReadResponse がエラーを返した場合(例: "Unexpected EOF")、このブロックが実行されます。
    • pc.close(): ここで persistConn オブジェクトの close メソッドが明示的に呼び出されます。このメソッドは、persistConn が保持している基盤の net.Conn をクローズし、関連するファイルディスクリプタを即座にOSに返却します。これにより、FDリークが防止されます。
  • else ブロック: エラーが発生しなかった場合(err == nil)、以前の if err == nil ブロックの内容がこの else ブロックに移動され、正常なレスポンス処理が続行されます。

この変更により、HTTPレスポンスの読み込み中にエラーが発生した場合でも、ネットワーク接続が迅速かつ確実にクローズされるようになり、ファイルディスクリプタの枯渇を防ぎ、アプリケーションの堅牢性と安定性が向上します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Unix/Linuxの ulimit コマンドに関する一般的な情報源
  • ネットワークプログラミングにおける「EOF」エラーに関する一般的な情報源
  • Goの net/http パッケージのソースコード (特に transport.go)