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

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

  1. Go言語のnetパッケージ: Goのnetパッケージは、TCP/IP、UDP、Unixドメインソケットなどのネットワーク通信機能を提供します。内部的には、OSのシステムコールを効率的にラップし、非同期I/OをGoルーチンとチャネルで抽象化しています。
  2. ファイルディスクリプタ(File Descriptor, FD): Unix系OSにおいて、ファイルやソケットなどのI/Oリソースを識別するために使用される整数値です。Goのnetパッケージでは、ネットワーク接続を抽象化するために内部的にファイルディスクリプタを管理しています。
  3. netFD構造体: Goのnetパッケージ内部で使用される構造体で、ネットワーク接続に関連するファイルディスクリプタやその他の状態をカプセル化しています。ReadWriteCloseなどのI/O操作はこのnetFDを通じて行われます。
  4. pollServer: Goのnetパッケージが内部的に使用するI/O多重化(multiplexing)機構です。Linuxのepoll、macOS/FreeBSDのkqueue、WindowsのI/O完了ポート(IOCP)など、OS固有の非同期I/Oメカニズムを抽象化し、多数のネットワーク接続を効率的に処理します。pollServerは、I/Oイベントの監視、準備ができたファイルディスクリプタの通知、およびそれらのファイルディスクリプタのライフサイクル管理(登録、削除など)を行います。
  5. ミューテックス(Mutex)と競合(Contention): ミューテックスは、共有リソースへのアクセスを同期するための排他制御メカニズムです。複数のGoルーチンが同時にミューテックスを取得しようとすると、競合が発生し、待機時間が増加してプログラムのパフォーマンスが低下します。プロファイリング結果のsync.(*Mutex).Lockは、ミューテックスのロック取得に費やされた時間を示しています。
  6. deferステートメント: Go言語のdeferステートメントは、関数がリターンする直前に指定された関数呼び出しを実行します。リソースの解放(ファイルのクローズ、ロックのアンロックなど)を確実に行うために便利ですが、その実行タイミングが関数の終了時であるため、ロックの保持期間が長くなることがあります。
  7. runtime.chanrecv1runtime.chansend1: これらはGoランタイムの内部関数で、それぞれチャネルからの受信操作とチャネルへの送信操作に関連するCPU時間を示します。Goのネットワークスタックは、非同期I/OイベントをGoルーチンに通知するためにチャネルを多用するため、これらの関数がプロファイリング結果に現れるのは一般的です。高い値は、チャネル操作が頻繁に行われているか、チャネルの待機が発生していることを示唆します。

技術的詳細

このコミットの技術的詳細は、GoのネットワークI/Oモデルと、netFDのライフサイクル管理におけるpollServerの役割に深く関連しています。

Goのnetパッケージは、ノンブロッキングI/OとOSのI/O多重化メカニズム(epoll, kqueueなど)を組み合わせて、多数の同時接続を効率的に処理します。各ネットワーク接続はnetFD構造体によって表現され、このnetFDpollServerに登録されます。pollServerは、I/Oイベントが発生した際に、対応するGoルーチンを起動して処理を継続させます。

netFD.Close()メソッドは、ネットワーク接続を閉じる際に呼び出されます。このメソッドの主な役割は以下の通りです。

  1. fd.incref(true): ファイルディスクリプタの参照カウントをインクリメントし、クローズ処理中に他のI/O操作が開始されないようにします。
  2. fd.pollServer.Evict(fd): pollServerからこのnetFDを削除し、それ以上I/Oイベントを監視しないようにします。これにより、このファイルディスクリプタに対する今後のI/O操作はエラー(errClosing)を返すようになります。
  3. 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)に関する一般的な情報