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

[インデックス 18113] ファイルの概要

このコミットは、Go言語の標準ライブラリである database/sql パッケージにおけるコネクションプールの挙動を修正するものです。具体的には、コネクションプール内の最後のコネクションが正しく払い出されないというバグを修正し、それに対応するテストケースを追加しています。

コミット

commit 0d12e24ebb037202c3324c230e075f1e448f6f34
Author: Marko Tiikkaja <marko@joh.to>
Date:   Thu Dec 26 11:27:18 2013 -0800

    database/sql: Use all connections in pool
    
    The last connection in the pool was not being handed out correctly.
    
    R=golang-codereviews, gobot, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/40410043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0d12e24ebb037202c3324c230e075f1e448f6f34

元コミット内容

database/sql: Use all connections in pool

The last connection in the pool was not being handed out correctly.

R=golang-codereviews, gobot, bradfitz
CC=golang-codereviews
https://golang.org/cl/40410043

変更の背景

このコミットの背景には、Go言語の database/sql パッケージが提供するデータベースコネクションプールにおける、特定の条件下でのコネクション払い出しの不具合がありました。コミットメッセージに「The last connection in the pool was not being handed out correctly.(プール内の最後のコネクションが正しく払い出されていなかった)」とあるように、コネクションプールの最大オープンコネクション数 (maxOpen) が設定されている場合、その上限に達した際に、プール内に利用可能なフリーコネクションがあるにも関わらず、新しいコネクションが作成されるか、あるいはデッドロックが発生する可能性がありました。

具体的には、db.maxOpen が設定されており、かつ db.numOpen (現在オープンしているコネクション数) が db.maxOpen に達している状況で、db.freeConn (利用可能なフリーコネクションのリスト) にコネクションが存在する場合、既存のロジックではこのフリーコネクションが利用されず、新しいコネクションを要求する処理が実行されてしまうという問題がありました。これにより、コネクションプールの効率が低下したり、最悪の場合、アプリケーションがデッドロックに陥る可能性がありました。このコミットは、この論理的な誤りを修正し、プール内のすべてのコネクションが適切に利用されるようにすることを目的としています。

前提知識の解説

このコミットを理解するためには、Go言語の database/sql パッケージにおける以下の概念を理解しておく必要があります。

  • database/sql パッケージ: Go言語の標準ライブラリで、SQLデータベースへの汎用的なインターフェースを提供します。特定のデータベースドライバに依存しない抽象化レイヤーであり、アプリケーションはドライバを切り替えるだけで異なるデータベースに接続できます。
  • コネクションプール (Connection Pool): データベースへの接続はコストの高い操作です。コネクションプールは、事前に確立されたデータベースコネクションのセットを管理し、必要に応じてアプリケーションに提供し、使用後に再利用することで、コネクションの確立・切断のオーバーヘッドを削減し、アプリケーションのパフォーマンスを向上させます。
  • DB.SetMaxOpenConns(n int): database/sql パッケージの DB オブジェクトのメソッドで、同時にオープンできるデータベースコネクションの最大数を設定します。この設定は、データベースサーバーへの負荷を制御し、リソースの枯渇を防ぐために重要です。n が0の場合は無制限を意味します。
  • db.numOpen: DB オブジェクトが現在オープンしている(使用中またはフリーな)データベースコネクションの総数を追跡する内部カウンタです。
  • db.freeConn: DB オブジェクトが管理する、現在使用可能でプールに戻された(アイドル状態の)コネクションのリストです。新しいコネクションが必要になった場合、まずこのリストから利用可能なコネクションが探されます。
  • db.conn() メソッド: database/sql パッケージの内部メソッドで、新しいデータベースコネクションを取得するロジックをカプセル化しています。このメソッドが、コネクションプールから既存のコネクションを再利用するか、新しいコネクションを作成するかを決定します。

このコミットの修正は、特に db.conn() メソッド内のコネクション払い出しロジック、特に db.maxOpen の設定と db.freeConn の状態を評価する条件式に焦点を当てています。

技術的詳細

このコミットの技術的な核心は、src/pkg/database/sql/sql.go ファイル内の db.conn() メソッドにおける条件式の変更にあります。

変更前のコードは以下のようになっていました。

// If db.maxOpen > 0 and the number of open connections is over the limit
// or there are no free connection, then make a request and wait.
if db.maxOpen > 0 && (db.numOpen >= db.maxOpen || db.freeConn.Len() == 0) {
    // ...
}

この条件式は、「db.maxOpen が0より大きく、かつ (db.numOpendb.maxOpen 以上である または db.freeConn が空である)」場合に、新しいコネクションを要求し、待機するというロジックでした。

この問題は、db.maxOpen > 0 && (db.numOpen >= db.maxOpen || db.freeConn.Len() == 0)|| (OR) 演算子にありました。

例えば、db.maxOpen が1に設定されており、db.numOpen も1であるとします。このとき、db.freeConn にコネクションが1つ存在する場合を考えます。 変更前の条件式では、db.maxOpen > 0true です。そして、db.numOpen >= db.maxOpen (1 >= 1) も true です。 この場合、true || db.freeConn.Len() == 0true と評価されます。 結果として、true && true となり、条件式全体が true になります。

これは、db.numOpendb.maxOpen に達しているという理由だけで、たとえ db.freeConn に利用可能なコネクションがあったとしても、新しいコネクションを要求する(または待機する)ロジックが実行されてしまうことを意味します。本来であれば、フリーコネクションがあればそれを再利用すべきです。このバグにより、プール内の最後のコネクションが適切に利用されず、デッドロックや不必要なコネクション作成が発生する可能性がありました。

修正後のコードは以下のようになります。

// If db.maxOpen > 0 and the number of open connections is over the limit
// and there are no free connection, make a request and wait.
if db.maxOpen > 0 && db.numOpen >= db.maxOpen && db.freeConn.Len() == 0 {
    // ...
}

変更点としては、db.numOpen >= db.maxOpen || db.freeConn.Len() == 0db.numOpen >= db.maxOpen && db.freeConn.Len() == 0 に変更されたことです。|| (OR) 演算子が && (AND) 演算子に置き換えられました。

この修正により、新しいコネクションを要求し待機する条件は、「db.maxOpen が0より大きく、かつ db.numOpendb.maxOpen 以上であり、かつ db.freeConn が空である」という、より厳密なものになりました。

上記の例で再評価すると、db.maxOpen が1、db.numOpen が1、db.freeConn にコネクションが1つ存在する場合、 db.maxOpen > 0true です。 db.numOpen >= db.maxOpen (1 >= 1) は true です。 db.freeConn.Len() == 0false です(フリーコネクションが存在するため)。 したがって、true && true && false となり、条件式全体が false に評価されます。

これにより、フリーコネクションが存在する限り、新しいコネクションを要求するロジックは実行されず、既存のフリーコネクションが優先的に利用されるようになります。この変更によって、コネクションプールの動作がより意図通りになり、効率的なリソース利用とデッドロックの回避が実現されました。

また、この修正を検証するために src/pkg/database/sql/sql_test.goTestSingleOpenConn という新しいテストケースが追加されました。このテストは、SetMaxOpenConns(1) を設定した状態で、2回連続でクエリを実行し、デッドロックが発生しないことを確認するものです。これは、まさに修正されたバグが引き起こす可能性のあるシナリオを再現し、修正が正しく機能していることを保証するためのものです。

コアとなるコードの変更箇所

src/pkg/database/sql/sql.godb.conn() メソッド内の条件式が変更されました。

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -620,8 +620,8 @@ func (db *DB) conn() (*driverConn, error) {
 	}\n \n 	// If db.maxOpen > 0 and the number of open connections is over the limit
-\t// or there are no free connection, then make a request and wait.\n-\tif db.maxOpen > 0 && (db.numOpen >= db.maxOpen || db.freeConn.Len() == 0) {\n+\t// and there are no free connection, make a request and wait.\n+\tif db.maxOpen > 0 && db.numOpen >= db.maxOpen && db.freeConn.Len() == 0 {\n \t\t// Make the connRequest channel. It\'s buffered so that the\n \t\t// connectionOpener doesn\'t block while waiting for the req to be read.\n \t\tch := make(chan interface{}, 1)

src/pkg/database/sql/sql_test.go に新しいテストケース TestSingleOpenConn が追加されました。

--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -1033,6 +1033,29 @@ func TestMaxOpenConns(t *testing.T) {\n \t}\n }\n \n+func TestSingleOpenConn(t *testing.T) {\n+\tdb := newTestDB(t, \"people\")\n+\tdefer closeDB(t, db)\n+\n+\tdb.SetMaxOpenConns(1)\n+\n+\trows, err := db.Query(\"SELECT|people|name|\")\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tif err = rows.Close(); err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\t// shouldn\'t deadlock\n+\trows, err = db.Query(\"SELECT|people|name|\")\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\tif err = rows.Close(); err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+}\n+\n // golang.org/issue/5323\n func TestStmtCloseDeps(t *testing.T) {\n \tif testing.Short() {

コアとなるコードの解説

src/pkg/database/sql/sql.go の変更

変更された行は、db.conn() メソッド内のコネクション取得ロジックの一部です。

元のコード: if db.maxOpen > 0 && (db.numOpen >= db.maxOpen || db.freeConn.Len() == 0) {

修正後のコード: if db.maxOpen > 0 && db.numOpen >= db.maxOpen && db.freeConn.Len() == 0 {

この変更は、条件式の論理演算子を || (OR) から && (AND) に変更したものです。

  • 変更前: db.maxOpen が設定されており、かつ「オープンコネクション数が上限に達している」または「フリーコネクションがない」場合に、新しいコネクションを要求する。この「または」が問題でした。フリーコネクションがあるにも関わらず、オープンコネクション数が上限に達しているという条件だけで新しいコネクションを要求してしまう可能性がありました。
  • 変更後: db.maxOpen が設定されており、かつ「オープンコネクション数が上限に達している」かつ「フリーコネクションがない」場合にのみ、新しいコネクションを要求する。これにより、フリーコネクションが存在する場合は、必ずそちらが優先的に利用されるようになり、コネクションプールの意図された動作が保証されます。

src/pkg/database/sql/sql_test.go の追加テスト

TestSingleOpenConn は、この修正が正しく機能していることを検証するための重要なテストケースです。

func TestSingleOpenConn(t *testing.T) {
    db := newTestDB(t, "people") // テスト用のDBインスタンスを作成
    defer closeDB(t, db)        // テスト終了時にDBをクローズ

    db.SetMaxOpenConns(1) // 最大オープンコネクション数を1に設定

    // 1回目のクエリ実行
    rows, err := db.Query("SELECT|people|name|")
    if err != nil {
        t.Fatal(err)
    }
    if err = rows.Close(); err != nil {
        t.Fatal(err)
    }

    // 2回目のクエリ実行 - デッドロックが発生しないことを確認
    // "shouldn't deadlock" というコメントが意図を明確にしている
    rows, err = db.Query("SELECT|people|name|")
    if err != nil {
        t.Fatal(err)
    }
    if err = rows.Close(); err != nil {
        t.Fatal(err)
    }
}

このテストの目的は、SetMaxOpenConns(1) という非常に厳しいコネクション制限の下で、連続してクエリを実行してもデッドロックが発生しないことを確認することです。

  1. db.SetMaxOpenConns(1): コネクションプールが同時に保持できるコネクション数を1に制限します。
  2. 1回目の db.Query(): 新しいコネクションが作成され、使用されます。db.numOpen は1になります。rows.Close() が呼ばれると、このコネクションはプールに戻され、db.freeConn に追加されます。
  3. 2回目の db.Query(): ここが重要です。修正前であれば、db.numOpendb.maxOpen (どちらも1) に達しているため、db.numOpen >= db.maxOpentrue となり、db.freeConn に利用可能なコネクションがあるにも関わらず、新しいコネクションを要求しようとしてデッドロックが発生する可能性がありました。修正後は、db.numOpen >= db.maxOpen && db.freeConn.Len() == 0 という条件が評価され、db.freeConn.Len() == 0false であるため、条件全体が false となり、既存のフリーコネクションが再利用されます。

このテストは、修正されたロジックが、コネクションプールが最大オープンコネクション数に達している状況でも、フリーコネクションを正しく再利用し、デッドロックを回避できることを保証します。

関連リンク

参考にした情報源リンク

  • Go database/sql パッケージのドキュメント: https://pkg.go.dev/database/sql
  • Go database/sql のコネクションプールに関する一般的な解説記事 (例: "Go database/sql connection pool explained" などで検索)
    • (具体的なURLは検索結果によるため、ここでは一般的な検索クエリを記載)
  • Go言語の論理演算子に関するドキュメントやチュートリアル (例: "Go logical operators")
    • (具体的なURLは検索結果によるため、ここでは一般的な検索クエリを記載)
  • Go言語のテストに関するドキュメントやチュートリアル (例: "Go testing")
    • (具体的なURLは検索結果によるため、ここでは一般的な検索クエリを記載)
  • Go言語のデッドロックに関する一般的な解説 (例: "Go deadlock")
    • (具体的なURLは検索結果によるため、ここでは一般的な検索クエリを記載)
  • Go言語の database/sql パッケージのソースコード (特に sql.gosql_test.go)