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

[インデックス 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.shutdowntrue であった場合(つまり、サーバーが既に接続をシャットダウンしているとクライアントが認識している場合)、クライアントは自身の Close() 呼び出しを「既にシャットダウン済み」と判断し、すぐに ErrShutdown を返してしまっていました。

しかし、この「シャットダウン済み」という判断は、必ずしも基盤となるソケットが閉じられていることを意味しませんでした。サーバーが接続を閉じたとしても、クライアント側のソケットはまだ開いたままになっている可能性がありました。この場合、クライアントが Close() を呼び出しても、上記の条件式によってソケットを閉じる処理がスキップされてしまい、結果としてソケットのファイルディスクリプタがリークしていたのです。

このコミットによる修正は、この条件式から client.shutdown のチェックを削除することで、この問題を解決しています。新しい条件式は以下のようになります。

if client.closing {
    client.mutex.Unlock()
    return ErrShutdown
}

これにより、client.shutdown の状態に関わらず、クライアントが能動的に Close() を呼び出していない限り(client.closingfalse である限り)、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 の変更

  1. 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)を保護することを示しています。また、closingshutdown フィールドには、それぞれの役割を明確にするためのコメントが追加されました。closing はユーザーが Close() を呼び出した状態、shutdown はサーバーが停止を指示した状態を指します。これにより、コードの意図がより明確になりました。
  2. 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.shutdowntrue であっても)、client.closingfalse であれば、クライアントはソケットを閉じるための後続の処理に進むようになります。この修正が、サーバー起因のシャットダウン時に発生していたソケットFDリークを防止する直接的な解決策です。クライアントが能動的に Close() を呼び出すことで、基盤となるネットワーク接続(ソケット)が確実に閉じられるようになりました。

src/pkg/net/rpc/client_test.go の新規追加

このファイルは、上記の修正が正しく機能することを検証するための単体テストです。

  1. shutdownCodec 構造体:

    type shutdownCodec struct {
        responded chan int
        closed    bool
    }
    
    • 解説: ClientCodec インターフェースをモックするためのカスタム構造体です。
      • responded chan int: ReadResponseHeader が呼び出されたことを通知するためのチャネルです。テスト内でゴルーチン間の同期に使用されます。
      • closed bool: Close() メソッドが呼び出されたかどうかを追跡するためのフラグです。
  2. 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.shutdowntrue に設定します。
    • Close():
      func (c *shutdownCodec) Close() error {
          c.closed = true
          return nil
      }
      
      • 解説: このメソッドは、クライアントの Close() メソッドが呼び出された際に、基盤となるコーデックを閉じるために呼び出されます。c.closed フラグを true に設定することで、このメソッドが呼び出されたことをテストで確認できるようにします。
  3. 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")
        }
    }
    
    • 解説:
      1. codec := &shutdownCodec{responded: make(chan int)}: shutdownCodec のインスタンスを作成します。
      2. client := NewClientWithCodec(codec): カスタムの shutdownCodec を使用して新しいRPCクライアントを作成します。この際、クライアントの内部で ReadResponseHeader が呼び出され、shutdownCodec がエラーを返します。これにより、クライアントの shutdown フラグが true に設定される状況が作られます。
      3. <-codec.responded: shutdownCodecReadResponseHeader メソッドが呼び出され、チャネルに値が送信されるのを待ちます。これにより、クライアントがサーバーからのシャットダウンを認識した状態になったことを確認します。
      4. client.Close(): クライアントの Close() メソッドを呼び出します。修正前であれば、この呼び出しは ErrShutdown を返し、codec.Close() は呼び出されませんでした。
      5. if !codec.closed { t.Error("client.Close did not close codec") }: client.Close() の呼び出し後、shutdownCodecclosed フラグが true になっていることを確認します。もし true になっていなければ、client.Close() が基盤となるコーデック(およびソケット)を適切に閉じていないことを意味し、テストは失敗します。

このテストは、サーバーが先に接続を閉じた場合でも、クライアントが自身の Close() メソッドを呼び出すことで、基盤となるソケットが確実に閉じられるようになったことを明確に検証しています。

関連リンク

参考にした情報源リンク