[インデックス 17229] ファイルの概要
このコミットは、Go言語の database/sql
パッケージにおける、プリペアドステートメントが多数のコネクションを使い果たした際に発生する「不良コネクションの蓄積」問題を修正するものです。この問題により、プリペアドステートメントの呼び出しコストが増大していました。具体的には、driver.ErrBadConn
が返された際に、不良なコネクションが適切にクローズされずに再利用され、結果としてパフォーマンスが低下する問題を解決します。
コミット
- コミットハッシュ:
13c7896fb69cb42da34c31480207aa4e8de19aa5
- Author: Matt Joiner anacrolix@gmail.com
- Date: Wed Aug 14 09:27:30 2013 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/13c7896fb69cb42da34c31480207aa4e8de19aa5
元コミット内容
database/sql: fix accumulation of bad conns on prepared statements
Fixes an issue where prepared statements that outlive many
connections become expensive to invoke.
Fixes #6081
R=golang-dev
CC=bradfitz, golang-dev
https://golang.org/cl/12646044
変更の背景
このコミットは、Goの database/sql
パッケージにおいて、プリペアドステートメントが長期間にわたって使用され、その間に多くのデータベースコネクションが「不良」とマークされると、そのプリペアドステートメントの実行コストが著しく増大するという問題(Issue #6081)を解決するために導入されました。
database/sql
パッケージは、データベースとのコネクションプールを管理しています。プリペアドステートメントは通常、特定のコネクションに関連付けられます。しかし、データベースサーバーの再起動、ネットワークの問題、またはデータベースセッションのタイムアウトなどにより、コネクションが使用不能(不良)になることがあります。このような場合、database/sql
は driver.ErrBadConn
を受け取り、そのコネクションを不良としてマークします。
問題は、不良とマークされたコネクションがコネクションプールから適切に削除されず、再利用されようとすることにありました。特に、プリペアドステートメントが多くの不良コネクションを「生き残る」と、そのステートメントを実行するたびに、不良なコネクションを次々と試行し、最終的に有効なコネクションを見つけるまでに多くの無駄な処理が発生していました。これにより、プリペアドステートメントの呼び出しが非常に高価になり、アプリケーションのパフォーマンスに悪影響を与えていました。
このコミットは、driver.ErrBadConn
が検出された際に、不良なコネクションを即座にクローズすることで、このコネクションがプールに再投入され、将来的に再利用されることを防ぎ、問題の根本原因を解消します。
前提知識の解説
database/sql
パッケージ
database/sql
はGo言語の標準ライブラリの一部であり、SQLデータベースへの汎用的なインターフェースを提供します。このパッケージは、特定のデータベースドライバーに依存しない抽象化レイヤーを提供し、コネクションプール管理、プリペアドステートメント、トランザクションなどの機能を提供します。
プリペアドステートメント (Prepared Statements)
プリペアドステートメントは、SQLクエリを事前にデータベースサーバーに送信し、解析・コンパイルさせておくことで、同じクエリを繰り返し実行する際のパフォーマンスを向上させるメカニズムです。特に、パラメータ化されたクエリ(例: SELECT * FROM users WHERE id = ?
)でSQLインジェクション攻撃を防ぐための主要な防御策としても機能します。
database/sql
において、DB.Prepare()
メソッドでプリペアドステートメントを作成すると、内部的にはコネクションプールからコネクションを取得し、そのコネクション上でデータベースドライバーの Prepare
メソッドを呼び出します。このプリペアドステートメントは、その後の Exec
や Query
呼び出しで再利用されます。
コネクションプール (Connection Pooling)
データベースコネクションの確立はコストの高い操作です。コネクションプールは、データベースコネクションを再利用するために、確立済みのコネクションを保持する仕組みです。これにより、新しいリクエストが来るたびにコネクションを確立するオーバーヘッドを削減し、アプリケーションの応答性を向上させます。
database/sql
パッケージは、内部的にコネクションプールを管理しており、DB.Open()
でデータベースを開くと、必要に応じてコネクションが作成され、プールに追加されます。コネクションが使用されなくなると、プールに戻され、他のリクエストで再利用できるようになります。
driver.ErrBadConn
database/sql/driver
パッケージは、データベースドライバーが実装すべきインターフェースを定義しています。driver.ErrBadConn
は、ドライバーがデータベースとのコネクションが不良である(例: ネットワーク切断、サーバーダウンなど)と判断した場合に返すことができるエラーです。このエラーが返されると、database/sql
パッケージは通常、そのコネクションをプールから削除し、新しいコネクションを確立しようとします。
しかし、このコミット以前は、driver.ErrBadConn
が返された際に、コネクションがプールから完全に削除されず、再利用可能な状態として扱われるケースがあったため、問題が発生していました。
技術的詳細
このコミットの技術的な核心は、database/sql
パッケージが不良なコネクションをどのように扱うかという点にあります。
database/sql
パッケージの DB
構造体は、データベースコネクションのプールを管理しています。コネクションは driverConn
構造体で表現され、DB.freeConn
スライスに利用可能なコネクションが保持されます。
問題の発生箇所は主に DB.putConn
メソッドでした。このメソッドは、使用済みのコネクションをプールに戻す役割を担っています。以前の実装では、driver.ErrBadConn
が putConn
に渡された場合、そのコネクションは再利用されないようにマークされていましたが、明示的にクローズされるわけではありませんでした。これにより、不良なコネクションがプール内に残り続け、プリペアドステートメントがそれを取得しようとするたびに、無駄な試行が発生していました。
具体的には、プリペアドステートメントは、内部的に Stmt
構造体として表現され、その Stmt
は DB
に関連付けられています。Stmt
が実行される際には、DB
から利用可能なコネクションを取得しようとします。もし取得したコネクションが不良であれば、そのコネクションは破棄され、別のコネクションが試行されます。このプロセスが繰り返されることで、多数の不良コネクションがプールに存在すると、有効なコネクションを見つけるまでに時間がかかり、パフォーマンスが低下していました。
このコミットでは、DB.putConn
メソッド内で driver.ErrBadConn
が検出された際に、該当する driverConn
オブジェクトに対して Close()
メソッドを呼び出すように変更されました。これにより、不良なコネクションは即座に閉じられ、プールから完全に削除されるため、将来的に再利用されることがなくなります。
また、DB.connIfFree
メソッド内の wanted.inUse
のチェックが移動されています。これは、wanted.dbmuClosed
のチェックの後に移動されており、論理的な順序を改善し、コネクションが既に閉じられているかどうかのチェックを優先することで、より効率的な処理を可能にしています。ただし、この変更自体が直接的に不良コネクションの蓄積問題を解決するわけではなく、コードの堅牢性と可読性を向上させるためのものです。
コアとなるコードの変更箇所
このコミットでは、主に src/pkg/database/sql/sql.go
と src/pkg/database/sql/sql_test.go
の2つのファイルが変更されています。
src/pkg/database/sql/sql.go
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -518,12 +518,12 @@ var (
func (db *DB) connIfFree(wanted *driverConn) (*driverConn, error) {
db.mu.Lock()
defer db.mu.Unlock()
- if wanted.inUse {
- return nil, errConnBusy
- }
if wanted.dbmuClosed {
return nil, errConnClosed
}
+ if wanted.inUse {
+ return nil, errConnBusy
+ }
for i, conn := range db.freeConn {
if conn != wanted {
continue
@@ -590,6 +590,7 @@ func (db *DB) putConn(dc *driverConn, err error) {
if err == driver.ErrBadConn {
// Don't reuse bad connections.
db.mu.Unlock()
+ dc.Close()
return
}
if putConnHook != nil {
src/pkg/database/sql/sql_test.go
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -1112,7 +1112,6 @@ func manyConcurrentQueries(t testOrBench) {
}
func TestIssue6081(t *testing.T) {
- t.Skip("known broken test")
db := newTestDB(t, "people")
defer closeDB(t, db)
コアとなるコードの解説
src/pkg/database/sql/sql.go
の変更点
-
connIfFree
メソッド内のwanted.inUse
チェックの移動:- if wanted.inUse { - return nil, errConnBusy - } if wanted.dbmuClosed { return nil, errConnClosed } + if wanted.inUse { + return nil, errConnBusy + }
この変更は、
connIfFree
関数内でwanted.inUse
のチェックがwanted.dbmuClosed
のチェックの後に移動されたことを示しています。これは、コネクションが既に閉じられているかどうかのチェックを優先し、その後に使用中であるかどうかのチェックを行うという、より論理的なフローを確立するためのものです。これにより、閉じられたコネクションに対して不必要なinUse
チェックを行うことを避けることができます。 -
putConn
メソッドにおけるdc.Close()
の追加:if err == driver.ErrBadConn { // Don't reuse bad connections. db.mu.Unlock() + dc.Close() return }
これがこのコミットの最も重要な変更点です。
putConn
メソッドは、コネクションが使用を終えてプールに戻される際に呼び出されます。もし、コネクションを返す際にdriver.ErrBadConn
がエラーとして渡された場合(これは、そのコネクションが不良であることを示します)、以前のコードでは単にそのコネクションを再利用しないようにマークするだけでした。しかし、この変更により、driver.ErrBadConn
が検出されたコネクション (dc
) は、db.mu.Unlock()
の直後にdc.Close()
が呼び出され、明示的にクローズされるようになりました。dc.Close()
が呼び出されることで、そのコネクションはデータベースドライバーによって実際に閉じられ、関連するリソースが解放されます。これにより、不良なコネクションがコネクションプール内に残り続け、将来的に誤って再利用されようとすることを防ぎます。結果として、プリペアドステートメントが不良なコネクションを繰り返し試行する無駄な処理がなくなり、パフォーマンスの低下が解消されます。
src/pkg/database/sql/sql_test.go
の変更点
--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -1112,7 +1112,6 @@ func manyConcurrentQueries(t testOrBench) {
}
func TestIssue6081(t *testing.T) {
- t.Skip("known broken test")
db := newTestDB(t, "people")
defer closeDB(t, db)
この変更は、TestIssue6081
というテスト関数から t.Skip("known broken test")
の行が削除されたことを示しています。これは、このコミットによってIssue #6081が修正されたため、以前は既知のバグのためにスキップされていたこのテストが、正常に実行されるようになったことを意味します。このテストは、修正が正しく機能していることを検証するために使用されます。
関連リンク
- Go Issue #6081: https://code.google.com/p/go/issues/detail?id=6081 (古いGoのIssueトラッカーのリンクですが、現在はGitHubに移行されています)
- Go CL 12646044: https://golang.org/cl/12646044 (GoのコードレビューシステムであるGerritのチェンジリストリンク)
参考にした情報源リンク
- Go issue 28130: "database/sql: statements created with conn.PrepareContext are re-prepared in transactions," - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG5i6C-BYhN6uKHxhRx_Ic73--BU-J5vLbkh_BGJcR2mhHRzRGXSgwlSDhN5KAIihSAYlMThVT4WEFho6A-EZokWLZq9RXIsYZ3MONUUjJZPZVCQI9aLxJbGxsOOQZZMQhZCHs=
- Go 1.4におけるトランザクションとプリペアドステートメントの競合状態の修正に関する議論 - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHrmhO9KTUqRv8TWtSIgPCGrcaUBXnJwV3TUNy2p7zh-T4kLn0k50AJa3ziB0P3B_QBekrsYeTfnAVsVWBQV4Yb89uo5MHLbFrBM0QWqfquOGnru-JJqK2F-lU2wNxCAiYL-cluFXxCWZMdu7z23wXnbGLHxQSWgNIX6FQOmOqyyxcZycWoKy3WvLZRGJkTkRUecwKfHFA=
database/sql
とプリペアドステートメントのパフォーマンスと再準備に関する一般的な情報 - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFxfYuguFNMxPLK_wEtyM8iwPm9v4vfYkzhmNtggm_L1QM-cvsyKKe6c6d7TlMKX5R711MvlFJWd4lFqTcHOdalOT1lZwxl8qu7u1pPjYlpmMJJPH141M_zyR8Gms63nU9N7gAC1WXgY7HHBxtDLJP31CRJ68faoZ-I-MJA6JbheWvv36vADTx.Prepare()
とTx.Stmt()
を使用したトランザクション内でのプリペアドステートメントの利用 - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFWNrZR3WZOGhxsVk6Mh6f-IY8JtCfIn3E3X8rMaVa8pPdR1LBN4EN82zOxHMWEN6AoKDQdJEftBt1pe_4WHT5i_E2sLHeS1XiOtg5aKWOHZu2ys9MDVVzJYsZrxpV_1YxINKljUHUzKuc=sql.Stmt
の適切なクローズとコネクションリークの防止 - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEv3wxUsrK0vZkPHcW8MTS_NqpL4wtOVMp3BBbX9goPv7ESivBtYBQm8L1_0VjruESsvMdQYQX5TGyrUnLVLnk6G8zbSVhjZ3inXGFom6Ebx_3kkW3ZR59L8BwZOHJT6cNxT4STnqBqiEPxabMfl34YC5lC2zCVwAX5nxH8Eh6Ckr7wCurryERyy-FZxSebT3rBpGPXEtlpnqskXHUomdfr-Ydxtms=QueryRow
使用時のsql.Stmt
クローズの重要性 - https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQExfBIpxnLOfzonHjke1eaoNhUQmfpQwfkmmdHhDhexQNJjbUJYBhhegmtQqC2f8Tp7Yix4Piif6MTdlaDicvL2SHwbjTh36nlg7ovcKwff4ozogm0FlclkW5A4myLcWl-GJzCgiMnE1DlIJdQFFoMa_ac8CNimJsdydgg2d0DVRF7kmy3KY0fjZoUFMXTDMYyaFDp_OQkhKuH2eJ4=