[インデックス 14538] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージにおける、ファイルディスクリプタ(netFD
)のクローズ処理における競合(contention)を解消し、パフォーマンスを改善することを目的としています。具体的には、Close
メソッド内でのミューテックスロックの保持期間を短縮し、接続エラー時の不要なClose
呼び出しを削除しています。
コミット
commit b18a7c7caeca314f71326cfa9b59ea3bbcbf0850
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Sat Dec 1 09:26:07 2012 +0100
net: remove unnecessary Close contention.
Contention profile in BenchmarkTCPOneShot (Core 2 Quad):
Before
Total: 80.285 seconds
44.743 55.7% 55.7% 44.743 55.7% runtime.chanrecv1
31.995 39.9% 95.6% 31.995 39.9% sync.(*Mutex).Lock
3.547 4.4% 100.0% 3.547 4.4% runtime.chansend1
After
Total: 48.341 seconds
45.810 94.8% 94.8% 45.810 94.8% runtime.chanrecv1
2.530 5.2% 100.0% 2.530 5.2% runtime.chansend1
0.001 0.0% 100.0% 0.001 0.0% sync.(*Mutex).Lock
R=golang-dev, dave, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/6845119
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b18a7c7caeca314f71326cfa9b59ea3bbcbf0850
元コミット内容
このコミットは、src/pkg/net/fd_unix.go
とsrc/pkg/net/sock_posix.go
の2つのファイルを変更しています。
src/pkg/net/fd_unix.go
では、netFD
構造体のClose
メソッドにおいて、defer fd.pollServer.Unlock()
を削除し、代わりに明示的にfd.pollServer.Unlock()
を呼び出すように変更しています。これにより、ロックの保持期間が短縮されます。また、fd.incref(true)
がエラーを返した場合にもロックが解放されるように修正されています。
src/pkg/net/sock_posix.go
では、socket
関数内の接続エラー処理において、fd.Close()
の呼び出しが削除されています。これは、既にclosesocket(s)
でソケットが閉じられているため、fd.Close()
の呼び出しが不要であると判断されたためです。
変更の背景
この変更の背景には、Go言語のネットワーク処理におけるパフォーマンスのボトルネックがありました。コミットメッセージに示されているプロファイリング結果(BenchmarkTCPOneShot
)を見ると、変更前はsync.(*Mutex).Lock
がCPU時間の約40%を占めており、ミューテックスの競合が顕著であったことがわかります。
netFD.Close()
メソッドは、ネットワーク接続を閉じる際に呼び出されます。このメソッド内でpollServer
という内部コンポーネントのロックを取得していましたが、defer
ステートメントを使用していたため、Close
メソッド全体の実行期間中、このロックが保持されていました。特に、fd.decref()
のような他の操作もロックが保持された状態で行われていたため、並行して多数の接続が閉じられるようなシナリオでは、このロックがボトルネックとなり、パフォーマンスが低下していました。
また、sock_posix.go
におけるfd.Close()
の削除は、エラーパスにおける冗長な処理を排除し、コードの健全性を向上させることを目的としています。接続に失敗した場合、ソケットは既にシステムコールによって閉じられているため、GoのnetFD.Close()
を再度呼び出すことは無意味であり、場合によっては問題を引き起こす可能性がありました。
このコミットは、これらのパフォーマンス上の問題とコードの冗長性を解決するために行われました。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびネットワークプログラミングに関する知識が必要です。
- Go言語の
net
パッケージ: Goのnet
パッケージは、TCP/IP、UDP、Unixドメインソケットなどのネットワーク通信機能を提供します。内部的には、OSのシステムコールを効率的にラップし、非同期I/OをGoルーチンとチャネルで抽象化しています。 - ファイルディスクリプタ(File Descriptor, FD): Unix系OSにおいて、ファイルやソケットなどのI/Oリソースを識別するために使用される整数値です。Goの
net
パッケージでは、ネットワーク接続を抽象化するために内部的にファイルディスクリプタを管理しています。 netFD
構造体: Goのnet
パッケージ内部で使用される構造体で、ネットワーク接続に関連するファイルディスクリプタやその他の状態をカプセル化しています。Read
、Write
、Close
などのI/O操作はこのnetFD
を通じて行われます。pollServer
: Goのnet
パッケージが内部的に使用するI/O多重化(multiplexing)機構です。Linuxのepoll
、macOS/FreeBSDのkqueue
、WindowsのI/O完了ポート(IOCP)など、OS固有の非同期I/Oメカニズムを抽象化し、多数のネットワーク接続を効率的に処理します。pollServer
は、I/Oイベントの監視、準備ができたファイルディスクリプタの通知、およびそれらのファイルディスクリプタのライフサイクル管理(登録、削除など)を行います。- ミューテックス(Mutex)と競合(Contention): ミューテックスは、共有リソースへのアクセスを同期するための排他制御メカニズムです。複数のGoルーチンが同時にミューテックスを取得しようとすると、競合が発生し、待機時間が増加してプログラムのパフォーマンスが低下します。プロファイリング結果の
sync.(*Mutex).Lock
は、ミューテックスのロック取得に費やされた時間を示しています。 defer
ステートメント: Go言語のdefer
ステートメントは、関数がリターンする直前に指定された関数呼び出しを実行します。リソースの解放(ファイルのクローズ、ロックのアンロックなど)を確実に行うために便利ですが、その実行タイミングが関数の終了時であるため、ロックの保持期間が長くなることがあります。runtime.chanrecv1
とruntime.chansend1
: これらはGoランタイムの内部関数で、それぞれチャネルからの受信操作とチャネルへの送信操作に関連するCPU時間を示します。Goのネットワークスタックは、非同期I/OイベントをGoルーチンに通知するためにチャネルを多用するため、これらの関数がプロファイリング結果に現れるのは一般的です。高い値は、チャネル操作が頻繁に行われているか、チャネルの待機が発生していることを示唆します。
技術的詳細
このコミットの技術的詳細は、GoのネットワークI/Oモデルと、netFD
のライフサイクル管理におけるpollServer
の役割に深く関連しています。
Goのnet
パッケージは、ノンブロッキングI/OとOSのI/O多重化メカニズム(epoll
, kqueue
など)を組み合わせて、多数の同時接続を効率的に処理します。各ネットワーク接続はnetFD
構造体によって表現され、このnetFD
はpollServer
に登録されます。pollServer
は、I/Oイベントが発生した際に、対応するGoルーチンを起動して処理を継続させます。
netFD.Close()
メソッドは、ネットワーク接続を閉じる際に呼び出されます。このメソッドの主な役割は以下の通りです。
fd.incref(true)
: ファイルディスクリプタの参照カウントをインクリメントし、クローズ処理中に他のI/O操作が開始されないようにします。fd.pollServer.Evict(fd)
:pollServer
からこのnetFD
を削除し、それ以上I/Oイベントを監視しないようにします。これにより、このファイルディスクリプタに対する今後のI/O操作はエラー(errClosing
)を返すようになります。fd.decref()
: ファイルディスクリプタの参照カウントをデクリメントします。参照カウントが0になった場合、実際のOSレベルのファイルディスクリプタが閉じられます。
変更前のClose
メソッドでは、fd.pollServer.Lock()
の直後にdefer fd.pollServer.Unlock()
が配置されていました。これは、Close
メソッドが終了するまでpollServer
のミューテックスを保持することを意味します。このロックは、pollServer
の内部状態(例えば、登録されているファイルディスクリプタのリスト)を保護するために使用されます。しかし、Evict
操作の後、fd.decref()
が呼び出される間もロックが保持されていました。decref
は、参照カウントが0になった場合にOSのclose
システムコールを呼び出す可能性があり、この操作は比較的時間がかかる場合があります。多数の接続が同時に閉じられる場合、このdecref
の実行中に他のGoルーチンがpollServer
のロックを待つことになり、競合が発生していました。
変更後のコードでは、fd.pollServer.Unlock()
がfd.pollServer.Evict(fd)
の直後に移動されました。これにより、pollServer
の内部状態を変更するクリティカルセクションがEvict
の呼び出しで終了し、その後のdecref
の実行中はロックが解放されるようになりました。これにより、ミューテックスの保持期間が短縮され、並行性が向上し、結果としてsync.(*Mutex).Lock
の競合が大幅に減少しました。
また、fd.incref(true)
がエラーを返した場合にもfd.pollServer.Unlock()
が追加されたことで、エラーパスにおいても確実にロックが解放されるようになり、デッドロックのリスクが排除されました。
sock_posix.go
の変更は、より単純な最適化です。socket
関数内でfd.connect(ursa)
が失敗した場合、既にclosesocket(s)
が呼び出され、OSレベルでソケットが閉じられています。この状況でfd.Close()
を再度呼び出すことは冗長であり、場合によっては二重クローズのエラーや、既に閉じられたファイルディスクリプタに対する操作による予期せぬ挙動を引き起こす可能性がありました。この不要な呼び出しを削除することで、エラー処理パスが簡素化され、堅牢性が向上しました。
コアとなるコードの変更箇所
src/pkg/net/fd_unix.go
--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -375,8 +375,8 @@ func (fd *netFD) decref() {
func (fd *netFD) Close() error {
fd.pollServer.Lock() // needed for both fd.incref(true) and pollserver.Evict
- defer fd.pollServer.Unlock()
if err := fd.incref(true); err != nil {
+ fd.pollServer.Unlock()
return err
}
// Unblock any I/O. Once it all unblocks and returns,
@@ -385,6 +385,7 @@ func (fd *netFD) Close() error {
// fairly quickly, since all the I/O is non-blocking, and any
// attempts to block in the pollserver will return errClosing.
fd.pollServer.Evict(fd)
+ fd.pollServer.Unlock()
fd.decref()
return nil
}
src/pkg/net/sock_posix.go
--- a/src/pkg/net/sock_posix.go
+++ b/src/pkg/net/sock_posix.go
@@ -61,7 +61,6 @@ func socket(net string, f, t, p int, ipv6only bool, ulsa, ursa syscall.Sockaddr,
}
if err = fd.connect(ursa); err != nil {
closesocket(s)
- fd.Close()
return nil, err
}
fd.isConnected = true
コアとなるコードの解説
src/pkg/net/fd_unix.go
の変更
defer fd.pollServer.Unlock()
の削除: 以前はClose
メソッドの開始時にdefer
を使ってロックを解放していましたが、これによりメソッド全体でロックが保持されていました。fd.pollServer.Unlock()
の明示的な追加(エラーパス):if err := fd.incref(true); err != nil { ... }
ブロック内でエラーが発生した場合、以前はdefer
によってロックが解放されるのを待つ必要がありましたが、この変更により即座にロックが解放され、デッドロックや不必要なロック保持を防ぎます。fd.pollServer.Unlock()
の明示的な追加(成功パス):fd.pollServer.Evict(fd)
の直後にロックが解放されるようになりました。これにより、pollServer
の内部状態を変更するクリティカルセクションが終了した直後にロックが解放され、その後のfd.decref()
の実行中に他のGoルーチンがpollServer
のロックを待つ必要がなくなります。この変更が、コミットメッセージに示されたsync.(*Mutex).Lock
の競合の大幅な減少に直接寄与しています。
src/pkg/net/sock_posix.go
の変更
fd.Close()
の削除:if err = fd.connect(ursa); err != nil { ... }
ブロック内で、接続エラーが発生した場合にfd.Close()
を呼び出していました。しかし、その直前のclosesocket(s)
が既にOSレベルでソケットを閉じているため、fd.Close()
の呼び出しは冗長であり、場合によっては問題を引き起こす可能性がありました。この削除により、エラー処理がよりクリーンで効率的になりました。
これらの変更は、Goのネットワークスタックにおける並行性と効率性を向上させるための重要な最適化です。特に、多数の短命な接続を扱うサーバーアプリケーションにおいて、その効果が顕著に現れると考えられます。
関連リンク
- Go言語の
net
パッケージのドキュメント: https://pkg.go.dev/net - Go言語の
sync
パッケージのドキュメント: https://pkg.go.dev/sync - Go言語の
defer
ステートメントに関する公式ブログ記事など(一般的な情報源)
参考にした情報源リンク
- Go言語のソースコード(特に
src/pkg/net
ディレクトリ) - Go言語のプロファイリングツール(
pprof
)に関するドキュメントやチュートリアル - Go言語のネットワークI/Oモデルに関する技術記事や解説
- Goの
pollServer
に関する議論や実装解説(例: GoのIssueトラッカーやメーリングリストのアーカイブ) - Unix系OSのファイルディスクリプタとI/O多重化(
epoll
,kqueue
)に関する一般的な情報