[インデックス 17440] ファイルの概要
このコミットは、Go言語の database/sql
パッケージにおける重要な機能追加と改善を導入しています。主な変更点は、データベースへの同時オープン接続数を制限する SetMaxOpenConns
関数の追加、アイドル接続プールの実装変更(スライスベースからリストベースへ)、そして並行処理テストとベンチマークの拡充です。これにより、データベース接続の管理がより柔軟かつ効率的になり、特に高負荷環境下での安定性とパフォーマンスが向上しています。
コミット
commit 4572e4848364b6098b565a2ef480e9b4e8ff5977
Author: Tad Glines <tad.glines@gmail.com>
Date: Thu Aug 29 17:20:39 2013 -0700
database/sql: add SetMaxOpenConns
Update #4805
Add the ability to set an open connection limit.
Fixed case where the Conn finalCloser was being called with db.mu locked.
Added seperate benchmarks for each path for Exec and Query.
Replaced slice based idle pool with list based idle pool.
R=bradfitz
CC=golang-dev
https://golang.org/cl/10726044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4572e4848364b6098b565a2ef480e9b4e8ff5977
元コミット内容
このコミットは、以下の主要な変更を含んでいます。
SetMaxOpenConns
の追加: データベースへのオープン接続数の上限を設定する機能が追加されました。これにより、アプリケーションが同時に開くデータベース接続の数を制御できるようになります。- Issue #4805 の解決: このコミットは、GoのIssueトラッカーで報告されていた問題 #4805 に対応しています。このIssueは、
database/sql
パッケージにおける接続管理の改善に関するものでした。 Conn finalCloser
のロック競合修正:Conn
のfinalCloser
がdb.mu
(データベースミューテックス) がロックされた状態で呼び出されるとデッドロックが発生する可能性があった問題が修正されました。- ベンチマークの拡充:
Exec
およびQuery
の各パスに対して、個別のベンチマークが追加されました。これにより、パフォーマンス特性のより詳細な分析が可能になります。 - アイドル接続プールの実装変更: アイドル状態の接続を管理するためのプールが、スライスベースの実装から
container/list
パッケージを使用したリストベースの実装に変更されました。
変更の背景
この変更の背景には、Goの database/sql
パッケージが持つデータベース接続管理の課題がありました。以前のバージョンでは、オープン接続数に明示的な上限を設定する機能がなく、アプリケーションが過剰な数の接続を開いてしまい、データベースサーバーに負荷をかけたり、リソースを枯渇させたりする可能性がありました。これは、特に高負荷なWebアプリケーションやサービスにおいて、パフォーマンスの低下や安定性の問題を引き起こす要因となっていました。
Issue #4805 は、この接続管理の改善、特にオープン接続数の制限機能の必要性を提起していました。また、既存のアイドル接続プールの実装がスライスベースであったため、接続の追加や削除の際に効率が悪いという問題も指摘されていました。スライスは要素の追加や削除が末尾以外で行われる場合に、要素の移動が発生し、パフォーマンスに影響を与える可能性があります。
このコミットは、これらの課題に対処し、database/sql
パッケージの堅牢性と効率性を向上させることを目的としています。SetMaxOpenConns
の導入により、開発者はデータベース接続のリソース消費をより細かく制御できるようになり、アイドル接続プールの改善は、接続の再利用効率を高め、全体的なパフォーマンスを向上させます。
前提知識の解説
Go言語の database/sql
パッケージ
database/sql
パッケージは、Go言語における標準的なSQLデータベース操作のための汎用インターフェースを提供します。このパッケージ自体は特定のデータベースドライバーを含まず、データベース固有の機能は database/sql/driver
インターフェースを実装する外部ドライバーによって提供されます。
主要な概念:
DB
構造体: データベースへのオープン接続のプールを表します。この構造体は、複数のゴルーチンから安全に利用できるように設計されており、接続の確立、解放、再利用を管理します。Conn
(driver.Conn): データベースへの単一の物理的な接続を表すインターフェースです。Stmt
(driver.Stmt): プリペアドステートメントを表すインターフェースです。SQLインジェクション攻撃を防ぎ、クエリの実行効率を向上させます。- 接続プール:
database/sql
パッケージは、データベース接続の再利用を目的とした接続プールを内部的に管理します。これにより、新しい接続を確立するオーバーヘッドを削減し、パフォーマンスを向上させます。アイドル接続(使用されていない接続)はプールに保持され、必要に応じて再利用されます。
データベース接続プール
データベース接続プールは、アプリケーションがデータベースに接続する際に発生するオーバーヘッドを削減するための一般的なパターンです。接続の確立は時間とリソースを消費する操作であるため、一度確立した接続を再利用することで、アプリケーションの応答性とスケーラビリティを向上させることができます。
接続プールは通常、以下の要素を管理します。
- 最大アイドル接続数 (MaxIdleConns): プール内でアイドル状態(使用されていない)で保持できる接続の最大数。この数を超えたアイドル接続は閉じられます。
- 最大オープン接続数 (MaxOpenConns): プール内で同時にオープンできる接続の最大数。これには、現在使用中の接続とアイドル接続の両方が含まれます。この制限に達すると、新しい接続要求は、既存の接続が解放されるまで待機します。
- 接続のライフタイム: 接続がプール内で保持される最大時間や、接続が再利用される前に検証される頻度など。
Goの container/list
パッケージ
container/list
パッケージは、Go言語で双方向リンクリストを実装するための標準ライブラリです。リンクリストは、要素が連続したメモリ位置に格納されるスライスとは異なり、各要素が次の要素(および前の要素)へのポインタを持つ構造です。
リンクリストの利点:
- 効率的な挿入と削除: リストの任意の位置での要素の挿入や削除が、O(1) の時間計算量で行えます。これは、スライスで中間要素を削除・挿入する際に発生する要素の移動(O(N))と比較して効率的です。
- 動的なサイズ変更: リストのサイズは動的に変更でき、事前に容量を確保する必要がありません。
リンクリストの欠点:
- ランダムアクセスが遅い: 特定のインデックスの要素にアクセスするには、リストの先頭から順にたどる必要があるため、O(N) の時間計算量がかかります。スライスではO(1)です。
- メモリオーバーヘッド: 各要素がポインタを持つため、スライスよりもメモリ使用量が多くなる可能性があります。
このコミットでは、アイドル接続プールをスライスからリンクリストに変更することで、接続の追加(プールへの返却)や削除(プールからの取得、または期限切れ接続のクローズ)の操作をより効率的に行うことを目指しています。
技術的詳細
SetMaxOpenConns
の実装
SetMaxOpenConns
関数は、DB
構造体に maxOpen
フィールドを追加することで実装されています。このフィールドは、同時にオープンできるデータベース接続の最大数を保持します。
新しい接続を確立する際、DB.conn()
メソッドは db.numOpen
(現在オープンしている接続数) と db.maxOpen
をチェックします。db.numOpen
が db.maxOpen
に達している場合、またはアイドル接続がない場合、接続要求は db.connRequests
という新しい list.List
に追加され、connectionOpener
ゴルーチンによって処理されるのを待ちます。
connectionOpener
は、db.openerCh
チャネルからのシグナルを受け取ると、新しい接続を開こうとします。このゴルーチンは、db.maxOpen
の制限と db.pendingOpens
(現在開かれている途中の接続数) を考慮して、同時に開かれる接続数が制限を超えないように調整します。
アイドル接続プールの変更
以前の DB
構造体では、アイドル接続は freeConn []*driverConn
というスライスで管理されていました。このコミットでは、これを freeConn *list.List
に変更し、container/list
パッケージの双方向リンクリストを使用するようにしました。
この変更により、putConnDBLocked
(接続をプールに戻す) や conn
(プールから接続を取得する) といった操作が、リストの先頭または末尾からの追加/削除として効率的に行えるようになりました。スライスベースの実装では、中間からの削除や挿入が要素の移動を伴い、パフォーマンスのボトルネックとなる可能性がありました。リンクリストを使用することで、これらの操作は定数時間 (O(1)) で実行できるようになります。
また、driverConn
構造体には listElem *list.Element
フィールドが追加され、freeConn
リスト内の自身の要素へのポインタを保持することで、リストからの削除を効率的に行えるようになっています。
finalCloser
のロック競合修正
driverConn.finalClose()
メソッドは、接続が完全にクローズされる際に呼び出されます。以前の実装では、このメソッドが db.mu
(DBのミューテックス) がロックされた状態で呼び出される可能性があり、デッドロックを引き起こす原因となっていました。
このコミットでは、dc.closeDBLocked()
が db.mu
を保持したまま dc.Unlock()
を呼び出すのではなく、func() error
を返すように変更されました。これにより、db.mu
が解放された後に実際のクローズ処理が実行されるようになり、ロック競合が回避されます。
ベンチマークとテストの拡充
sql_test.go
ファイルには、SetMaxOpenConns
の動作を検証するための新しいテストケース TestMaxOpenConns
が追加されました。このテストは、同時実行されるクエリの数を制御し、オープン接続数が設定された上限を超えないことを確認します。
さらに、Exec
と Query
の各パス、およびトランザクションやプリペアドステートメントを含む様々なシナリオでの並行処理をテストするための新しいベンチマーク関数が追加されました。これにより、database/sql
パッケージの並行処理性能がより詳細に評価できるようになりました。NOSERT
という新しい fakeStmt
コマンドが追加され、テスト中に実際のデータベース挿入を行わずに Exec
の準備作業をシミュレートできるようになっています。
コアとなるコードの変更箇所
src/pkg/database/sql/sql.go
DB
構造体の変更:freeConn []*driverConn
をfreeConn *list.List
に変更。connRequests *list.List
(接続要求キュー) を追加。numOpen int
(現在オープンしている接続数) を追加。pendingOpens int
(オープン中の接続数) を追加。openerCh chan struct{}
(接続オープナーゴルーチンへのシグナルチャネル) を追加。maxOpen int
(最大オープン接続数) を追加。
driverConn
構造体の変更:listElem *list.Element
(freeConnリスト内の自身の要素へのポインタ) を追加。
Open
関数の変更:db.freeConn
とdb.connRequests
をlist.New()
で初期化。db.openerCh
をバッファ付きチャネルとして初期化。db.connectionOpener()
ゴルーチンを起動。
Close
関数の変更:db.openerCh
をクローズし、connectionOpener
ゴルーチンを終了させる。freeConn
リスト内のすべての接続をクローズするロジックをリストベースに変更。
SetMaxIdleConns
関数の変更:db.freeConn
の操作をリストベースに変更。maxOpen
とmaxIdle
の整合性を保つロジックを追加。
SetMaxOpenConns
関数の追加:db.maxOpen
を設定し、maxIdle
との整合性を調整する。
maybeOpenNewConnections
関数の追加:db.connRequests
とdb.maxOpen
に基づいて、新しい接続を開く必要があるかどうかを判断し、openerCh
にシグナルを送る。
connectionOpener
関数の追加:openerCh
からのシグナルを受け取り、openNewConnection
を呼び出すゴルーチン。
openNewConnection
関数の追加:- 新しいデータベース接続を開き、
db.numOpen
を更新し、putConnDBLocked
を呼び出す。
- 新しいデータベース接続を開き、
connRequest
型の追加:- 接続要求を表すチャネル型。
conn
関数の変更:maxOpen
制限とconnRequests
を考慮した接続取得ロジックを追加。db.freeConn
からの接続取得をリストベースに変更。
putConn
関数の変更:putConnDBLocked
を呼び出すように変更。
putConnDBLocked
関数の追加:- 接続要求を満たすか、アイドルプールに接続を戻すロジック。
src/pkg/database/sql/sql_test.go
TestMaxOpenConns
の追加:SetMaxOpenConns
の動作を検証するテストケース。
TestConcurrency
の変更:- 様々な並行処理テストケース (
concurrentDBQueryTest
,concurrentDBExecTest
など) を追加し、doConcurrentTest
関数で実行するように変更。
- 様々な並行処理テストケース (
- ベンチマーク関数の追加:
BenchmarkConcurrentDBExec
,BenchmarkConcurrentStmtQuery
など、各操作パスに対する詳細なベンチマークを追加。
doConcurrentTest
関数の追加:- 並行処理テストを実行するためのヘルパー関数。
concurrentTest
インターフェースと関連するテスト構造体の追加:- 様々な種類の並行処理テストを抽象化するためのインターフェースと実装。
numFreeConns
の変更:len(db.freeConn)
からdb.freeConn.Len()
に変更。
src/pkg/database/sql/fakedb_test.go
NOSERT
コマンドの追加:fakeConn
とfakeStmt
にNOSERT
コマンドを追加。これはINSERT
と同様の準備作業を行うが、実際には行を挿入しない。並行処理テストで使用される。
コアとなるコードの解説
DB
構造体と接続管理の新しいメカニズム
type DB struct {
// ... 既存のフィールド ...
mu sync.Mutex // protects following fields
freeConn *list.List // of *driverConn (アイドル接続プール)
connRequests *list.List // of connRequest (接続要求キュー)
numOpen int // 現在オープンしている接続数
pendingOpens int // オープン中の接続数
openerCh chan struct{} // 接続オープナーゴルーチンへのシグナル
closed bool
// ... その他のフィールド ...
maxOpen int // <= 0 means unlimited (最大オープン接続数)
}
DB
構造体には、maxOpen
フィールドが追加され、オープン接続数の上限を管理します。freeConn
は container/list.List
に変更され、アイドル接続の効率的な追加・削除を可能にします。connRequests
は、接続が利用可能になるのを待っているリクエストをキューイングするために使用されます。numOpen
は現在アクティブな接続の総数を追跡し、pendingOpens
は現在確立中の接続の数を追跡します。openerCh
は、新しい接続を開く必要があることを connectionOpener
ゴルーチンに通知するためのチャネルです。
SetMaxOpenConns
関数
func (db *DB) SetMaxOpenConns(n int) {
db.mu.Lock()
db.maxOpen = n
if n < 0 {
db.maxOpen = 0 // 負の値は無制限として扱う
}
syncMaxIdle := db.maxOpen > 0 && db.maxIdleConnsLocked() > db.maxOpen
db.mu.Unlock()
if syncMaxIdle {
db.SetMaxIdleConns(n) // maxOpenがmaxIdleより小さい場合、maxIdleを調整
}
}
この関数は、db.maxOpen
の値を設定します。もし maxOpen
が 0
以下に設定された場合、接続数に制限はなくなります。また、maxOpen
が maxIdleConnsLocked()
(最大アイドル接続数) よりも小さくなった場合、SetMaxIdleConns
を呼び出して maxIdle
も調整し、maxIdle
が maxOpen
を超えないようにします。
conn
メソッドにおける接続取得ロジック
func (db *DB) conn() (*driverConn, error) {
db.mu.Lock()
if db.closed {
db.mu.Unlock()
return nil, errDBClosed
}
// maxOpenが設定されており、オープン接続数が上限に達しているか、アイドル接続がない場合
if db.maxOpen > 0 && (db.numOpen >= db.maxOpen || db.freeConn.Len() == 0) {
ch := make(chan interface{}, 1) // バッファ付きチャネル
req := connRequest(ch)
db.connRequests.PushBack(req) // 接続要求をキューに追加
db.maybeOpenNewConnections() // 新しい接続を開く必要があるかチェック
db.mu.Unlock()
ret, ok := <-ch // 接続が利用可能になるまで待機
if !ok {
return nil, errDBClosed
}
switch ret.(type) {
case *driverConn:
return ret.(*driverConn), nil
case error:
return nil, ret.(error)
default:
panic("sql: Unexpected type passed through connRequest.ch")
}
}
// アイドル接続がある場合、プールから取得
if f := db.freeConn.Front(); f != nil {
conn := f.Value.(*driverConn)
conn.listElem = nil // リスト要素への参照をクリア
db.freeConn.Remove(f)
conn.inUse = true
db.mu.Unlock()
return conn, nil
}
db.mu.Unlock() // ロックを解放してから新しい接続を開く
// 新しい接続を開く
ci, err := db.driver.Open(db.dsn)
if err != nil {
return nil, err
}
db.mu.Lock() // 再度ロック
db.numOpen++ // オープン接続数をインクリメント
dc := &driverConn{
db: db,
ci: ci,
}
db.addDepLocked(dc, dc)
dc.inUse = true
db.mu.Unlock()
return dc, nil
}
conn
メソッドは、接続を取得する際の中心的なロジックを含んでいます。maxOpen
が設定されている場合、numOpen
が maxOpen
に達しているか、アイドル接続がない場合は、接続要求を connRequests
キューに入れ、connectionOpener
ゴルーチンが新しい接続を開くのを待ちます。これにより、オープン接続数が上限を超えないように制御されます。アイドル接続がある場合は、freeConn
リストの先頭から取得されます。それ以外の場合は、新しい接続が確立されます。
putConnDBLocked
メソッド
func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
if db.connRequests.Len() > 0 { // 接続要求がある場合
req := db.connRequests.Front().Value.(connRequest)
db.connRequests.Remove(db.connRequests.Front())
if err != nil {
req <- err // エラーを要求元に通知
} else {
dc.inUse = true
req <- dc // 接続を要求元に渡す
}
return true
} else if err == nil && !db.closed && db.maxIdleConnsLocked() > 0 && db.maxIdleConnsLocked() > db.freeConn.Len() {
// アイドルプールに空きがあり、エラーがない場合、アイドルプールに戻す
dc.listElem = db.freeConn.PushFront(dc)
return true
}
return false // 接続要求がなく、アイドルプールにも戻せない場合
}
putConnDBLocked
は、使用済みの接続を処理する際に呼び出されます。もし接続を待っているリクエストがあれば、そのリクエストに接続(またはエラー)を渡します。そうでなければ、アイドル接続プールに空きがあり、かつエラーがない場合に、接続をプールに戻します。これにより、接続の再利用が促進されます。
関連リンク
- Go
database/sql
パッケージのドキュメント: https://pkg.go.dev/database/sql - Go
container/list
パッケージのドキュメント: https://pkg.go.dev/container/list - Go Issue #4805:
database/sql: add SetMaxOpenConns
(このコミットが解決したIssue) - 検索しても直接的なリンクは見つかりませんでしたが、コミットメッセージに記載されています。
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goのソースコード (特に
src/pkg/database/sql/
) - 一般的なデータベース接続プールの概念に関する情報
- Goの
container/list
パッケージに関する情報