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

[インデックス 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.Readsyscall.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言語およびネットワークプログラミングに関する前提知識が必要です。

  1. Go言語のnetパッケージ:

    • Goの標準ライブラリで、TCP/UDPソケット、HTTPクライアント/サーバーなど、ネットワーク通信機能を提供します。
    • net.Connインターフェースは、ネットワーク接続の一般的な抽象化を提供し、ReadWriteCloseメソッドの他に、SetDeadlineSetReadDeadlineSetWriteDeadlineメソッドを通じてタイムアウトを設定できます。
  2. デッドライン(Deadline):

    • ネットワークI/O操作(読み取りまたは書き込み)が完了しなければならない絶対的な時刻(time.Time)を指します。
    • SetReadDeadline(t time.Time): 読み取り操作のデッドラインを設定します。この時刻までに読み取りが完了しない場合、操作はタイムアウトエラーを返します。
    • SetWriteDeadline(t time.Time): 書き込み操作のデッドラインを設定します。この時刻までに書き込みが完了しない場合、操作はタイムアウトエラーを返します。
    • SetDeadline(t time.Time): 読み取りと書き込みの両方のデッドラインを設定します。
    • デッドラインは、ネットワークの遅延や相手側の応答がない場合に、アプリケーションが無限にブロックされるのを防ぐために重要です。
  3. ノンブロッキングI/Oとsyscall.EAGAIN:

    • Unix系システムでは、ソケットをノンブロッキングモードに設定できます。このモードでは、I/O操作(readwriteなど)が即座に完了できない場合(例: 読み取るデータがない、書き込むバッファがいっぱい)、操作はブロックせずにエラーコードEAGAIN(またはEWOULDBLOCK)を返します。
    • Goのnetパッケージは、内部的にノンブロッキングI/Oを使用し、runtime.pollなどのI/O多重化メカニズムと連携して、必要に応じてゴルーチンをブロック/アンブロックします。
  4. syscallパッケージ:

    • Goの標準ライブラリで、オペレーティングシステム(OS)のシステムコールに直接アクセスするための機能を提供します。
    • syscall.Readsyscall.Writesyscall.Recvfromsyscall.Sendtosyscall.Recvmsgsyscall.Sendmsgなどは、それぞれOSの対応するシステムコールを呼び出します。
    • これらのシステムコールは、ソケットがノンブロッキングモードの場合、データが利用できない、またはバッファが満杯の場合にEAGAINエラーを返すことがあります。
  5. netFD構造体:

    • Goのnetパッケージ内部で使用されるファイルディスクリプタ(FD)を抽象化した構造体です。
    • sysfd: OSのファイルディスクリプタ(整数値)。
    • rdeadline: 読み取りデッドラインのナノ秒単位のUnixタイムスタンプ。
    • wdeadline: 書き込みデッドラインのナノ秒単位のUnixタイムスタンプ。
    • decref(): 参照カウントを減らすためのメソッド。
  6. errTimeout:

    • Goのnetパッケージ内部で定義されている、タイムアウトエラーを示す特別なエラー値。

これらの知識があることで、コミットがなぜ必要とされ、どのように問題を解決しているのかを深く理解できます。特に、ノンブロッキング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メソッドです。具体的には、以下のメソッドにデッドラインチェックのロジックが追加されています。

  1. 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
    
  2. 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
    
  3. 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
    
  4. 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 {
    
  5. 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
    
  6. 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:

    • もしデッドラインが過ぎていれば、エラー変数errnetパッケージ内部で定義されているタイムアウトエラーerrTimeoutを代入します。
  • break:

    • breakステートメントは、現在のforループを直ちに終了させます。これにより、システムコールが実行されることなく、タイムアウトエラーが呼び出し元に返されます。

この変更の重要な点は、デッドラインチェックがシステムコールを実行する前に行われることです。これにより、たとえソケットが常に読み取り可能または書き込み可能であり、システムコールがEAGAINを一度も返さずに即座に成功し続けたとしても、各ループイテレーションの開始時にデッドラインが確実に評価されるようになります。これにより、高速な接続環境下でもデッドラインが正しく機能し、アプリケーションが予期せずブロックされることを防ぎます。

src/pkg/net/timeout_test.goに追加されたテストは、この新しいロジックが様々な条件下で正しく動作することを検証します。特に、TestReadDeadlineDataAvailableTestWriteDeadlineBufferAvailableは、データがすぐに利用可能である、またはバッファがすぐに書き込み可能であるという「高速な接続」のシナリオをシミュレートし、それでもデッドラインが尊重されることを確認しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: netパッケージ
  • Go言語の公式ドキュメント: syscallパッケージ
  • Unix系システムのノンブロッキングI/Oに関する一般的な情報
  • Go言語のI/O多重化(runtime.pollなど)に関する情報
  • GitHubのGoリポジトリのIssueトラッカー