[インデックス 19321] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/rpc
パッケージにおける重要なバグ修正と、それに関連するテストの追加を含んでいます。
変更されたファイルは以下の通りです。
src/pkg/net/rpc/client.go
:net/rpc
クライアントの実装が含まれる主要なファイルです。このファイルでは、Client
構造体の定義と、RPC呼び出しの確立および終了を管理するメソッドが定義されています。今回のコミットでは、Client.Close()
メソッドのロジックが修正されました。src/pkg/net/rpc/client_test.go
:net/rpc
クライアントの動作を検証するためのテストファイルです。このコミットで新規に追加されました。特に、ソケットのリーク問題が修正されたことを確認するためのテストケースが含まれています。
コミット
commit 82ca3087439399737f66395a568ba9f5642b295b
Author: David Crawshaw <david.crawshaw@zentus.com>
Date: Sun May 11 14:46:44 2014 -0700
net/rpc: do not leak client socket on closed connection
Fixes #6897.
LGTM=bradfitz
R=golang-codereviews, bradfitz, r, rsc
CC=golang-codereviews
https://golang.org/cl/91230045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/82ca3087439399737f66395a568ba9f5642b295b
元コミット内容
net/rpc: do not leak client socket on closed connection
Fixes #6897.
LGTM=bradfitz
R=golang-codereviews, bradfitz, r, rsc
CC=golang-codereviews
https://golang.org/cl/91230045
変更の背景
このコミットは、Goの net/rpc
パッケージにおける重要なバグ、具体的には Issue #6897: net/rpc: socket fd leak when server closes connection first を修正するために行われました。
この問題は、RPCクライアントとサーバー間の接続において、サーバー側が先に接続を閉じた場合に発生していました。通常、クライアントは自身の Close()
メソッドを呼び出して接続を終了させますが、サーバーが先に接続を終了させた場合、クライアントの Close()
メソッドはエラー (ErrShutdown
) を返してしまい、基盤となるソケットファイルディスクリプタ (FD) が適切に閉じられないという現象が発生していました。
ファイルディスクリプタのリークは、特に長期間稼働するアプリケーションや多数のRPC接続を扱うシステムにおいて深刻な問題を引き起こします。OSにはプロセスごとに開けるファイルディスクリプタの数に上限があるため、リークが続くと最終的にはFDが枯渇し、新たな接続の確立やファイル操作ができなくなり、アプリケーション全体のパフォーマンス低下やクラッシュに繋がる可能性があります。このコミットは、このようなリソース枯渇の問題を防ぎ、net/rpc
クライアントの堅牢性を向上させることを目的としています。
前提知識の解説
Goの net/rpc
パッケージ
net/rpc
パッケージは、Go言語でRPC(Remote Procedure Call)を実装するための標準ライブラリです。RPCは、異なるアドレス空間にあるプログラムが、あたかもローカルな手続きであるかのように互いに通信できるようにする技術です。net/rpc
は、クライアントとサーバー間の通信を抽象化し、メソッド呼び出しをネットワーク経由で透過的に行えるようにします。
RPCクライアント (net/rpc.Client
)
net/rpc.Client
は、RPCサーバーに対してリモートメソッドを呼び出すためのクライアントサイドのエンティティです。クライアントはサーバーへのネットワーク接続を管理し、リクエストの送信、レスポンスの受信、エラーハンドリングなどを行います。Client
は複数のゴルーチンから同時に使用することができ、並行処理に対応しています。
ClientCodec
インターフェース
ClientCodec
インターフェースは、RPCメッセージのエンコード(リクエストのシリアライズ)とデコード(レスポンスのデシリアライズ)を担当します。net/rpc
パッケージは、デフォルトでGoの encoding/gob
パッケージを使用したコーデックを提供しますが、ClientCodec
インターフェースを実装することで、JSONやProtobufなど、任意のデータフォーマットをRPC通信に使用することができます。このインターフェースは、RPC通信のプロトコル層を抽象化し、柔軟性を提供します。
ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系OSにおいて、開かれたファイルやソケット、パイプなどのI/Oリソースを識別するためにカーネルがプロセスに割り当てる非負の整数です。プログラムがファイルを開いたり、ネットワーク接続を確立したりするたびに、新しいFDが割り当てられます。これらのFDは、リソースが不要になったときに明示的に閉じる必要があります。閉じ忘れると、FDがシステム内で消費され続け、利用可能なFDの数が減少し、最終的には新たなリソースを確保できなくなる「FDリーク」が発生します。
sync.Mutex
sync.Mutex
は、Go言語の標準ライブラリ sync
パッケージで提供される相互排他ロック(ミューテックス)です。複数のゴルーチンが共有リソースに同時にアクセスする際に、データ競合を防ぐために使用されます。Lock()
メソッドでロックを取得し、Unlock()
メソッドでロックを解放します。これにより、一度に一つのゴルーチンだけが保護されたコードセクション(クリティカルセクション)を実行できるようになり、共有データの整合性が保たれます。
技術的詳細
このバグの核心は、net/rpc
クライアントの Close()
メソッド内の条件分岐にありました。
Client
構造体には、接続の状態を示す2つのブール型フィールドがあります。
closing
: クライアント自身がClose()
メソッドを呼び出して、能動的に接続を閉じようとしている状態を示します。shutdown
: サーバー側から接続がシャットダウンされたことをクライアントが認識した状態を示します。これは、サーバーがエラーを返したり、接続を閉じたりした場合に設定されます。
問題となっていたのは、Client.Close()
メソッドの冒頭にある以下の条件式でした。
if client.shutdown || client.closing {
client.mutex.Unlock()
return ErrShutdown
}
このロジックでは、もし client.shutdown
が true
であった場合(つまり、サーバーが既に接続をシャットダウンしているとクライアントが認識している場合)、クライアントは自身の Close()
呼び出しを「既にシャットダウン済み」と判断し、すぐに ErrShutdown
を返してしまっていました。
しかし、この「シャットダウン済み」という判断は、必ずしも基盤となるソケットが閉じられていることを意味しませんでした。サーバーが接続を閉じたとしても、クライアント側のソケットはまだ開いたままになっている可能性がありました。この場合、クライアントが Close()
を呼び出しても、上記の条件式によってソケットを閉じる処理がスキップされてしまい、結果としてソケットのファイルディスクリプタがリークしていたのです。
このコミットによる修正は、この条件式から client.shutdown
のチェックを削除することで、この問題を解決しています。新しい条件式は以下のようになります。
if client.closing {
client.mutex.Unlock()
return ErrShutdown
}
これにより、client.shutdown
の状態に関わらず、クライアントが能動的に Close()
を呼び出していない限り(client.closing
が false
である限り)、Close()
メソッドはソケットを閉じるための後続の処理を実行するようになります。たとえサーバーが先に接続を閉じていたとしても、クライアントが Close()
を呼び出せば、そのソケットは適切に閉じられるようになりました。
この変更は、Client
構造体のフィールドの順序も変更し、mutex
が保護する対象をコメントで明確にしています。これはコードの可読性と保守性を向上させるためのものです。
また、この修正の正しさを検証するために、client_test.go
という新しいテストファイルが追加されました。このテストは、カスタムの ClientCodec
を使用して、サーバーからのシャットダウンを模倣し、その後にクライアントの Close()
メソッドが基盤となるコーデックの Close()
メソッドを適切に呼び出すことを確認します。これにより、ソケットが確実に閉じられるようになったことがテストによって保証されます。
コアとなるコードの変更箇所
src/pkg/net/rpc/client.go
--- a/src/pkg/net/rpc/client.go
+++ b/src/pkg/net/rpc/client.go
@@ -39,14 +39,16 @@ type Call struct {
// with a single Client, and a Client may be used by
// multiple goroutines simultaneously.
type Client struct {
- mutex sync.Mutex // protects pending, seq, request
- sending sync.Mutex
+ codec ClientCodec
+
+ sending sync.Mutex
+
+ mutex sync.Mutex // protects following
request Request
seq uint64
- codec ClientCodec
tpending map[uint64]*Call
- closing bool
- shutdown bool
+ closing bool // user has called Close
+ shutdown bool // server has told us to stop
}
// A ClientCodec implements writing of RPC requests and
@@ -274,7 +276,7 @@ func Dial(network, address string) (*Client, error) {
func (client *Client) Close() error {
client.mutex.Lock()
- if client.shutdown || client.closing {
+ if client.closing {
client.mutex.Unlock()
return ErrShutdown
}
src/pkg/net/rpc/client_test.go
(新規追加)
--- /dev/null
+++ b/src/pkg/net/rpc/client_test.go
@@ -0,0 +1,36 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package rpc
+
+import (
+ "errors"
+ "testing"
+)
+
+type shutdownCodec struct {
+ responded chan int
+ closed bool
+}
+
+func (c *shutdownCodec) WriteRequest(*Request, interface{}) error { return nil }
+func (c *shutdownCodec) ReadResponseBody(interface{}) error { return nil }
+func (c *shutdownCodec) ReadResponseHeader(*Response) error {
+ c.responded <- 1
+ return errors.New("shutdownCodec ReadResponseHeader")
+}
+func (c *shutdownCodec) Close() error {
+ c.closed = true
+ return nil
+}
+
+func TestCloseCodec(t *testing.T) {
+ codec := &shutdownCodec{responded: make(chan int)}
+ client := NewClientWithCodec(codec)
+ <-codec.responded
+ client.Close()
+ if !codec.closed {
+ t.Error("client.Close did not close codec")
+ }
+}
コアとなるコードの解説
src/pkg/net/rpc/client.go
の変更
-
Client
構造体のフィールド順序とコメントの変更:- 変更前:
type Client struct { mutex sync.Mutex // protects pending, seq, request sending sync.Mutex request Request seq uint64 codec ClientCodec pending map[uint64]*Call closing bool shutdown bool }
- 変更後:
type Client struct { codec ClientCodec sending sync.Mutex mutex sync.Mutex // protects following request Request seq uint64 pending map[uint64]*Call closing bool // user has called Close shutdown bool // server has told us to stop }
- 解説:
codec
フィールドが構造体の先頭に移動し、mutex
のコメントが// protects following
と変更されました。これは、mutex
がその後に続くフィールド(request
,seq
,pending
,closing
,shutdown
)を保護することを示しています。また、closing
とshutdown
フィールドには、それぞれの役割を明確にするためのコメントが追加されました。closing
はユーザーがClose()
を呼び出した状態、shutdown
はサーバーが停止を指示した状態を指します。これにより、コードの意図がより明確になりました。
- 変更前:
-
Client.Close()
メソッドの条件式変更:- 変更前:
func (client *Client) Close() error { client.mutex.Lock() if client.shutdown || client.closing { client.mutex.Unlock() return ErrShutdown } // ... 後続のクローズ処理 ... }
- 変更後:
func (client *Client) Close() error { client.mutex.Lock() if client.closing { client.mutex.Unlock() return ErrShutdown } // ... 後続のクローズ処理 ... }
- 解説:
if
文の条件式からclient.shutdown ||
の部分が削除されました。これにより、クライアントがClose()
メソッドを呼び出した際に、たとえサーバーが既にシャットダウンを通知していたとしても(client.shutdown
がtrue
であっても)、client.closing
がfalse
であれば、クライアントはソケットを閉じるための後続の処理に進むようになります。この修正が、サーバー起因のシャットダウン時に発生していたソケットFDリークを防止する直接的な解決策です。クライアントが能動的にClose()
を呼び出すことで、基盤となるネットワーク接続(ソケット)が確実に閉じられるようになりました。
- 変更前:
src/pkg/net/rpc/client_test.go
の新規追加
このファイルは、上記の修正が正しく機能することを検証するための単体テストです。
-
shutdownCodec
構造体:type shutdownCodec struct { responded chan int closed bool }
- 解説:
ClientCodec
インターフェースをモックするためのカスタム構造体です。responded chan int
:ReadResponseHeader
が呼び出されたことを通知するためのチャネルです。テスト内でゴルーチン間の同期に使用されます。closed bool
:Close()
メソッドが呼び出されたかどうかを追跡するためのフラグです。
- 解説:
-
shutdownCodec
のメソッド実装:WriteRequest
,ReadResponseBody
: テストの目的とは直接関係ないため、ダミーの実装 (return nil
) が提供されています。ReadResponseHeader
:func (c *shutdownCodec) ReadResponseHeader(*Response) error { c.responded <- 1 return errors.New("shutdownCodec ReadResponseHeader") }
- 解説: このメソッドは、サーバーからのレスポンスヘッダを読み込む際に呼び出されます。テストでは、このメソッドが呼び出されたことを
c.responded
チャネルに送信することで通知し、その後意図的にエラーを返します。このエラーは、サーバーが接続をシャットダウンした状況をシミュレートするために使用されます。net/rpc
クライアントは、このエラーを受け取るとclient.shutdown
をtrue
に設定します。
- 解説: このメソッドは、サーバーからのレスポンスヘッダを読み込む際に呼び出されます。テストでは、このメソッドが呼び出されたことを
Close()
:func (c *shutdownCodec) Close() error { c.closed = true return nil }
- 解説: このメソッドは、クライアントの
Close()
メソッドが呼び出された際に、基盤となるコーデックを閉じるために呼び出されます。c.closed
フラグをtrue
に設定することで、このメソッドが呼び出されたことをテストで確認できるようにします。
- 解説: このメソッドは、クライアントの
-
TestCloseCodec
テスト関数:func TestCloseCodec(t *testing.T) { codec := &shutdownCodec{responded: make(chan int)} client := NewClientWithCodec(codec) <-codec.responded client.Close() if !codec.closed { t.Error("client.Close did not close codec") } }
- 解説:
codec := &shutdownCodec{responded: make(chan int)}
:shutdownCodec
のインスタンスを作成します。client := NewClientWithCodec(codec)
: カスタムのshutdownCodec
を使用して新しいRPCクライアントを作成します。この際、クライアントの内部でReadResponseHeader
が呼び出され、shutdownCodec
がエラーを返します。これにより、クライアントのshutdown
フラグがtrue
に設定される状況が作られます。<-codec.responded
:shutdownCodec
のReadResponseHeader
メソッドが呼び出され、チャネルに値が送信されるのを待ちます。これにより、クライアントがサーバーからのシャットダウンを認識した状態になったことを確認します。client.Close()
: クライアントのClose()
メソッドを呼び出します。修正前であれば、この呼び出しはErrShutdown
を返し、codec.Close()
は呼び出されませんでした。if !codec.closed { t.Error("client.Close did not close codec") }
:client.Close()
の呼び出し後、shutdownCodec
のclosed
フラグがtrue
になっていることを確認します。もしtrue
になっていなければ、client.Close()
が基盤となるコーデック(およびソケット)を適切に閉じていないことを意味し、テストは失敗します。
- 解説:
このテストは、サーバーが先に接続を閉じた場合でも、クライアントが自身の Close()
メソッドを呼び出すことで、基盤となるソケットが確実に閉じられるようになったことを明確に検証しています。
関連リンク
- Go CL (Change List): https://golang.org/cl/91230045
参考にした情報源リンク
- GitHub Issue: net/rpc: socket fd leak when server closes connection first #6897
- Go
net/rpc
パッケージドキュメント: https://pkg.go.dev/net/rpc (一般的な参照)