[インデックス 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リークの可能性を排除し、テストの信頼性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念について知っておく必要があります。
-
ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、ファイルやソケットなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。ネットワーク接続(TCPソケットなど)もFDとして扱われます。プロセスが同時に開けるFDの数には上限があり、この上限は
ulimit -n
コマンドで確認・設定できます。FDが適切にクローズされないと、利用可能なFDが枯渇し、新たな接続やファイル操作ができなくなる「FDリーク」が発生します。 -
ulimit -n
: Unix系OSのシェルコマンドで、現在のユーザーが実行するプロセスが同時に開くことができるファイルディスクリプタの最大数を表示または設定します。この値が低いと、多数のネットワーク接続を扱うアプリケーション(HTTPクライアントなど)は、FDリークが発生した場合にすぐにリソース不足に陥ります。 -
Go言語の
net/http
パッケージ: Go言語の標準ライブラリで、HTTPクライアントおよびサーバーの実装を提供します。このパッケージは、HTTPプロトコルに準拠した通信を容易に行うための高レベルなAPIを提供します。 -
net.Conn
インターフェース: Go言語のnet
パッケージで定義されているインターフェースで、汎用的なネットワーク接続を表します。TCP接続やUDP接続など、様々な種類のネットワーク接続がこのインターフェースを実装します。Read
、Write
、Close
などのメソッドを持ちます。 -
persistConn
構造体:net/http
パッケージの内部で使われる構造体で、HTTPクライアントがサーバーとの間で確立する永続的な(Keep-Alive)TCP接続を管理します。この構造体は、基盤となるnet.Conn
を保持し、複数のHTTPリクエスト/レスポンスのやり取りで再利用されることがあります。readLoop
メソッドは、この永続接続上でサーバーからのレスポンスを継続的に読み取るためのゴルーチンで実行されます。 -
ReadResponse
関数:net/http
パッケージの内部関数で、bufio.Reader
からHTTPレスポンスを解析して*http.Response
オブジェクトを生成します。この関数は、レスポンスのヘッダやボディを読み込む際に、ネットワークI/Oエラーが発生する可能性があります。 -
"Unexpected EOF" エラー: ネットワークプログラミングにおいてよく遭遇するエラーの一つです。これは、データストリームの途中で予期せず接続が切断されたり、データの終端に達したりした場合に発生します。HTTP通信においては、クライアントがサーバーからのレスポンスを読み込んでいる最中にサーバーが接続をクローズした場合などに発生します。
-
ファイナライザ (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
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go
net
パッケージのドキュメント: https://pkg.go.dev/net - Goのファイルディスクリプタに関する議論(一般的な情報): https://go.dev/doc/articles/go_and_unix_system_calls (直接的な言及ではないが、GoとUnixシステムコールの関係性を示す)
ulimit
コマンドに関する情報(OSのドキュメントなど)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Unix/Linuxの
ulimit
コマンドに関する一般的な情報源 - ネットワークプログラミングにおける「EOF」エラーに関する一般的な情報源
- Goの
net/http
パッケージのソースコード (特にtransport.go
)