[インデックス 19295] ファイルの概要
このコミットは、Go言語の標準ライブラリであるdatabase/sql
パッケージにおける、オープンなデータベース接続数のカウントに関する競合状態(レースコンディション)を修正するものです。特に、TestMaxOpenConns
というテストが不安定に失敗する問題に対処しています。
コミット
commit ce6b75dab634e272e0449f85853fca7f1850da8b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed May 7 11:54:29 2014 -0700
database/sql: fix accounting of open connections
Existing test TestMaxOpenConns was failing occasionally, especially
with higher values of GOMAXPROCS.
Fixes #7532
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/95130043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ce6b75dab634e272e0449f85853fca7f1850da8b
元コミット内容
database/sql: fix accounting of open connections
既存のTestMaxOpenConns
テストが、特にGOMAXPROCS
の値が高い場合に時々失敗していました。
このコミットは #7532 を修正します。
変更の背景
このコミットの背景には、Goのdatabase/sql
パッケージが管理するデータベース接続の最大数に関する問題がありました。具体的には、TestMaxOpenConns
というテストが、設定された最大オープン接続数(SetMaxOpenConns
で設定される)が正しく機能しているかを検証する際に、時折失敗するという事象が発生していました。
この問題は、特にGOMAXPROCS
環境変数の値が高い(つまり、Goランタイムがより多くのOSスレッドを並行して利用できる)場合に顕著でした。これは、複数のゴルーチンが同時にデータベース接続を開こうとした際に、db.numOpen
というオープン接続数を追跡するカウンタの更新タイミングに競合状態が存在したことを示唆しています。
結果として、一時的に設定された最大接続数を超えてしまうことがあり、テストの不安定性につながっていました。このコミットは、この競合状態を解消し、オープン接続数の正確な会計(accounting)を保証することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびデータベース関連の概念を理解しておく必要があります。
database/sql
パッケージ: Go言語の標準ライブラリの一部であり、SQLデータベースとの対話のための汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、データベースドライバは別途インポートして利用します。これにより、アプリケーションコードは特定のデータベース実装に依存することなく、抽象化された方法でデータベースを操作できます。- コネクションプール: データベースへの接続の確立は、ネットワークオーバーヘッドや認証処理など、比較的コストの高い操作です。コネクションプールは、確立済みのデータベース接続を再利用可能な状態で保持しておくことで、これらのオーバーヘッドを削減し、アプリケーションのパフォーマンスを向上させるメカニズムです。
database/sql
パッケージは内部的にコネクションプールを管理しており、DB.SetMaxOpenConns()
などのメソッドでプールの振る舞いを設定できます。 db.numOpen
:database/sql
パッケージのDB
構造体内部で管理されるフィールドで、現在アクティブにオープンしているデータベース接続の総数を追跡するためのカウンタです。このカウンタは、SetMaxOpenConns
で設定された最大接続数を超えないようにするために重要な役割を果たします。driver.Open
:database/sql
パッケージがデータベースドライバと連携する際に使用されるインターフェースの一部です。具体的には、データベースドライバが新しい物理的なデータベース接続を確立する際に呼び出されるメソッドです。この操作はI/Oを伴うため、時間がかかる可能性があります。GOMAXPROCS
: Goランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数です。デフォルトでは、利用可能なCPUコア数に設定されます。GOMAXPROCS
の値が高いほど、Goスケジューラはより多くのゴルーチンを並行して実行しようとします。これにより、複数のゴルーチンが共有リソース(この場合はdb.numOpen
カウンタ)に同時にアクセスする機会が増え、競合状態が顕在化しやすくなります。
技術的詳細
このコミットが修正する問題は、database/sql
パッケージのDB.conn()
メソッドにおけるdb.numOpen
カウンタの更新タイミングに起因する競合状態です。
問題のメカニズム:
- 以前の実装: 変更前のコードでは、新しいデータベース接続を確立する際に、
db.numOpen
のインクリメント(db.numOpen++
)が、実際にデータベースドライバが新しい接続を開く操作(db.driver.Open(db.dsn)
)が成功した後に行われていました。 - 競合状態の発生:
- 複数のゴルーチンがほぼ同時に
DB.conn()
メソッドを呼び出し、新しい接続を要求します。 - これらのゴルーチンは、
db.numOpen
が最大接続数に達していないことを確認し、ロックを解放してdb.driver.Open()
を呼び出します。 db.driver.Open()
はI/O操作であり、完了までに時間がかかります。この間に、他のゴルーチンがdb.numOpen
のチェックを通過し、さらにdb.driver.Open()
を呼び出す可能性があります。- 結果として、
db.driver.Open()
がまだ完了していないにもかかわらず、db.numOpen
がインクリメントされていないため、database/sql
パッケージは「まだ接続数に余裕がある」と誤って判断し、設定されたMaxOpenConns
を超えてしまう可能性がありました。
- 複数のゴルーチンがほぼ同時に
TestMaxOpenConns
の失敗:TestMaxOpenConns
は、MaxOpenConns
の制限が正しく適用されることを検証するテストです。上記の競合状態により、一時的に制限を超過する接続が作成されることがあり、テストが不安定に失敗する原因となっていました。GOMAXPROCS
が高いほど、並行性が増し、この競合状態が発生しやすくなります。
修正のロジック:
このコミットは、「楽観的な(optimistic)」インクリメントと、エラー時の「悲観的な(pessimistic)」デクリメントというアプローチでこの問題を解決します。
- 楽観的なインクリメント:
db.numOpen++
の処理を、db.driver.Open()
を呼び出す前に移動します。これにより、新しい接続を開く試みが始まった時点で、すぐにdb.numOpen
をインクリメントします。これは、「これから接続を開くので、接続数を1つ増やす」という意図を即座に反映させるものです。 - エラー時のデクリメント: もし
db.driver.Open()
が何らかの理由で失敗した場合(例えば、データベースが利用できない、認証エラーなど)、先にインクリメントしたdb.numOpen
の値を元に戻す(デクリメントする)処理を追加します。これにより、失敗した接続試行がnumOpen
のカウントに影響を与えないようにします。
この変更により、db.driver.Open()
が実行されている間もdb.numOpen
が「現在開こうとしている接続」を含んだ状態になるため、他のゴルーチンがdb.numOpen
をチェックする際に、より正確な情報に基づいて判断できるようになります。これにより、MaxOpenConns
の制限がより厳密に守られるようになり、TestMaxOpenConns
の不安定性が解消されます。
コアとなるコードの変更箇所
変更はsrc/pkg/database/sql/sql.go
ファイルのDB.conn()
メソッド内で行われています。
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -652,13 +652,16 @@ func (db *DB) conn() (*driverConn, error) {
return conn, nil
}
+ db.numOpen++ // optimistically
db.mu.Unlock()
ci, err := db.driver.Open(db.dsn)
if err != nil {
+ db.mu.Lock()
+ db.numOpen-- // correct for earlier optimism
+ db.mu.Unlock()
return nil, err
}
db.mu.Lock()
- db.numOpen++
dc := &driverConn{
db: db,
ci: ci,
コアとなるコードの解説
変更の核心は、db.numOpen++
の行が移動され、エラーハンドリングのパスにdb.numOpen--
が追加された点です。
変更前:
func (db *DB) conn() (*driverConn, error) {
// ... (既存の接続を再利用するロジック)
db.mu.Unlock() // ロックを解放して、ドライバのOpenを呼び出す
ci, err := db.driver.Open(db.dsn) // 新しい物理接続を開く
if err != nil {
return nil, err
}
db.mu.Lock() // 再びロックを取得
db.numOpen++ // ここでオープン接続数をインクリメント
// ...
}
変更前は、db.driver.Open()
が成功した後にdb.numOpen
がインクリメントされていました。この間に他のゴルーチンがdb.numOpen
をチェックすると、まだインクリメントされていないため、誤って「接続数に余裕がある」と判断し、結果的に最大接続数を超えてしまう可能性がありました。
変更後:
func (db *DB) conn() (*driverConn, error) {
// ... (既存の接続を再利用するロジック)
db.numOpen++ // optimistically // ここに移動
db.mu.Unlock() // ロックを解放して、ドライバのOpenを呼び出す
ci, err := db.driver.Open(db.dsn) // 新しい物理接続を開く
if err != nil {
db.mu.Lock()
db.numOpen-- // correct for earlier optimism // エラー時にデクリメント
db.mu.Unlock()
return nil, err
}
db.mu.Lock() // 再びロックを取得
// ... (以前の db.numOpen++ は削除された)
}
変更のポイント:
db.numOpen++ // optimistically
:- この行が
db.driver.Open()
の呼び出し前に移動されました。 - コメントにあるように、「楽観的に」接続数をインクリメントしています。これは、新しい接続を開く試みが始まった時点で、すぐに
db.numOpen
を増やすことを意味します。 - これにより、
db.driver.Open()
が実行されている間も、db.numOpen
は「現在開こうとしている接続」を含んだ状態になります。他のゴルーチンがこのカウンタを参照する際に、より正確な情報に基づいて判断できるようになります。
- この行が
- エラーパスでの
db.numOpen-- // correct for earlier optimism
:db.driver.Open()
がエラーを返した場合、つまり新しい接続の確立に失敗した場合に実行されるコードブロックです。- 先に「楽観的に」インクリメントした
db.numOpen
の値を元に戻す(デクリメントする)ために追加されました。 - これにより、失敗した接続試行が
numOpen
のカウントに影響を与えず、カウンタが常に正確なオープン接続数を反映するように保証されます。
- ミューテックス(
db.mu.Lock()
とdb.mu.Unlock()
):db.numOpen
へのアクセスは、db.mu
というミューテックスによって保護されています。これにより、複数のゴルーチンが同時にdb.numOpen
を読み書きする際の競合状態を防ぎ、カウンタの一貫性を保ちます。db.driver.Open()
の呼び出し中は、I/O操作であり時間がかかるため、ロックを解放しています。これは、他のゴルーチンがその間に他の処理(例えば、既存の接続の取得)を行えるようにするためです。
この修正により、database/sql
パッケージは、並行性の高い環境下でもオープン接続数をより正確に管理できるようになり、MaxOpenConns
の制限が確実に守られるようになりました。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/ce6b75dab634e272e0449f85853fca7f1850da8b
- Go Code Review (CL): https://golang.org/cl/95130043
参考にした情報源リンク
- Go
database/sql
パッケージ公式ドキュメント (Go言語の標準ライブラリに関する一般的な知識) - Go言語の並行処理と競合状態に関する一般的な知識