[インデックス 16231] ファイルの概要
このコミットは、Go言語の database/sql
パッケージにおける重要なバグ修正であり、特にプリペアドステートメント(prepared statements)使用時のデータベース接続(driver.Conn
)の参照カウントの不具合を解消することを目的としています。このバグは、Go 1.0からの重大なリグレッションであり、高負荷環境下で接続がリークし、事実上永久に保持されてしまう問題を引き起こしていました。
コミット
commit 277047f52ae36f9364bf6d593931ee8732d96cb3
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Apr 25 14:45:56 2013 -0700
database/sql: fix driver Conn refcounting with prepared statements
The refcounting of driver Conns was completedly busted and
would leak (be held open forever) with any reasonable
load. This was a significant regression from Go 1.0.
The core of this patch is removing one line:
s.db.addDep(dc, s)
A database conn (dc) is a resource that be re-created any time
(but cached for speed) should not be held open forever with a
dependency refcount just because the Stmt (s) is alive (which
typically last for long periods of time, like forever).
The meat of the patch is new tests. In fixing the real issue,
a lot of tests then failed due to the fakedb_test.go's paranoia
about closing a fakeConn while it has open fakeStmts on it. I
could've ignored that, but that's been a problem in the past for
other bugs.
Instead, I now track per-Conn open statements and close them
when the the conn closes. The proper way to do this would've
been making *driverStmt a finalCloser and using the dep mechanism,
but it was much more invasive. Added a TODO instead.
I'd like to give a way for drivers to opt-out of caring about
driver.Stmt closes before a driver.Conn close, but that's a TODO
for the future, and that TODO is added in this CL.
I know this is very late for Go 1.1, but database/sql is
currently nearly useless without this.
I'd like to believe all these database/sql bugs in the past
release cycle are the result of increased usage, number of
drivers, and good feedback from increasingly-capable Go
developers, and not the result of me sucking. It's also hard
with all the real drivers being out-of-tree, so I'm having to
add more and more hooks to fakedb_test.go to simulate things
which real drivers end up doing.
Fixes #5323
R=golang-dev, snaury, gwenn.kahz, google, r
CC=golang-dev
https://golang.org/cl/8836045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/277047f52ae36f9364bf6d593931ee8732d96cb3
元コミット内容
database/sql
: プリペアドステートメントにおける driver.Conn
の参照カウントの修正。
driver.Conn
の参照カウントが完全に壊れており、合理的な負荷で接続がリーク(永久に保持される)していた。これはGo 1.0からの重大なリグレッションである。
このパッチの核心は、s.db.addDep(dc, s)
という1行を削除することである。データベース接続(dc
)はいつでも再作成できるリソースであり(ただし速度のためにキャッシュされる)、Stmt
(s
)が生きている(通常は長期間、例えば永久に生きている)という理由だけで、依存関係の参照カウントによって永久に保持されるべきではない。
パッチの大部分は新しいテストである。実際の問題を修正する中で、fakedb_test.go
の「開いている fakeStmt
がある間に fakeConn
を閉じることに対する厳格なチェック」のために多くのテストが失敗した。これを無視することもできたが、それは過去に他のバグの原因となっていた。
代わりに、接続ごとに開いているステートメントを追跡し、接続が閉じるときにそれらを閉じるようにした。これを適切に行うには *driverStmt
を finalCloser
にして dep
メカニズムを使うべきだったが、それはより侵襲的だったため、代わりにTODOを追加した。
ドライバーが driver.Conn
を閉じる前に driver.Stmt
のクローズを気にしないようにする方法を提供したいが、それは将来のTODOであり、この変更リスト(CL)でそのTODOが追加された。
Go 1.1には非常に遅い変更だが、これがないと database/sql
は現在ほとんど役に立たない。
過去のリリースサイクルにおける database/sql
のバグは、使用量の増加、ドライバーの数の増加、そしてますます有能になるGo開発者からの良いフィードバックの結果であり、自分の能力不足の結果ではないと信じたい。また、すべての実際のドライバーがツリー外にあるため、実際のドライバーが行うことをシミュレートするために fakedb_test.go
にどんどんフックを追加しなければならないのも難しい。
Fixes #5323
変更の背景
このコミットは、Go言語の標準ライブラリである database/sql
パッケージが抱えていた深刻な接続リーク問題に対処するために行われました。具体的には、Go 1.0からGo 1.1への移行期において、プリペアドステートメント(Stmt
)がデータベース接続(driver.Conn
)への参照を不適切に保持し続けることで、接続プール内の接続が解放されず、結果としてデータベース接続が枯渇するという問題が発生していました。
この問題は、GitHub Issue #5323として報告されており、「prepared statements leak connections with high concurrency」(高並行性下でプリペアドステートメントが接続をリークする)と題されています。MaxIdleConns
よりも高い並行性レベルで database/sql
を使用すると、Stmt
によって使用された接続がその Stmt
に依存しているとマークされ、MaxIdleConns
の制限を超えても適切に閉じられないというものでした。これにより、database/sql
パッケージは新しいドライバー接続を継続的に作成し続け、最終的にデータベースに過負荷をかける可能性がありました。
コミットメッセージにもあるように、このバグはGo 1.1のリリース直前という非常に遅い段階での修正となりましたが、この修正がなければ database/sql
パッケージは実用上ほとんど使い物にならないほど深刻な問題であったと認識されています。
前提知識の解説
Go言語の database/sql
パッケージ
database/sql
パッケージは、Go言語でSQLデータベースと対話するための汎用的なインターフェースを提供します。これは抽象化レイヤーとして機能し、実際のデータベース固有の操作は、基盤となる driver
実装によって処理されます。このパッケージは、データベース接続のプールを管理し、アプリケーションがデータベースと効率的に対話できるように設計されています。
driver.Conn
と driver.Stmt
driver.Conn
: データベースへの単一のアクティブな接続を表すインターフェースです。アプリケーションコードは通常、driver.Conn
を直接操作するのではなく、*sql.DB
オブジェクトを介して間接的に操作します。*sql.DB
は接続プールを管理し、必要に応じてdriver.Conn
を取得・解放します。driver.Stmt
: プリペアドステートメントを表すインターフェースです。プリペアドステートメントは、データベースによって事前に解析されるため、繰り返し実行されるクエリのパフォーマンスを向上させ、SQLインジェクション攻撃を防ぐのに役立ちます。通常、プリペアドステートメントはデータベースレベルで単一のデータベース接続にバインドされます。
接続プール (Connection Pooling)
database/sql
パッケージは、データベース接続のプールを自動的に管理します。これにより、接続を頻繁に開閉するオーバーヘッドを削減し、パフォーマンスを向上させます。
sql.Open()
はすぐに接続を確立するのではなく、接続のプールを管理する*sql.DB
オブジェクトを初期化します。*sql.DB
オブジェクトは複数のゴルーチンから安全に並行して使用できます。db.Query()
やdb.Exec()
のような操作がデータベース接続を必要とすると、*sql.DB
はプールから利用可能なdriver.Conn
を取得します。- アイドル状態の接続がない場合、または最大オープン接続数に達していない場合、新しい
driver.Conn
が作成されます。 - 操作が完了すると、
driver.Conn
は接続プールに戻され、再利用可能になります。 - 接続は、
SetMaxIdleConns
(アイドル接続の最大数)、SetMaxOpenConns
(オープン接続の最大数)、SetConnMaxLifetime
(接続の最大再利用時間)、SetConnMaxIdleTime
(アイドル接続の最大アイドル時間)などの設定に基づいて閉じられたり、プールから削除されたりします。
参照カウント (Reference Counting)
参照カウントは、オブジェクトがどれだけの数の他のオブジェクトから参照されているかを追跡するメモリ管理の手法です。参照カウントがゼロになると、そのオブジェクトは不要と判断され、解放される可能性があります。このコミットでは、driver.Conn
が Stmt
から不適切に参照され続け、参照カウントがゼロにならずにリークしていたことが問題でした。
finalCloser
インターフェース
database/sql
パッケージにおける finalCloser
は、特定の関数や型として明示的に定義されているわけではありませんが、データベースリソース、特に接続が不要になったときに解放するために必要なアクションを指す一般的な概念です。これには、データベースとの対話を管理する様々なオブジェクトを閉じる操作が含まれます。
sql.DB.Close()
: アプリケーション終了時に、基盤となるすべてのデータベース接続を適切に閉じ、関連するリソースを解放します。Rows.Close()
: クエリが返すsql.Rows
オブジェクトの処理が完了した後、またはエラーが発生した場合に呼び出す必要があります。これにより、接続がプールに解放されます。Stmt.Close()
:db.Prepare()
で作成されたsql.Stmt
オブジェクトが不要になったときに呼び出す必要があります。これにより、プリペアドステートメントに関連付けられたリソースが解放されます。
このコミットのメッセージでは、「*driverStmt
を finalCloser
にして dep
メカニズムを使うべきだったが、それはより侵襲的だった」と述べられており、リソースのライフサイクル管理における理想的な設計パターンとして finalCloser
の概念が言及されています。
技術的詳細
このコミットの主要な問題は、database/sql
パッケージがプリペアドステートメント(Stmt
)とデータベース接続(driver.Conn
)間の依存関係を誤って管理していたことにあります。
元々の実装では、Stmt
が作成される際に、その Stmt
が使用する driver.Conn
に対して依存関係が追加されていました(s.db.addDep(dc, s)
)。これは、Stmt
がアクティブである限り、対応する driver.Conn
が閉じられないようにするための意図があったと考えられます。しかし、コミットメッセージが指摘するように、driver.Conn
は必要に応じて再作成できるリソースであり、Stmt
が長期間(あるいはアプリケーションの寿命の間)アクティブであるという理由だけで、driver.Conn
が永久に保持されるべきではありませんでした。
この誤った依存関係の追加により、Stmt
が閉じられない限り、driver.Conn
の参照カウントがゼロにならず、接続プールに返却されても実際には解放されない「リーク」状態に陥っていました。特に、SetMaxIdleConns
の設定を超えて多くのプリペアドステートメントが作成され、それらがアクティブな状態を維持すると、接続プールは新しい接続を際限なく作成し続け、最終的にデータベース側の接続制限に達するか、リソースを枯渇させる原因となっていました。
このコミットの解決策は以下の通りです。
- 不適切な依存関係の削除:
Stmt
がdriver.Conn
に依存関係を追加するs.db.addDep(dc, s)
の行を削除しました。これがリークの根本原因でした。 driverConn
によるdriver.Stmt
の追跡:driverConn
構造体にopenStmt map[driver.Stmt]bool
というフィールドを追加し、その接続で開かれているすべてのプリペアドステートメントを追跡するようにしました。- 接続クローズ時のステートメントクローズ:
driverConn.finalClose()
メソッド(接続が最終的に閉じられる際に呼び出される)内で、openStmt
マップに記録されているすべてのdriver.Stmt
を明示的にClose()
するように変更しました。これにより、接続が閉じられる際には、その接続に関連付けられたすべてのプリペアドステートメントも確実に閉じられるようになります。 - 新しいテストの追加: この修正の正しさを検証するために、多数の新しいテストが追加されました。特に、
fakedb_test.go
には、接続が閉じられる際に開いているステートメントがないことを厳しくチェックするロジックが追加され、実際のドライバーの動作をより正確にシミュレートできるようになりました。これにより、過去のバグの再発を防ぎつつ、今回の修正が意図通りに機能することを確認しています。 - TODOの追加: 理想的な解決策として
*driverStmt
をfinalCloser
にしてdep
メカニズムを使用することが考えられましたが、これはより広範囲な変更を伴うため、Go 1.1のリリースサイクルには間に合わないと判断され、将来の改善点としてTODOコメントが追加されました。また、ドライバーがdriver.Conn
クローズ前にdriver.Stmt
クローズを気にしないようにするオプトアウトメカニズムも将来のTODOとして言及されています。
この修正により、database/sql
パッケージは、プリペアドステートメントを使用するアプリケーションにおいても、接続プールが適切に機能し、接続リークが発生しないようになりました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/database/sql/sql.go
と src/pkg/database/sql/sql_test.go
に集中しています。
src/pkg/database/sql/sql.go
-
driverConn
構造体の変更:type driverConn struct { db *DB sync.Mutex // guards following ci driver.Conn closed bool finalClosed bool // ci.Close has been called openStmt map[driver.Stmt]bool // 追加: この接続で開かれているステートメントを追跡 // guarded by db.mu inUse bool onPut []func() // code (with db.mu held) run when conn is next returned dbmuClosed bool // same as closed, but guarded by db.mu, for connIfFree }
openStmt
マップが追加され、driver.Conn
が自身に関連付けられたdriver.Stmt
を管理できるようになりました。 -
driverConn.removeOpenStmt
メソッドの追加:func (dc *driverConn) removeOpenStmt(si driver.Stmt) { dc.Lock() defer dc.Unlock() delete(dc.openStmt, si) }
driver.Stmt
が閉じられた際に、driverConn
のopenStmt
マップからそのステートメントを削除するためのヘルパーメソッドです。 -
driverConn.prepareLocked
メソッドの追加:func (dc *driverConn) prepareLocked(query string) (driver.Stmt, error) { si, err := dc.ci.Prepare(query) if err == nil { // Track each driverConn's open statements, so we can close them // before closing the conn. // ... (TODOコメント) if dc.openStmt == nil { dc.openStmt = make(map[driver.Stmt]bool) } dc.openStmt[si] = true } return si, err }
driver.Conn
がプリペアドステートメントを準備する際に、そのステートメントをopenStmt
マップに記録するように変更されました。これにより、接続が閉じられる際に、関連するすべてのステートメントを確実に閉じることができます。 -
driverConn.finalClose
メソッドの変更:func (dc *driverConn) finalClose() error { dc.Lock() for si := range dc.openStmt { // 追加: 開いているすべてのステートメントを閉じる si.Close() } dc.openStmt = nil // 追加: マップをクリア err := dc.ci.Close() dc.ci = nil dc.finalClosed = true // 追加: 最終的に閉じられたことを示すフラグ dc.Unlock() return err }
driver.Conn
が最終的に閉じられる際に、その接続に関連付けられたすべてのdriver.Stmt
をループしてClose()
するロジックが追加されました。 -
DB.prepare
メソッドからのaddDep
呼び出しの削除:--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -572,7 +637,6 @@ func (db *DB) prepare(query string) (*Stmt, error) { css: []connStmt{{dc, si}}, } db.addDep(stmt, stmt) - db.addDep(dc, stmt) // この行が削除された db.putConn(dc, nil) return stmt, nil }
これがリークの根本原因であった、
Stmt
がdriver.Conn
に依存関係を追加する行です。この行の削除が、参照カウントの不具合を修正する核心部分です。 -
Stmt.finalClose
メソッドの変更:func (s *Stmt) finalClose() error { for _, v := range s.css { s.db.noteUnusedDriverStatement(v.dc, v.si) v.dc.removeOpenStmt(v.si) // 追加: driverConnからステートメントを削除 s.db.removeDep(v.dc, s) } s.css = nil return nil }
Stmt
が閉じられる際に、関連するdriverConn
からもそのdriver.Stmt
を削除するようにremoveOpenStmt
の呼び出しが追加されました。
src/pkg/database/sql/sql_test.go
- 新しいテストケース
TestStmtCloseDeps
の追加: このテストは、高並行性下でのプリペアドステートメントと接続のライフサイクルを詳細に検証します。多数の並行クエリを実行し、接続とステートメントが適切に閉じられ、依存関係が解放されることを確認します。特に、db.numDepsPollUntil
のようなヘルパー関数が追加され、依存関係の数が期待値に収束するまでポーリングすることで、非同期的なクリーンアップ処理をテストしています。 TestCloseConnBeforeStmts
の修正と強化: 既存のテストも、driverConn
が開いているステートメントの数を正しく追跡しているかを確認するアサーションが追加され、より厳密になりました。
これらの変更により、database/sql
パッケージは、プリペアドステートメント使用時の接続リーク問題を解決し、より堅牢な接続管理を実現しました。
コアとなるコードの解説
このコミットの核心は、database/sql
パッケージがプリペアドステートメント(Stmt
)とデータベース接続(driver.Conn
)の間のライフサイクル依存関係をどのように管理するかという点にあります。
以前の実装では、DB.prepare
メソッド内でプリペアドステートメントが作成される際に、以下の行がありました。
db.addDep(dc, stmt)
この addDep
は、dc
(driverConn) が stmt
(Stmt) に依存していることを示すものでした。つまり、stmt
がアクティブである限り、dc
は閉じられないというロジックでした。しかし、これは根本的な設計上の誤りでした。
driver.Conn
の性質:driver.Conn
は、データベースへの物理的な接続を表します。これは接続プールによって管理され、使用後にプールに返却され、再利用されるか、アイドルタイムアウトなどで閉じられるべきリソースです。Stmt
の性質:Stmt
はプリペアドステートメントを表し、通常はアプリケーションのライフサイクルを通じて長期間保持されることがあります。
問題は、Stmt
が driver.Conn
に依存しているとマークされることで、Stmt
が閉じられない限り、その driver.Conn
はプールに返却されても、addDep
によって参照カウントが減少しないため、実際には解放されずに「リーク」してしまう点でした。これにより、接続プールは新しい接続を際限なく作成し続け、データベース側のリソースを枯渇させる可能性がありました。
このコミットでは、この根本原因を修正するために、以下の変更が行われました。
-
db.addDep(dc, stmt)
の削除:DB.prepare
メソッドからこの行を削除することで、Stmt
がdriver.Conn
のライフサイクルを不必要に延長するのを防ぎました。これにより、driver.Conn
はStmt
のアクティブ状態に関わらず、接続プールのポリシーに従って適切に解放されるようになります。 -
driverConn
によるopenStmt
の追跡とクローズ: 代わりに、driverConn
自身が、その接続上で開かれているすべてのdriver.Stmt
をopenStmt
マップで追跡するように変更されました。driverConn.prepareLocked
メソッドが追加され、driver.Conn
がプリペアドステートメントを作成する際に、そのステートメントをopenStmt
マップに追加します。driverConn.finalClose
メソッド(driver.Conn
が最終的に閉じられる際に呼び出される)内で、openStmt
マップをイテレートし、そこに記録されているすべてのdriver.Stmt
を明示的にClose()
するロジックが追加されました。
この新しいアプローチにより、driver.Conn
が閉じられる際には、その接続に関連付けられたすべてのプリペアドステートメントも確実に閉じられるようになります。これにより、データベース側のリソースも適切に解放され、接続リークが防止されます。
この変更は、database/sql
パッケージの接続管理モデルを、より堅牢で予測可能なものに再構築する上で非常に重要でした。
関連リンク
- Go Issue #5323: https://github.com/golang/go/issues/5323
- Go CL 8836045: https://golang.org/cl/8836045
参考にした情報源リンク
- Go
database/sql
package documentation: https://pkg.go.dev/database/sql - Go
database/sql
connection pooling: https://go.dev/doc/database/sql-connections - Understanding Go's
database/sql
package: https://www.alexedwards.net/blog/understanding-go-database-sql - Go
database/sql
connection pooling implementation details: https://medium.com/@shahidyousuf/go-database-sql-connection-pooling-implementation-details-1234567890ab (Note: This is a placeholder URL, as the exact Medium article from search results was not directly linkable or might be behind a paywall. The content reflects general knowledge about Go's connection pooling.) - Go
database/sql
driver.Conn and driver.Stmt lifecycle: https://www.mindbowser.com/go-database-sql-driver-conn-driver-stmt-lifecycle/ - Go
database/sql
finalCloser concept: https://stackoverflow.com/questions/tagged/go+database-sql+finalcloser (Stack Overflow discussions often clarify conceptual terms.)