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

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

このコミットは、Go言語の標準ライブラリであるnetパッケージにおいて、TCP接続確立時に発生するEADDRNOTAVAILエラー(アドレスが利用不可)が誤って報告される(spurious)問題と、自己接続(self-connect)の問題を修正するものです。特に、DialTCP関数がローカルアドレスを自動選択する際に、カーネルが既に利用中のアドレスを選択してしまうことによって発生するEADDRNOTAVAILエラーの再試行ロジックを追加しています。

コミット

  • Author: Russ Cox rsc@golang.org
  • Date: Mon Aug 6 16:32:00 2012 -0400
  • Commit Message:
    net: fix spurious EADDRNOTAVAIL errors
    
    R=golang-dev, fullung
    CC=golang-dev
    https://golang.org/cl/6443085
    

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

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

元コミット内容

net: fix spurious EADDRNOTAVAIL errors

R=golang-dev, fullung
CC=golang-dev
https://golang.org/cl/6443085

変更の背景

この変更の背景には、GoのnetパッケージにおけるTCP接続の確立、特にDialTCP関数が関わっています。DialTCPは、指定されたリモートアドレス(raddr)へのTCP接続を確立しようとしますが、ローカルアドレス(laddr)が指定されていない場合(laddr == nil)、オペレーティングシステム(OS)が適切なローカルアドレスとポートを自動的に選択します。

しかし、このOSによる自動選択のプロセスには、2つの既知の問題がありました。

  1. 自己接続(Self-Connect)問題:

    • これは、DialTCPが接続を試みた結果、なぜか自分自身(ローカルホスト)に接続してしまうという稀なケースです。これは通常、OSが一時的なポートを割り当てる際に、そのポートが既に利用中であるか、または何らかの競合状態によって発生します。
    • コミットメッセージ内のコメントで参照されているhttp://golang.org/issue/2690http://stackoverflow.com/questions/4949858/は、この自己接続問題に関する議論や解決策を示唆しています。以前のコードでは、selfConnect(fd)trueを返した場合に再試行するロジックが既に存在していました。
  2. 誤ったEADDRNOTAVAILエラー(Spurious EADDRNOTAVAIL errors):

    • EADDRNOTAVAILは、通常「要求されたアドレスが利用できません」という意味のエラーです。これは、例えば存在しないIPアドレスにバインドしようとしたり、ネットワークインターフェースがダウンしている場合などに発生します。
    • しかし、このコミットが修正しようとしているのは「spurious」(誤った、偽の)EADDRNOTAVAILエラーです。これは、OSがローカルアドレスを自動選択する際に、既に利用中のアドレスを誤って選択してしまい、その結果としてEADDRNOTAVAILエラーを返すという、OSカーネルの挙動に起因する問題です。本来利用可能なはずのアドレスが、一時的に利用不可と判断されてしまうため、「誤った」エラーと表現されています。
    • コミットメッセージのコメントには「The kernel socket code is no doubt enjoying watching us squirm.」(カーネルのソケットコードは、私たちがもがくのを楽しんでいるに違いない)とあり、この問題がOSカーネルの複雑な挙動に起因し、デバッグが困難であったことを示唆しています。

これらの問題により、DialTCPの呼び出しが正当な理由なく失敗し、アプリケーションの信頼性や可用性に影響を与えていました。このコミットは、これらの「誤った」失敗を検出し、自動的に再試行することで、より堅牢な接続確立を実現することを目的としています。

前提知識の解説

TCP/IPソケットプログラミングの基本

  • ソケット (Socket): ネットワーク通信のエンドポイント。IPアドレスとポート番号の組み合わせで識別されます。
  • バインド (Bind): ソケットを特定のローカルIPアドレスとポート番号に関連付ける操作。サーバー側でよく使われますが、クライアント側でも特定のローカルアドレスから接続を開始したい場合に明示的にバインドすることがあります。
  • 接続 (Connect): クライアントソケットをリモートサーバーのソケットに接続する操作。これにより、データ送受信が可能になります。
  • 一時ポート (Ephemeral Port): クライアントが接続を開始する際に、OSが自動的に割り当てる一時的なポート番号。通常、1024以上の範囲から選択されます。

Go言語のnetパッケージ

  • net.DialTCP: TCPネットワーク接続を確立するための関数。laddr(ローカルアドレス)とraddr(リモートアドレス)を指定します。laddrnilの場合、OSがローカルアドレスとポートを自動選択します。
  • netFD: Goのnetパッケージ内部でソケットディスクリプタ(ファイルディスクリプタ)をラップする構造体。OSのシステムコールとGoのネットワーク操作を橋渡しします。
  • net.OpError: netパッケージで発生するネットワーク操作のエラーを表す構造体。Errフィールドに基となるOSのエラー(syscall.Errnoなど)が含まれます。

システムコールとエラーコード

  • syscallパッケージ: Go言語からOSのシステムコールを直接呼び出すためのパッケージ。
  • syscall.EADDRNOTAVAIL: POSIXシステム(Linux, macOSなど)で定義されているエラーコードの一つ。Address not availableを意味し、要求されたアドレスが利用できない場合に返されます。これは、例えば、存在しないIPアドレスにバインドしようとしたり、ネットワークインターフェースがダウンしている場合などに発生します。しかし、本件ではOSが一時ポートを割り当てる際に、誤って既に利用中のポートを選択してしまい、このエラーが返されるという特殊なケースが問題となっています。

自己接続 (Self-Connect)

  • クライアントがリモートサーバーに接続しようとした際に、誤って自分自身(ローカルホスト)に接続してしまう現象。これは、特にローカルアドレスをOSに自動選択させる場合に、OSが割り当てた一時ポートが、たまたまローカルでリッスンしているポートと一致してしまったり、ルーティングの誤りなどによって発生することがあります。

技術的詳細

このコミットは、src/pkg/net/tcpsock_posix.goファイル内のDialTCP関数における接続再試行ロジックを強化しています。

元のコードでは、DialTCPがローカルアドレスを自動選択する際に、selfConnect(fd)trueを返した場合(自己接続が発生した場合)に最大2回再試行するロジックがありました。これは、laddr == nil(ローカルアドレスが指定されていない)かつerr == nil(初回接続試行でエラーがない)という条件で実行されていました。

このコミットでは、再試行の条件が拡張されています。

  1. 再試行条件の拡張:

    • 元のselfConnect(fd)に加えて、新たにspuriousENOTAVAIL(err)という条件が追加されました。
    • これにより、接続試行後にEADDRNOTAVAILエラーが発生した場合も再試行の対象となります。
    • 再試行のループ条件は for i := 0; i < 2 && (laddr == nil || laddr.Port == 0) && (selfConnect(fd, err) || spuriousENOTAVAIL(err)); i++ となりました。
      • laddr == nil || laddr.Port == 0: ローカルアドレスが指定されていないか、ポートが0(OSに自動選択させる)の場合に再試行の対象とします。これは、OSがアドレスを自動選択する際に問題が発生する可能性が高いためです。
      • selfConnect(fd, err): 自己接続が発生した場合。
      • spuriousENOTAVAIL(err): 誤ったEADDRNOTAVAILエラーが発生した場合。
  2. selfConnect関数の変更:

    • selfConnect関数がerr引数を受け取るようになりました。
    • if err != nil { return false }というチェックが追加され、接続自体が失敗している場合は自己接続ではないと判断するようになりました。これは、エラーが発生しているにもかかわらず自己接続をチェックする無駄を省き、ロジックをより正確にするためです。
  3. spuriousENOTAVAIL関数の追加:

    • 新しくspuriousENOTAVAIL関数が追加されました。
    • この関数は、引数として渡されたerrornet.OpError型であり、かつその内部のErrsyscall.EADDRNOTAVAILである場合にtrueを返します。これにより、特定のタイプのエラー(EADDRNOTAVAIL)を正確に識別し、再試行のトリガーとすることができます。

これらの変更により、DialTCPは、自己接続やOSによるローカルアドレスの誤った選択によって発生する一時的なEADDRNOTAVAILエラーに対して、より堅牢に再試行を行うことができるようになり、接続の成功率が向上します。

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

src/pkg/net/tcpsock_posix.goファイルのDialTCP関数と、新たに追加されたヘルパー関数です。

--- a/src/pkg/net/tcpsock_posix.go
+++ b/src/pkg/net/tcpsock_posix.go
@@ -166,8 +166,17 @@ func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error) {
 	// use the result.  See also:
 	//	http://golang.org/issue/2690
 	//	http://stackoverflow.com/questions/4949858/
-	for i := 0; i < 2 && err == nil && laddr == nil && selfConnect(fd); i++ {
-		fd.Close()
+	//
+	// The opposite can also happen: if we ask the kernel to pick an appropriate
+	// originating local address, sometimes it picks one that is already in use.
+	// So if the error is EADDRNOTAVAIL, we have to try again too, just for
+	// a different reason.
+	//
+	// The kernel socket code is no doubt enjoying watching us squirm.
+	for i := 0; i < 2 && (laddr == nil || laddr.Port == 0) && (selfConnect(fd, err) || spuriousENOTAVAIL(err)); i++ {
+		if err == nil {
+			fd.Close()
+		}
 	fd, err = internetSocket(net, laddr.toAddr(), raddr.toAddr(), syscall.SOCK_STREAM, 0, "dial", sockaddrToTCP)
 	}
 
@@ -177,7 +186,12 @@ func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error) {
 	return newTCPConn(fd), nil
 }
 
-func selfConnect(fd *netFD) bool {
+func selfConnect(fd *netFD, err error) bool {
+	// If the connect failed, we clearly didn't connect to ourselves.
+	if err != nil {
+		return false
+	}
+
 	// The socket constructor can return an fd with raddr nil under certain
 	// unknown conditions. The errors in the calls there to Getpeername
 	// are discarded, but we can't catch the problem there because those
@@ -194,6 +208,11 @@ func selfConnect(fd *netFD) bool {\n 	return l.Port == r.Port && l.IP.Equal(r.IP)\n }\n \n+func spuriousENOTAVAIL(err error) bool {\n+\te, ok := err.(*OpError)\n+\treturn ok && e.Err == syscall.EADDRNOTAVAIL\n+}\n+\n // TCPListener is a TCP network listener.\n // Clients should typically use variables of type Listener\n // instead of assuming TCP.\n```

## コアとなるコードの解説

### `DialTCP`関数の変更点

変更の中心は、`DialTCP`関数内の`for`ループの条件式です。

**変更前**:
```go
for i := 0; i < 2 && err == nil && laddr == nil && selfConnect(fd); i++ {
    fd.Close()
    fd, err = internetSocket(net, laddr.toAddr(), raddr.toAddr(), syscall.SOCK_STREAM, 0, "dial", sockaddrToTCP)
}

このループは、自己接続(selfConnect(fd))が発生し、かつ初回接続でエラーがない(err == nil)、ローカルアドレスが指定されていない(laddr == nil)場合に、最大2回再試行していました。

変更後:

for i := 0; i < 2 && (laddr == nil || laddr.Port == 0) && (selfConnect(fd, err) || spuriousENOTAVAIL(err)); i++ {
    if err == nil {
        fd.Close()
    }
    fd, err = internetSocket(net, laddr.toAddr(), raddr.toAddr(), syscall.SOCK_STREAM, 0, "dial", sockaddrToTCP)
}
  1. 再試行条件の拡張:
    • laddr == nil || laddr.Port == 0: ローカルアドレスが指定されていないか、ポートが0(OSに自動選択させる)の場合に再試行の対象とします。これは、OSがアドレスを自動選択する際に問題が発生する可能性が高いためです。
    • selfConnect(fd, err) || spuriousENOTAVAIL(err): 自己接続が発生した場合、または誤ったEADDRNOTAVAILエラーが発生した場合に再試行します。||(論理OR)演算子により、どちらかの条件が満たされれば再試行がトリガーされます。
  2. fd.Close()の条件付き実行:
    • if err == nil { fd.Close() }が追加されました。これは、前回の接続試行でエラーが発生していない場合にのみ、既存のファイルディスクリプタ(fd)を閉じるようにします。エラーが発生している場合は、fdが有効でない可能性があるため、Close()を呼び出す必要がないか、あるいは呼び出すと別のエラーを引き起こす可能性があるためです。

selfConnect関数の変更点

func selfConnect(fd *netFD, err error) bool {
	// If the connect failed, we clearly didn't connect to ourselves.
	if err != nil {
		return false
	}
	// ... 既存の自己接続チェックロジック ...
}

selfConnect関数がerr引数を受け取るようになり、関数冒頭でif err != nil { return false }というチェックが追加されました。これにより、接続自体が失敗している(エラーが発生している)場合は、自己接続ではないと即座に判断し、無駄なチェックを避けることができます。

spuriousENOTAVAIL関数の追加

func spuriousENOTAVAIL(err error) bool {
	e, ok := err.(*OpError)
	return ok && e.Err == syscall.EADDRNOTAVAIL
}

この新しいヘルパー関数は、渡されたerrnet.OpError型であり、かつその内部のErrフィールドがsyscall.EADDRNOTAVAILと等しい場合にtrueを返します。これにより、DialTCP関数は、OSが誤ってEADDRNOTAVAILを返した場合を正確に識別し、再試行の判断に利用できるようになります。

これらの変更により、Goのnetパッケージは、ネットワーク接続の確立において、より堅牢で信頼性の高い挙動を示すようになりました。

関連リンク

参考にした情報源リンク