[インデックス 17448] ファイルの概要
このコミットは、Go言語の database/sql
パッケージに SetMaxOpenConns
関数を追加し、データベースへの同時オープン接続数の上限を設定できるようにするものです。これにより、アプリケーションがデータベースに確立できる接続数を制御し、リソースの過負荷を防ぐことが可能になります。また、アイドル接続プールをスライスベースからリストベース(container/list
)に置き換え、Conn finalCloser
が db.mu
ロック中に呼び出される問題を修正しています。
コミット
commit 41c5d8d85f6c031c591c26404a5009a333d5d974
Author: Tad Glines <tad.glines@gmail.com>
Date: Fri Aug 30 09:27:33 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 separate 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/41c5d8d85f6c031c591c26404a5009a333d5d974
元コミット内容
このコミットは、Goの database/sql
パッケージに以下の主要な変更を導入します。
SetMaxOpenConns
の追加: データベースへのオープン接続数の上限を設定する機能を追加します。- Issue #4805 の更新: 接続プールに関する既存の課題を解決します。
Conn finalCloser
の修正:db.mu
(データベースミューテックス) がロックされている状態でConn finalCloser
が呼び出されるケースを修正します。- アイドルプール実装の変更: アイドル接続プールをスライスベースからリストベース(
container/list
)に置き換えます。 - ベンチマークの追加:
Exec
およびQuery
の各パスに対して個別のベンチマークを追加します。
変更の背景
このコミットの背景には、Goの database/sql
パッケージにおけるデータベース接続管理の課題がありました。特に、Issue #4805「Control over connection pool」で指摘されていたように、sql.Open()
関数がすぐにデータベース接続を確立せず、実際の接続が最初の操作まで遅延されること、そして接続プールに対するより明示的な制御の必要性が挙げられていました。
従来の database/sql
パッケージでは、オープン接続数に上限がなかったため、アプリケーションが大量の同時リクエストを処理する際に、データベースサーバーに過剰な接続を確立し、データベースのリソースを枯渇させる可能性がありました。これは「too many connections」エラーやデータベースのパフォーマンス低下を引き起こす原因となります。
また、アイドル接続プールがスライスで実装されていたため、接続の追加や削除の際に効率が低下する可能性がありました。特に、多数の接続が頻繁にプールに出入りするような高負荷なシナリオでは、スライスの再割り当てや要素の移動がオーバーヘッドとなることが考えられます。
さらに、Conn finalCloser
が db.mu
ロック中に呼び出されるという問題は、デッドロックや競合状態を引き起こす可能性があり、接続のクリーンアッププロセスに悪影響を与える可能性がありました。これらの問題を解決し、より堅牢で効率的なデータベース接続管理を実現するために、このコミットが導入されました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の database/sql
パッケージに関する概念と、一般的な並行処理の知識が必要です。
database/sql
パッケージ: Go言語の標準ライブラリの一部で、SQLデータベースとの対話のための汎用インターフェースを提供します。特定のデータベースドライバー(例:github.com/lib/pq
for PostgreSQL)と組み合わせて使用されます。- データベース接続プール (Connection Pool): データベースへの接続はリソースを消費し、確立に時間がかかるため、アプリケーションは通常、接続を再利用するために接続プールを使用します。接続プールは、使用可能な接続のキャッシュを維持し、必要に応じてアプリケーションに提供します。これにより、接続の確立と切断のオーバーヘッドが削減され、パフォーマンスが向上します。
sql.DB
:database/sql
パッケージの中心的な構造体で、データベースへの抽象的なアクセスを提供します。これはデータベースへの単一の論理的な接続を表し、内部的に接続プールを管理します。sql.DB
オブジェクトはアプリケーションのライフサイクル全体で維持されるべきであり、頻繁に開閉すべきではありません。SetMaxIdleConns(n int)
: アイドル接続プールに保持されるアイドル状態の接続の最大数を設定します。n <= 0
の場合、アイドル接続は保持されません。SetMaxOpenConns(n int)
: このコミットで追加された関数で、データベースへのオープン接続(使用中およびアイドル状態の接続の合計)の最大数を設定します。n <= 0
の場合、オープン接続数に制限はありません(デフォルト)。この制限に達すると、新しいデータベース操作は既存の接続が利用可能になるまで待機します。driver.Driver
インターフェース:database/sql
パッケージは、特定のデータベースドライバーが実装すべきインターフェースを定義しています。driver.Driver
はOpen(name string) (Conn, error)
メソッドを持ち、データベースへの新しい接続を確立します。driver.Conn
インターフェース: データベースへの単一の接続を表します。sync.Mutex
: Go言語のsync
パッケージで提供されるミューテックス(相互排他ロック)です。共有リソースへのアクセスを同期するために使用され、複数のゴルーチンが同時に同じデータにアクセスするのを防ぎます。db.mu
はsql.DB
構造体内の共有フィールドを保護するために使用されます。finalCloser
: Goのランタイムがオブジェクトが不要になったときに呼び出すことができるクリーンアップ関数です。ここでは、driverConn
がクローズされる際に呼び出される関数を指します。container/list
パッケージ: Go言語の標準ライブラリの一部で、双方向リンクリストを実装しています。要素の追加や削除が効率的で、特にリストの途中からの要素削除や、先頭・末尾への頻繁な追加・削除に適しています。スライスと比較して、要素の移動によるオーバーヘッドが少ないという利点があります。
技術的詳細
このコミットの技術的な詳細は、主に database/sql
パッケージ内の DB
構造体とその関連メソッドの変更に集中しています。
-
SetMaxOpenConns
の導入:DB
構造体にmaxOpen int
フィールドが追加されました。これはオープン接続数の上限を保持します。SetMaxOpenConns(n int)
メソッドが追加され、このmaxOpen
フィールドを設定できるようになりました。n <= 0
の場合は無制限となります。SetMaxOpenConns
が呼び出された際、もしmaxIdleConns
がmaxOpen
より大きい場合、maxIdleConns
はmaxOpen
に合わせて自動的に削減されます。これは、アイドル接続数がオープン接続数の上限を超えることを防ぐためです。
-
アイドル接続プールの変更 (Slice to
container/list
):- 従来の
DB
構造体のfreeConn
フィールドは[]*driverConn
(スライス) でしたが、*list.List
(双方向リンクリスト) に変更されました。 container/list
を使用することで、接続の追加 (PushFront
) や削除 (Remove
) がより効率的になります。スライスの場合、中間要素の削除や先頭への追加は、後続の要素の移動を伴うため、パフォーマンスのボトルネックになる可能性がありました。リンクリストでは、ポインタの付け替えだけで済むため、これらの操作が定数時間で行えます。driverConn
構造体には、listElem *list.Element
フィールドが追加され、freeConn
リスト内の自身の要素への参照を保持するようになりました。これにより、リストからの接続の削除が容易になります。
- 従来の
-
接続要求と接続オープナーの導入:
DB
構造体にconnRequests *list.List
(接続要求のキュー)、numOpen int
(現在オープンしている接続数)、pendingOpens int
(現在開いている途中の接続数)、openerCh chan struct{}
(接続オープナーへのシグナルチャネル) が追加されました。connectionOpener()
という新しいゴルーチンが導入されました。このゴルーチンはopenerCh
からのシグナルを受け取り、新しい接続を非同期に開きます。maybeOpenNewConnections()
メソッドが追加され、db.mu
がロックされている状態で、接続要求がある場合や接続上限に達していない場合に、openerCh
にシグナルを送って新しい接続を開くように促します。connRequest
型が定義され、新しい接続を待つゴルーチンが使用するチャネルを表します。db.conn()
メソッドが変更され、maxOpen
の制限がある場合やアイドル接続がない場合に、connRequests
リストに接続要求を追加し、connectionOpener
が接続を開くのを待つようになりました。これにより、接続上限を超えて接続が確立されるのを防ぎ、接続が利用可能になるまでリクエストをブロックします。
-
Conn finalCloser
のdb.mu
ロック問題の修正:driverConn.closeDBLocked()
メソッドのロジックが変更されました。以前はdb.mu
がロックされた状態でdb.removeDepLocked
を直接呼び出していましたが、新しい実装ではdb.removeDepLocked
が返す関数をdb.mu
のロックを解除した後に呼び出すように変更されました。driverConn.finalClose()
メソッドも修正され、db.mu
のロックを解除した後にdb.numOpen--
とdb.maybeOpenNewConnections()
を呼び出すようになりました。これにより、finalCloser
の実行中にdb.mu
が不必要にロックされることを防ぎ、デッドロックのリスクを軽減します。
-
putConnDBLocked
の導入:putConnDBLocked
という新しいヘルパー関数が導入されました。この関数は、接続要求を処理するか、アイドル接続プールに接続を戻す役割を担います。これにより、接続の解放と再利用のロジックが一元化され、db.mu
のロック管理がより適切に行われるようになりました。
これらの変更により、database/sql
パッケージはより堅牢な接続プール管理機能を提供し、高負荷な環境でのパフォーマンスと安定性を向上させています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/database/sql/sql.go
ファイルに集中しています。
-
DB
構造体の変更:// Before type DB struct { driver driver.Driver dsn string mu sync.Mutex // protects following fields freeConn []*driverConn closed bool dep map[finalCloser]depSet lastPut map[*driverConn]string // stacktrace of last conn's put; debug only maxIdle int // zero means defaultMaxIdleConns; negative means 0 } // After type DB struct { driver driver.Driver dsn string mu sync.Mutex // protects following fields freeConn *list.List // of *driverConn <-- Changed from slice to list connRequests *list.List // of connRequest <-- New numOpen int // <-- New pendingOpens int // <-- New openerCh chan struct{} // <-- New closed bool dep map[finalCloser]depSet lastPut map[*driverConn]string // stacktrace of last conn's put; debug only maxIdle int // zero means defaultMaxIdleConns; negative means 0 maxOpen int // <= 0 means unlimited <-- New }
-
driverConn
構造体の変更:// Before (relevant part) type driverConn struct { // ... } // After (relevant part) type driverConn struct { // ... listElem *list.Element // <-- New: Reference to its element in the freeConn list }
-
Open
関数の変更:DB
構造体の初期化時に、freeConn
をlist.New()
で初期化し、connRequests
もlist.New()
で初期化、そしてconnectionOpener()
ゴルーチンを起動するようになりました。// Before (relevant part) func Open(driverName, dataSourceName string) (*DB, error) { // ... db := &DB{ driver: driveri, dsn: dataSourceName, lastPut: make(map[*driverConn]string), } return db, nil } // After (relevant part) func Open(driverName, dataSourceName string) (*DB, error) { // ... db := &DB{ driver: driveri, dsn: dataSourceName, openerCh: make(chan struct{}, connectionRequestQueueSize), // <-- New lastPut: make(map[*driverConn]string), } db.freeConn = list.New() // <-- New db.connRequests = list.New() // <-- New go db.connectionOpener() // <-- New return db, nil }
-
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) // Adjust maxIdle if it exceeds new maxOpen } }
-
connectionOpener
およびmaybeOpenNewConnections
の追加: 新しいゴルーチンとヘルパー関数が追加され、接続の非同期オープンと接続要求の処理を管理します。 -
db.conn()
メソッドの変更: 接続の取得ロジックが変更され、maxOpen
の制限とconnRequests
を考慮するようになりました。 -
driverConn.finalClose()
の変更:db.mu
のロックを解除した後にdb.numOpen--
とdb.maybeOpenNewConnections()
を呼び出すようになりました。// Before (relevant part) func (dc *driverConn) finalClose() error { // ... dc.Unlock() return err } // After (relevant part) func (dc *driverConn) finalClose() error { // ... dc.Unlock() dc.db.mu.Lock() // <-- New: Lock db.mu dc.db.numOpen-- // <-- New dc.db.maybeOpenNewConnections() // <-- New dc.db.mu.Unlock() // <-- New: Unlock db.mu return err }
-
putConnDBLocked
ヘルパー関数の追加: 接続をアイドルプールに戻すか、接続要求を満たすための新しいロジックが追加されました。
これらの変更は、database/sql
パッケージの接続管理メカニズムを根本的に改善し、より柔軟で堅牢な接続プール機能を提供します。
コアとなるコードの解説
DB
構造体の変更と接続プールの再設計
最も重要な変更は、DB
構造体に maxOpen
フィールドが追加され、freeConn
がスライスから *list.List
に変更されたことです。
maxOpen int
: このフィールドは、データベースへの同時にオープンできる接続の最大数を保持します。SetMaxOpenConns
メソッドを通じて設定され、0
または負の値は無制限を意味します。これにより、アプリケーションはデータベースへの接続数を厳密に制御し、データベースサーバーの過負荷を防ぐことができます。freeConn *list.List
: アイドル状態の接続を保持するプールが、[]*driverConn
(スライス) から*list.List
(双方向リンクリスト) に変更されました。- スライスの問題点: スライスは連続したメモリブロックを使用するため、要素の追加や削除(特に中間からの削除)は、後続の要素の移動を伴い、パフォーマンスのオーバーヘッドが発生する可能性があります。接続プールでは、接続の取得(リストの先頭からの削除)や解放(リストの末尾への追加)が頻繁に行われるため、スライスの再割り当てや要素の移動がボトルネックになる可能性がありました。
- リンクリストの利点:
container/list
は、各要素が前後の要素へのポインタを持つため、要素の追加や削除がポインタの付け替えだけで済み、定数時間O(1)
で実行できます。これにより、接続プールの操作がより効率的になり、高負荷な環境でのパフォーマンスが向上します。
connRequests *list.List
: 新しく追加されたこのリストは、新しい接続を待っているゴルーチンからの要求(connRequest
)をキューとして保持します。maxOpen
の制限に達している場合や、アイドル接続がない場合に、接続を必要とするゴルーチンはここに要求を登録し、接続が利用可能になるまで待機します。numOpen int
とpendingOpens int
:numOpen
は現在オープンしている(使用中またはアイドル状態の)接続の総数を追跡します。pendingOpens
は、現在開いている途中の接続の数を追跡し、接続オープナーが新しい接続を確立している間、一時的にカウントされます。これにより、接続数の上限を正確に管理できます。openerCh chan struct{}
: これは、connectionOpener
ゴルーチンに新しい接続を開くようにシグナルを送るためのチャネルです。maybeOpenNewConnections
関数がこのチャネルに値を送信することで、非同期的に接続の確立をトリガーします。
SetMaxOpenConns
メソッド
このメソッドは、DB
構造体の maxOpen
フィールドを設定し、オープン接続数の上限を定義します。
func (db *DB) SetMaxOpenConns(n int) {
db.mu.Lock()
db.maxOpen = n
if n < 0 {
db.maxOpen = 0 // 負の値は無制限を意味するため、0に設定
}
// maxOpenが設定され、かつmaxIdleConnsがmaxOpenを超える場合、maxIdleConnsを調整
syncMaxIdle := db.maxOpen > 0 && db.maxIdleConnsLocked() > db.maxOpen
db.mu.Unlock()
if syncMaxIdle {
db.SetMaxIdleConns(n) // maxIdleConnsをmaxOpenに合わせる
}
}
この関数は、db.mu
をロックして maxOpen
を安全に更新します。また、maxIdleConns
が新しい maxOpen
の値を超えないように自動的に調整するロジックが含まれています。これは、アイドル接続数がオープン接続数の上限よりも多くなるという矛盾した状態を防ぐために重要です。
connectionOpener
ゴルーチンと接続要求の処理
connectionOpener
は、Open
関数で起動される独立したゴルーチンです。
// Runs in a seperate goroutine, opens new connections when requested.
func (db *DB) connectionOpener() {
for _ = range db.openerCh { // openerChからのシグナルを待機
db.openNewConnection() // 新しい接続を開く
}
}
このゴルーチンは db.openerCh
からのシグナルを継続的にリッスンし、シグナルを受け取るたびに db.openNewConnection()
を呼び出して新しいデータベース接続を確立します。これにより、接続の確立が非同期で行われ、メインの処理がブロックされるのを防ぎます。
db.conn()
メソッドは、接続が必要な場合にこの新しい接続管理ロジックを利用します。
func (db *DB) conn() (*driverConn, error) {
db.mu.Lock()
if db.closed {
db.mu.Unlock()
return nil, errDBClosed
}
// 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) {
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
}
このロジックにより、maxOpen
の制限が尊重され、接続が不足している場合には接続要求がキューに入れられ、connectionOpener
によって非同期に処理されるようになります。
Conn finalCloser
の db.mu
ロック問題の修正
driverConn.finalClose()
メソッドの変更は、デッドロックのリスクを軽減するために重要です。
func (dc *driverConn) finalClose() error {
// ... (既存のクローズ処理)
dc.Unlock() // driverConnのロックを解除
// db.muをロックして、DBの状態を更新
dc.db.mu.Lock()
dc.db.numOpen-- // オープン接続数をデクリメント
dc.db.maybeOpenNewConnections() // 新しい接続が必要かチェック
dc.db.mu.Unlock() // db.muのロックを解除
return err
}
以前は、finalCloser
が db.mu
がロックされた状態で呼び出される可能性があり、これがデッドロックの原因となることがありました。この修正では、driverConn
のクローズ処理が完了し、driverConn
のミューテックスが解除された後に、db.mu
をロックして db.numOpen
の更新と maybeOpenNewConnections
の呼び出しを行うように変更されました。これにより、db.mu
のロックが短時間になり、他のゴルーチンとの競合が減少します。
これらの変更は、Goの database/sql
パッケージの接続管理を大幅に改善し、より堅牢でスケーラブルなデータベースアプリケーションの開発を可能にします。
関連リンク
- Go Issue #4805: https://go.dev/issue/4805
参考にした情報源リンク
- https://go.dev/doc/database/sql
- https://dev.to/douglasmiranda/go-database-sql-connection-pool-300k
- https://medium.com/@vladimir.gorej/go-database-sql-connection-pool-explained-2023-10-26-a72121212121
- https://alexedwards.net/blog/configuring-go-sql-driver
- https://studyraid.com/go-database-sql-connection-pool-explained/
- https://go-database-sql.org/connections.html
- https://medium.com/@vladimir.gorej/go-database-sql-connection-pool-explained-2023-10-26-a72121212121
- https://turso.tech/blog/go-database-sql-connection-pool-explained
- https://stackoverflow.com/questions/60716420/go-sql-rows-close-deadlock
- https://github.com/golang/go/issues/37178
- https://github.com/golang/go/issues/54110
- https://medium.com/@vladimir.gorej/go-database-sql-connection-pool-explained-2023-10-26-a72121212121
- https://narkive.com/go-nuts/go-database-sql-connection-pool-management-and-deadlocks-gq0g.html
- https://boyter.org/2023/01/go-sqlite-database-is-locked-errors/
- https://koho.dev/posts/go-database-sql-connection-pool
- https://medium.com/@vladimir.gorej/go-database-sql-connection-pool-explained-2023-10-26-a72121212121
- https://github.com/golang/go/commit/4f6d4bb
- https://medium.com/@vladimir.gorej/go-database-sql-connection-pool-explained-2023-10-26-a72121212121
- https://webreference.com/programming/go/database-sql-connection-pool-management