[インデックス 14462] ファイルの概要
このコミットは、Go言語のnet
パッケージにおけるソケットの読み書きデッドライン(期限)処理の改善に関するものです。特に、高速な送受信が行われる場合にデッドラインチェックが適切に機能しない問題を解決することを目的としています。
コミット
commit 5fa3aeb14d56f6af4d6ad3cc9c81a20775770911
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Nov 23 22:15:26 2012 -0800
net: check read and write deadlines before doing syscalls
Otherwise a fast sender or receiver can make sockets always
readable or writable, preventing deadline checks from ever
occuring.
Update #4191 (fixes it with other CL, coming separately)
Fixes #4403
R=golang-dev, alex.brainman, dave, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/6851096
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5fa3aeb14d56f6af4d6ad3cc9c81a20775770911
元コミット内容
net: check read and write deadlines before doing syscalls
Otherwise a fast sender or receiver can make sockets always readable or writable, preventing deadline checks from ever occuring.
Update #4191 (fixes it with other CL, coming separately)
Fixes #4403
変更の背景
このコミットは、Go言語のnet
パッケージにおける重要なバグ修正と改善を目的としています。主な背景は、ソケットの読み書き操作において設定されたデッドライン(期限)が、特定の条件下で正しく機能しないという問題が存在したことです。
具体的には、コミットメッセージに「高速な送信者または受信者がソケットを常に読み取り可能または書き込み可能にし、デッドラインチェックが全く発生しないようにする可能性がある」と記載されています。これは、データが非常に速いペースで送受信される場合、syscall.Read
やsyscall.Write
のようなシステムコールが即座に成功し、ブロック状態にならないために、デッドラインのチェックロジックに到達する前にデータ処理が完了してしまうというシナリオを指しています。
GoのネットワークI/Oは、内部的にノンブロッキングI/Oとruntime.poll
(またはそれに相当するOS固有のI/O多重化メカニズム、例: epoll, kqueue)を組み合わせて実装されています。デッドラインが設定されている場合、I/O操作がブロックされる前にデッドラインに達していないかを確認し、達していればタイムアウトエラーを返す必要があります。しかし、データが常に利用可能(読み取りの場合)またはバッファが常に利用可能(書き込みの場合)であると、システムコールがブロックせずにすぐに戻ってくるため、デッドラインチェックのためのポーリングメカニズムが起動せず、結果としてデッドラインが無視されてしまうという問題が発生していました。
この問題は、特にリアルタイム性が求められるアプリケーションや、ネットワークの輻輳や遅延を適切に処理する必要がある場合に、予期せぬ挙動やハングアップを引き起こす可能性がありました。コミットメッセージで参照されているIssue #4403 (net: Read/Write deadline not working for fast connections
) がこの問題の直接的な報告であり、Issue #4191 (net: Read/Write deadline not working for fast connections
) も関連する問題として挙げられています。
このコミットは、システムコールを実行する前に明示的にデッドラインをチェックすることで、この問題を解決し、GoのネットワークI/Oにおけるデッドライン処理の堅牢性を向上させています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびネットワークプログラミングに関する前提知識が必要です。
-
Go言語の
net
パッケージ:- Goの標準ライブラリで、TCP/UDPソケット、HTTPクライアント/サーバーなど、ネットワーク通信機能を提供します。
net.Conn
インターフェースは、ネットワーク接続の一般的な抽象化を提供し、Read
、Write
、Close
メソッドの他に、SetDeadline
、SetReadDeadline
、SetWriteDeadline
メソッドを通じてタイムアウトを設定できます。
-
デッドライン(Deadline):
- ネットワークI/O操作(読み取りまたは書き込み)が完了しなければならない絶対的な時刻(
time.Time
)を指します。 SetReadDeadline(t time.Time)
: 読み取り操作のデッドラインを設定します。この時刻までに読み取りが完了しない場合、操作はタイムアウトエラーを返します。SetWriteDeadline(t time.Time)
: 書き込み操作のデッドラインを設定します。この時刻までに書き込みが完了しない場合、操作はタイムアウトエラーを返します。SetDeadline(t time.Time)
: 読み取りと書き込みの両方のデッドラインを設定します。- デッドラインは、ネットワークの遅延や相手側の応答がない場合に、アプリケーションが無限にブロックされるのを防ぐために重要です。
- ネットワークI/O操作(読み取りまたは書き込み)が完了しなければならない絶対的な時刻(
-
ノンブロッキングI/Oと
syscall.EAGAIN
:- Unix系システムでは、ソケットをノンブロッキングモードに設定できます。このモードでは、I/O操作(
read
、write
など)が即座に完了できない場合(例: 読み取るデータがない、書き込むバッファがいっぱい)、操作はブロックせずにエラーコードEAGAIN
(またはEWOULDBLOCK
)を返します。 - Goの
net
パッケージは、内部的にノンブロッキングI/Oを使用し、runtime.poll
などのI/O多重化メカニズムと連携して、必要に応じてゴルーチンをブロック/アンブロックします。
- Unix系システムでは、ソケットをノンブロッキングモードに設定できます。このモードでは、I/O操作(
-
syscall
パッケージ:- Goの標準ライブラリで、オペレーティングシステム(OS)のシステムコールに直接アクセスするための機能を提供します。
syscall.Read
、syscall.Write
、syscall.Recvfrom
、syscall.Sendto
、syscall.Recvmsg
、syscall.Sendmsg
などは、それぞれOSの対応するシステムコールを呼び出します。- これらのシステムコールは、ソケットがノンブロッキングモードの場合、データが利用できない、またはバッファが満杯の場合に
EAGAIN
エラーを返すことがあります。
-
netFD
構造体:- Goの
net
パッケージ内部で使用されるファイルディスクリプタ(FD)を抽象化した構造体です。 sysfd
: OSのファイルディスクリプタ(整数値)。rdeadline
: 読み取りデッドラインのナノ秒単位のUnixタイムスタンプ。wdeadline
: 書き込みデッドラインのナノ秒単位のUnixタイムスタンプ。decref()
: 参照カウントを減らすためのメソッド。
- Goの
-
errTimeout
:- Goの
net
パッケージ内部で定義されている、タイムアウトエラーを示す特別なエラー値。
- Goの
これらの知識があることで、コミットがなぜ必要とされ、どのように問題を解決しているのかを深く理解できます。特に、ノンブロッキングI/Oとデッドライン処理の間の相互作用が、この問題の核心です。
技術的詳細
このコミットの技術的な核心は、Goのnet
パッケージにおけるソケットI/O操作(Read
, ReadFrom
, ReadMsg
, Write
, WriteTo
, WriteMsg
)の内部実装において、syscall
パッケージを介して実際のシステムコールを呼び出す前に、明示的にデッドラインが経過していないかをチェックするロジックを追加した点にあります。
従来のGoのネットワークI/O処理では、デッドラインが設定されている場合、I/O操作がブロックされる際にruntime.poll
などのポーリングメカニズムがデッドラインを監視し、期限が過ぎていればタイムアウトエラーを返すように設計されていました。しかし、前述の「変更の背景」で述べたように、データが常に利用可能であったり、バッファが常に書き込み可能であったりする「高速な接続」のシナリオでは、システムコール(例: syscall.Read
)が即座に成功し、EAGAIN
エラーを返してブロック状態に入る機会がありませんでした。これにより、ポーリングメカニズムが起動せず、デッドラインチェックがスキップされてしまうという問題が発生していました。
このコミットでは、src/pkg/net/fd_unix.go
内の各I/Oメソッドのループの先頭に、以下の形式のデッドラインチェックを追加しています。
読み取り操作の場合(Read
, ReadFrom
, ReadMsg
):
if fd.rdeadline > 0 { // 読み取りデッドラインが設定されている場合
if time.Now().UnixNano() >= fd.rdeadline { // 現在時刻がデッドラインを過ぎている場合
err = errTimeout // タイムアウトエラーを設定
break // ループを抜ける
}
}
書き込み操作の場合(Write
, WriteTo
, WriteMsg
):
if fd.wdeadline > 0 { // 書き込みデッドラインが設定されている場合
if time.Now().UnixNano() >= fd.wdeadline { // 現在時刻がデッドラインを過ぎている場合
err = errTimeout // タイムアウトエラーを設定
break // ループを抜ける
}
}
この変更により、システムコールが成功してすぐに戻ってきたとしても、次のループイテレーションの開始時に必ずデッドラインがチェックされるようになります。もしデッドラインが既に過ぎていれば、システムコールを実行する前にerrTimeout
が設定され、ループが中断されるため、デッドラインが確実に尊重されるようになります。
また、src/pkg/net/timeout_test.go
には、この修正が正しく機能することを検証するための包括的なテストケースが追加されています。
TestVariousDeadlines
: 非常に短いナノ秒単位から長い秒単位までの様々なデッドラインを設定し、高速なデータ転送が行われる状況でタイムアウトが正しく発生するかを検証します。サーバーはneverEnding
リーダーからデータを無限にコピーし、クライアントはデッドラインを設定してデータを読み捨てます。クライアントが設定されたデッドラインでタイムアウトすることを確認します。TestReadDeadlineDataAvailable
: 読み取りデッドラインが過去に設定されている場合でも、ソケットにデータが利用可能であるときに、読み取り操作がタイムアウトエラーを返すことを検証します。これは、データがすぐに読み取れる場合でもデッドラインが優先されることを保証します。TestWriteDeadlineBufferAvailable
: 書き込みデッドラインが過去に設定されている場合でも、ソケットの書き込みバッファに空きがあるときに、書き込み操作がタイムアウトエラーを返すことを検証します。これは、バッファがすぐに書き込み可能である場合でもデッドラインが優先されることを保証します。
これらのテストは、デッドライン処理の堅牢性を大幅に向上させ、特に高速なネットワーク環境下でのGoアプリケーションの信頼性を高める上で不可欠です。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主にsrc/pkg/net/fd_unix.go
ファイル内のnetFD
構造体のI/Oメソッドです。具体的には、以下のメソッドにデッドラインチェックのロジックが追加されています。
-
func (fd *netFD) Read(p []byte) (n int, err error)
- 追加行: 426-430
--- a/src/pkg/net/fd_unix.go +++ b/src/pkg/net/fd_unix.go @@ -423,6 +423,12 @@ func (fd *netFD) Read(p []byte) (n int, err error) { }\n \tdefer fd.decref()\n \tfor {\n+\t\tif fd.rdeadline > 0 {\n+\t\t\tif time.Now().UnixNano() >= fd.rdeadline {\n+\t\t\t\terr = errTimeout\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t}\n \t\tn, err = syscall.Read(int(fd.sysfd), p)\n \t\tif err == syscall.EAGAIN {\n \t\t\terr = errTimeout
-
func (fd *netFD) ReadFrom(p []byte) (n int, sa syscall.Sockaddr, err error)
- 追加行: 456-460
--- a/src/pkg/net/fd_unix.go +++ b/src/pkg/net/fd_unix.go @@ -453,6 +459,12 @@ func (fd *netFD) ReadFrom(p []byte) (n int, sa syscall.Sockaddr, err error) { }\n \tdefer fd.decref()\n \tfor {\n+\t\tif fd.rdeadline > 0 {\n+\t\t\tif time.Now().UnixNano() >= fd.rdeadline {\n+\t\t\t\terr = errTimeout\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t}\n \t\tn, sa, err = syscall.Recvfrom(fd.sysfd, p, 0)\n \t\tif err == syscall.EAGAIN {\n \t\t\terr = errTimeout
-
func (fd *netFD) ReadMsg(p []byte, oob []byte) (n, oobn, flags int, sa syscall.Sockaddr, err error)
- 追加行: 484-488
--- a/src/pkg/net/fd_unix.go +++ b/src/pkg/net/fd_unix.go @@ -481,6 +493,12 @@ func (fd *netFD) ReadMsg(p []byte, oob []byte) (n, oobn, flags int, sa syscall.S\n \t}\n \tdefer fd.decref()\n \tfor {\n+\t\tif fd.rdeadline > 0 {\n+\t\t\tif time.Now().UnixNano() >= fd.rdeadline {\n+\t\t\t\terr = errTimeout\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t}\n \t\tn, oobn, flags, sa, err = syscall.Recvmsg(fd.sysfd, p, oob, 0)\n \t\tif err == syscall.EAGAIN {\n \t\t\terr = errTimeout
-
func (fd *netFD) Write(p []byte) (int, error)
- 追加行: 515-519
--- a/src/pkg/net/fd_unix.go +++ b/src/pkg/net/fd_unix.go @@ -512,6 +530,12 @@ func (fd *netFD) Write(p []byte) (int, error) { var err error\n \tnn := 0\n \tfor {\n+\t\tif fd.wdeadline > 0 {\n+\t\t\tif time.Now().UnixNano() >= fd.wdeadline {\n+\t\t\t\terr = errTimeout\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t}\n \t\tvar n int\n \t\tn, err = syscall.Write(int(fd.sysfd), p[nn:])\n \t\tif n > 0 {
-
func (fd *netFD) WriteTo(p []byte, sa syscall.Sockaddr) (n int, err error)
- 追加行: 554-558
--- a/src/pkg/net/fd_unix.go +++ b/src/pkg/net/fd_unix.go @@ -551,6 +575,12 @@ func (fd *netFD) WriteTo(p []byte, sa syscall.Sockaddr) (n int, err error) { }\n \tdefer fd.decref()\n \tfor {\n+\t\tif fd.wdeadline > 0 {\n+\t\t\tif time.Now().UnixNano() >= fd.wdeadline {\n+\t\t\t\terr = errTimeout\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t}\n \t\terr = syscall.Sendto(fd.sysfd, p, 0, sa)\n \t\tif err == syscall.EAGAIN {\n \t\t\terr = errTimeout
-
func (fd *netFD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (n int, oobn, flags int, err error)
- 追加行: 581-585
--- a/src/pkg/net/fd_unix.go +++ b/src/pkg/net/fd_unix.go @@ -578,6 +608,12 @@ func (fd *netFD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (n int, oob\n \t}\n \tdefer fd.decref()\n \tfor {\n+\t\tif fd.wdeadline > 0 {\n+\t\t\tif time.Now().UnixNano() >= fd.wdeadline {\n+\t\t\t\terr = errTimeout\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t}\n \t\terr = syscall.Sendmsg(fd.sysfd, p, oob, sa, 0)\n \t\tif err == syscall.EAGAIN {\n \t\t\terr = errTimeout
また、src/pkg/net/timeout_test.go
には、これらの変更を検証するための新しいテストケースが大量に追加されています。
コアとなるコードの解説
src/pkg/net/fd_unix.go
内の変更は、GoのネットワークI/Oの低レベルな実装部分に直接影響を与えます。各I/Oメソッド(Read
, Write
など)は、内部的に無限ループfor {}
を持ち、その中でsyscall
パッケージを介してOSのシステムコール(syscall.Read
, syscall.Write
など)を呼び出しています。
このループの目的は、ノンブロッキングI/Oの特性を管理することです。システムコールがEAGAIN
エラーを返した場合(つまり、データがまだ準備できていない、またはバッファが満杯である場合)、Goランタイムはゴルーチンをブロックし、ソケットが準備できたときに再スケジュールします。ソケットが準備できたら、ループの次のイテレーションで再度システムコールを試行します。
追加されたデッドラインチェックのコードは、このループの先頭に配置されています。
if fd.rdeadline > 0 { // または fd.wdeadline > 0
if time.Now().UnixNano() >= fd.rdeadline { // または fd.wdeadline
err = errTimeout
break
}
}
このコードブロックの各部分を解説します。
-
fd.rdeadline > 0
(またはfd.wdeadline > 0
):netFD
構造体のrdeadline
(読み取りデッドライン)またはwdeadline
(書き込みデッドライン)フィールドは、デッドラインが設定されていない場合は0
、設定されている場合はナノ秒単位のUnixタイムスタンプが格納されます。- この条件は、そもそもデッドラインが設定されているかどうかを確認します。デッドラインが設定されていない場合は、このチェックはスキップされ、従来の動作が維持されます。
-
time.Now().UnixNano() >= fd.rdeadline
:time.Now().UnixNano()
は、現在の時刻をナノ秒単位のUnixタイムスタンプで取得します。- この条件は、現在の時刻が設定されたデッドライン時刻以上であるか、つまりデッドラインが既に過ぎているかを確認します。
-
err = errTimeout
:- もしデッドラインが過ぎていれば、エラー変数
err
にnet
パッケージ内部で定義されているタイムアウトエラーerrTimeout
を代入します。
- もしデッドラインが過ぎていれば、エラー変数
-
break
:break
ステートメントは、現在のfor
ループを直ちに終了させます。これにより、システムコールが実行されることなく、タイムアウトエラーが呼び出し元に返されます。
この変更の重要な点は、デッドラインチェックがシステムコールを実行する前に行われることです。これにより、たとえソケットが常に読み取り可能または書き込み可能であり、システムコールがEAGAIN
を一度も返さずに即座に成功し続けたとしても、各ループイテレーションの開始時にデッドラインが確実に評価されるようになります。これにより、高速な接続環境下でもデッドラインが正しく機能し、アプリケーションが予期せずブロックされることを防ぎます。
src/pkg/net/timeout_test.go
に追加されたテストは、この新しいロジックが様々な条件下で正しく動作することを検証します。特に、TestReadDeadlineDataAvailable
とTestWriteDeadlineBufferAvailable
は、データがすぐに利用可能である、またはバッファがすぐに書き込み可能であるという「高速な接続」のシナリオをシミュレートし、それでもデッドラインが尊重されることを確認しています。
関連リンク
- Go Issue #4403: net: Read/Write deadline not working for fast connections
- Go Issue #4191: net: Read/Write deadline not working for fast connections (このコミットで更新される関連Issue)
- Go Code Review CL 6851096: net: check read and write deadlines before doing syscalls
参考にした情報源リンク
- Go言語の公式ドキュメント:
net
パッケージ - Go言語の公式ドキュメント:
syscall
パッケージ - Unix系システムのノンブロッキングI/Oに関する一般的な情報
- Go言語のI/O多重化(
runtime.poll
など)に関する情報 - GitHubのGoリポジトリのIssueトラッカー