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

[インデックス 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カウンタの更新タイミングに起因する競合状態です。

問題のメカニズム:

  1. 以前の実装: 変更前のコードでは、新しいデータベース接続を確立する際に、db.numOpenのインクリメント(db.numOpen++)が、実際にデータベースドライバが新しい接続を開く操作(db.driver.Open(db.dsn))が成功した後に行われていました。
  2. 競合状態の発生:
    • 複数のゴルーチンがほぼ同時に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を超えてしまう可能性がありました。
  3. 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++ は削除された)
}

変更のポイント:

  1. db.numOpen++ // optimistically:
    • この行がdb.driver.Open()の呼び出しに移動されました。
    • コメントにあるように、「楽観的に」接続数をインクリメントしています。これは、新しい接続を開く試みが始まった時点で、すぐにdb.numOpenを増やすことを意味します。
    • これにより、db.driver.Open()が実行されている間も、db.numOpenは「現在開こうとしている接続」を含んだ状態になります。他のゴルーチンがこのカウンタを参照する際に、より正確な情報に基づいて判断できるようになります。
  2. エラーパスでのdb.numOpen-- // correct for earlier optimism:
    • db.driver.Open()がエラーを返した場合、つまり新しい接続の確立に失敗した場合に実行されるコードブロックです。
    • 先に「楽観的に」インクリメントしたdb.numOpenの値を元に戻す(デクリメントする)ために追加されました。
    • これにより、失敗した接続試行がnumOpenのカウントに影響を与えず、カウンタが常に正確なオープン接続数を反映するように保証されます。
  3. ミューテックス(db.mu.Lock()db.mu.Unlock():
    • db.numOpenへのアクセスは、db.muというミューテックスによって保護されています。これにより、複数のゴルーチンが同時にdb.numOpenを読み書きする際の競合状態を防ぎ、カウンタの一貫性を保ちます。
    • db.driver.Open()の呼び出し中は、I/O操作であり時間がかかるため、ロックを解放しています。これは、他のゴルーチンがその間に他の処理(例えば、既存の接続の取得)を行えるようにするためです。

この修正により、database/sqlパッケージは、並行性の高い環境下でもオープン接続数をより正確に管理できるようになり、MaxOpenConnsの制限が確実に守られるようになりました。

関連リンク

参考にした情報源リンク

  • Go database/sqlパッケージ公式ドキュメント (Go言語の標準ライブラリに関する一般的な知識)
  • Go言語の並行処理と競合状態に関する一般的な知識