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

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

このコミットは、Go言語の標準ライブラリである database/sql パッケージに、データベース接続のアイドル接続プールにおける最大接続数を設定する DB.SetMaxIdleConns メソッドを追加するものです。これにより、アプリケーション開発者はデータベース接続の管理をより細かく制御できるようになり、リソースの効率的な利用とパフォーマンスの向上が期待されます。

コミット

commit 3a2fe62f44a8a8513a087f75798425db7f9cc7bd
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Mar 18 15:33:04 2013 -0700

    database/sql: add DB.SetMaxIdleConns
    
    Update #4805
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/7634045

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

https://github.com/golang/go/commit/3a2fe62f44a8a8513a087f75798425db7f9cc7bd

元コミット内容

このコミットは、database/sql パッケージに DB.SetMaxIdleConns メソッドを追加し、アイドル状態のデータベース接続の最大数を設定できるようにします。これにより、Goアプリケーションがデータベースとの接続を管理する方法に柔軟性がもたらされます。

変更の背景

Goの database/sql パッケージは、データベースとのやり取りを抽象化し、接続プールを内部で管理しています。しかし、このコミット以前は、アイドル状態の接続をプールに保持する最大数(maxIdleConns)がハードコードされており、開発者がこの値を変更する手段がありませんでした。

問題点としては、以下の点が挙げられます。

  1. リソースの非効率な利用: デフォルトのアイドル接続数がアプリケーションの負荷やデータベースの特性に合わない場合、不要な接続が長時間保持されたり、逆に接続の再確立が頻繁に発生したりして、リソースの無駄やパフォーマンスの低下を招く可能性がありました。
  2. データベース側の制約: データベースによっては、同時に保持できる接続数に上限がある場合があります。アプリケーションがその上限を超えてアイドル接続を保持しようとすると、エラーが発生したり、データベースのパフォーマンスに悪影響を与えたりする可能性がありました。
  3. パフォーマンスの最適化の欠如: アプリケーションの特性(例: 短期間に大量のクエリを実行するバッチ処理、長時間接続を維持するサービスなど)に応じて、アイドル接続数を調整することで、接続の確立・切断にかかるオーバーヘッドを削減し、全体的なパフォーマンスを向上させることができます。

このコミットは、これらの課題を解決するために、開発者がプログラムからアイドル接続数を動的に設定できるようにする SetMaxIdleConns メソッドを導入しました。これは、GoのIssue #4805 に対応するものです。

前提知識の解説

データベース接続プール (Connection Pool)

データベース接続プールは、アプリケーションがデータベースに接続する際に、接続の確立と切断にかかるオーバーヘッドを削減するための技術です。アプリケーションがデータベースにアクセスするたびに新しい接続を確立するのではなく、事前に一定数の接続を作成し、それらをプール(貯蔵庫)に保持しておきます。

  • 利点:
    • パフォーマンス向上: 接続の確立はコストの高い操作であるため、これを再利用することで応答時間を短縮します。
    • リソース管理: データベースへの同時接続数を制限し、データベースのリソース枯渇を防ぎます。
    • スケーラビリティ: 多数のリクエストを効率的に処理できるようになります。

アイドル接続 (Idle Connections)

接続プール内の接続には、使用中の接続とアイドル状態の接続があります。アイドル接続とは、現在どのアプリケーションリクエストにも使用されていないが、プール内に保持されており、将来のリクエストのために再利用できる状態にある接続のことです。

  • maxIdleConns: アイドル接続プールに保持できるアイドル接続の最大数です。この値を超えると、最も古いアイドル接続が閉じられます。
  • maxOpenConns: 接続プール全体で同時に開いておくことができる接続の最大数です。これには使用中の接続とアイドル接続の両方が含まれます。

これらの設定は、アプリケーションの負荷パターン、データベースの性能、ネットワークのレイテンシなどを考慮して適切に調整する必要があります。

Goの database/sql パッケージ

database/sql パッケージは、Go言語でSQLデータベースを操作するための汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、driver.Driver インターフェースを実装する外部のデータベースドライバと組み合わせて使用されます。

  • DB 構造体: データベースへの接続プールを管理する主要な構造体です。
  • Open 関数: データベースドライバ名とデータソース名(DSN)を指定して DB オブジェクトを返します。
  • Query, Exec などのメソッド: SQLクエリの実行に使用されます。

技術的詳細

このコミットでは、database/sql パッケージの DB 構造体と関連するメソッドに以下の変更が加えられています。

  1. DB 構造体への maxIdle フィールドの追加: DB 構造体に maxIdle int フィールドが追加されました。

    • zero means defaultMaxIdleConns: maxIdle0 の場合、デフォルトのアイドル接続数 (defaultMaxIdleConns、このコミットでは 2) が使用されます。
    • negative means 0: maxIdle が負の値の場合、アイドル接続は保持されません(つまり、アイドル接続数は 0 となります)。
    • 正の値は、設定された最大アイドル接続数を示します。
  2. maxIdleConnsLocked() メソッドの導入: 既存の maxIdleConns() 関数が maxIdleConnsLocked() に変更され、DB 構造体の maxIdle フィールドの値を考慮して、実際に使用される最大アイドル接続数を返すようになりました。

    • n == 0 の場合: defaultMaxIdleConns (2) を返します。
    • n < 0 の場合: 0 を返します。
    • それ以外の場合 (n > 0): n の値をそのまま返します。
  3. SetMaxIdleConns(n int) メソッドの追加: DB 構造体に新しいパブリックメソッド SetMaxIdleConns が追加されました。このメソッドは、アイドル接続プールに保持する最大接続数を設定します。

    • メソッドは db.mu.Lock()defer db.mu.Unlock() を使用して、DB 構造体のミューテックスをロックし、並行アクセスから保護します。
    • 引数 n0 より大きい場合、db.maxIdlen が設定されます。
    • 引数 n0 以下の場合、db.maxIdle-1 が設定されます。これは maxIdleConnsLocked() メソッドによって 0 として解釈され、アイドル接続が保持されないことを意味します。
    • 設定後、既存のアイドル接続数が新しい maxIdle の値を超えている場合、超過した接続は閉じられます。これは、db.freeConn スライスから接続を取り出し、それぞれの driverConnClose() メソッドをゴルーチン内で呼び出すことで行われます。これにより、接続のクローズ処理がブロックされず、非同期に行われます。
  4. putConn メソッドの変更: putConn メソッド内でアイドル接続数をチェックする箇所が、db.maxIdleConns() から db.maxIdleConnsLocked() に変更されました。これにより、SetMaxIdleConns で設定された新しい最大アイドル接続数が適切に反映されるようになります。

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

src/pkg/database/sql/sql.go

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -197,6 +197,7 @@ type DB struct {
 	dep       map[finalCloser]depSet
 	onConnPut map[*driverConn][]func() // code (with mu held) run when conn is next returned
 	lastPut   map[*driverConn]string   // stacktrace of last conn's put; debug only
+	maxIdle   int                      // zero means defaultMaxIdleConns; negative means 0
 }
 
 // driverConn wraps a driver.Conn with a mutex, to
@@ -332,11 +333,45 @@ func (db *DB) Close() error {
 	return err
 }
 
-func (db *DB) maxIdleConns() int {
-	const defaultMaxIdleConns = 2
-	// TODO(bradfitz): ask driver, if supported, for its default preference
-	// TODO(bradfitz): let users override?
-	return defaultMaxIdleConns
+const defaultMaxIdleConns = 2
+
+func (db *DB) maxIdleConnsLocked() int {
+	n := db.maxIdle
+	switch {
+	case n == 0:
+		// TODO(bradfitz): ask driver, if supported, for its default preference
+		return defaultMaxIdleConns
+	case n < 0:
+		return 0
+	default:
+		return n
+	}
+}
+
+// SetMaxIdleConns sets the maximum number of connections in the idle
+// connection pool.
+//
+// If n <= 0, no idle connections are retained.
+func (db *DB) SetMaxIdleConns(n int) {
+	db.mu.Lock()
+	defer db.mu.Unlock()
+	if n > 0 {
+		db.maxIdle = n
+	} else {
+		// No idle connections.
+		db.maxIdle = -1
+	}
+	for len(db.freeConn) > 0 && len(db.freeConn) > n {
+		nfree := len(db.freeConn)
+		dc := db.freeConn[nfree-1]
+		db.freeConn[nfree-1] = nil
+		db.freeConn = db.freeConn[:nfree-1]
+		go func() {
+			dc.Lock()
+			dc.ci.Close()
+			dc.Unlock()
+		}()
+	}
 }
 
 // conn returns a newly-opened or cached *driverConn
@@ -441,7 +476,7 @@ func (db *DB) putConn(dc *driverConn, err error) {
 	if putConnHook != nil {
 		putConnHook(db, dc)
 	}\n-\tif n := len(db.freeConn); !db.closed && n < db.maxIdleConns() {
+\tif n := len(db.freeConn); !db.closed && n < db.maxIdleConnsLocked() {
 		db.freeConn = append(db.freeConn, dc)
 		db.mu.Unlock()
 		return

src/pkg/database/sql/sql_test.go

--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -761,3 +761,32 @@ func TestSimultaneousQueries(t *testing.T) {
 	}\n \tdefer r2.Close()\n }\n+\n+func TestMaxIdleConns(t *testing.T) {\n+\tdb := newTestDB(t, "people")\n+\tdefer closeDB(t, db)\n+\n+\ttx, err := db.Begin()\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\ttx.Commit()\n+\tif got := len(db.freeConn); got != 1 {\n+\t\tt.Errorf("freeConns = %d; want 1", got)\n+\t}\n+\n+\tdb.SetMaxIdleConns(0)\n+\n+\tif got := len(db.freeConn); got != 0 {\n+\t\tt.Errorf("freeConns after set to zero = %d; want 0", got)\n+\t}\n+\n+\ttx, err = db.Begin()\n+\tif err != nil {\n+\t\tt.Fatal(err)\n+\t}\n+\ttx.Commit()\n+\tif got := len(db.freeConn); got != 0 {\n+\t\tt.Errorf("freeConns = %d; want 0", got)\n+\t}\n+}\n```

## コアとなるコードの解説

### `DB` 構造体への `maxIdle` フィールドの追加

`DB` 構造体はデータベース接続プール全体の状態を管理します。`maxIdle` フィールドの追加により、この構造体がアイドル接続数の設定値を保持できるようになりました。

```go
type DB struct {
	// ... 既存のフィールド ...
	maxIdle   int                      // zero means defaultMaxIdleConns; negative means 0
}

maxIdleConnsLocked() メソッド

このメソッドは、DB 構造体の maxIdle フィールドの値に基づいて、実際に使用されるアイドル接続の最大数を決定します。これにより、デフォルト値、ゼロ、またはユーザーが設定した値のいずれかを柔軟に適用できます。

const defaultMaxIdleConns = 2

func (db *DB) maxIdleConnsLocked() int {
	n := db.maxIdle
	switch {
	case n == 0:
		// TODO(bradfitz): ask driver, if supported, for its default preference
		return defaultMaxIdleConns
	case n < 0:
		return 0
	default:
		return n
	}
}

SetMaxIdleConns(n int) メソッド

このメソッドは、ユーザーがアイドル接続数を設定するための主要なインターフェースです。ミューテックス (db.mu) を使用してスレッドセーフティを確保し、設定値に応じて db.maxIdle を更新します。特に重要なのは、新しい設定値が現在のアイドル接続数よりも小さい場合に、余分なアイドル接続を閉じるロジックです。これにより、設定変更が即座に反映され、リソースが適切に解放されます。

func (db *DB) SetMaxIdleConns(n int) {
	db.mu.Lock()
	defer db.mu.Unlock()
	if n > 0 {
		db.maxIdle = n
	} else {
		// No idle connections.
		db.maxIdle = -1
	}
	// 現在のアイドル接続数が新しい最大値を超えている場合、余分な接続を閉じる
	for len(db.freeConn) > 0 && len(db.freeConn) > n {
		nfree := len(db.freeConn)
		dc := db.freeConn[nfree-1] // 最も新しいアイドル接続を取得
		db.freeConn[nfree-1] = nil
		db.freeConn = db.freeConn[:nfree-1] // スライスから削除
		go func() { // ゴルーチンで非同期に接続を閉じる
			dc.Lock()
			dc.ci.Close() // 実際のデータベース接続を閉じる
			dc.Unlock()
		}()
	}
}

putConn メソッドの変更

putConn メソッドは、使用済みの接続をプールに戻す際に呼び出されます。この変更により、接続をプールに戻す際に、SetMaxIdleConns で設定された新しい最大アイドル接続数が考慮されるようになりました。

// ...
	if n := len(db.freeConn); !db.closed && n < db.maxIdleConnsLocked() {
		db.freeConn = append(db.freeConn, dc)
		db.mu.Unlock()
		return
// ...

TestMaxIdleConns テストケース

このテストケースは、SetMaxIdleConns メソッドが正しく機能することを確認します。

  1. newTestDB でデータベース接続を作成し、トランザクションを開始・コミットすることで、1つのアイドル接続がプールに存在することを確認します。
  2. db.SetMaxIdleConns(0) を呼び出し、アイドル接続数をゼロに設定します。これにより、既存のアイドル接続が閉じられ、プールが空になることを確認します。
  3. 再度トランザクションを開始・コミットし、アイドル接続数が依然としてゼロであることを確認します。これは、SetMaxIdleConns(0) が新しい接続がプールされるのを防ぐことを意味します。
func TestMaxIdleConns(t *testing.T) {
	db := newTestDB(t, "people")
	defer closeDB(t, db)

	// 1. トランザクションで接続を使用し、プールに1つのアイドル接続があることを確認
	tx, err := db.Begin()
	if err != nil {
		t.Fatal(err)
	}
	tx.Commit()
	if got := len(db.freeConn); got != 1 {
		t.Errorf("freeConns = %d; want 1", got)
	}

	// 2. SetMaxIdleConns(0) を呼び出し、アイドル接続が閉じられることを確認
	db.SetMaxIdleConns(0)

	if got := len(db.freeConn); got != 0 {
		t.Errorf("freeConns after set to zero = %d; want 0", got)
	}

	// 3. 再度トランザクションで接続を使用し、アイドル接続がプールされないことを確認
	tx, err = db.Begin()
	if err != nil {
		t.Fatal(err)
	}
	tx.Commit()
	if got := len(db.freeConn); got != 0 {
		t.Errorf("freeConns = %d; want 0", got)
	}
}

関連リンク

参考にした情報源リンク