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

[インデックス 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/sqldriver.ErrBadConn を受け取り、そのコネクションを不良としてマークします。

問題は、不良とマークされたコネクションがコネクションプールから適切に削除されず、再利用されようとすることにありました。特に、プリペアドステートメントが多くの不良コネクションを「生き残る」と、そのステートメントを実行するたびに、不良なコネクションを次々と試行し、最終的に有効なコネクションを見つけるまでに多くの無駄な処理が発生していました。これにより、プリペアドステートメントの呼び出しが非常に高価になり、アプリケーションのパフォーマンスに悪影響を与えていました。

このコミットは、driver.ErrBadConn が検出された際に、不良なコネクションを即座にクローズすることで、このコネクションがプールに再投入され、将来的に再利用されることを防ぎ、問題の根本原因を解消します。

前提知識の解説

database/sql パッケージ

database/sql はGo言語の標準ライブラリの一部であり、SQLデータベースへの汎用的なインターフェースを提供します。このパッケージは、特定のデータベースドライバーに依存しない抽象化レイヤーを提供し、コネクションプール管理、プリペアドステートメント、トランザクションなどの機能を提供します。

プリペアドステートメント (Prepared Statements)

プリペアドステートメントは、SQLクエリを事前にデータベースサーバーに送信し、解析・コンパイルさせておくことで、同じクエリを繰り返し実行する際のパフォーマンスを向上させるメカニズムです。特に、パラメータ化されたクエリ(例: SELECT * FROM users WHERE id = ?)でSQLインジェクション攻撃を防ぐための主要な防御策としても機能します。

database/sql において、DB.Prepare() メソッドでプリペアドステートメントを作成すると、内部的にはコネクションプールからコネクションを取得し、そのコネクション上でデータベースドライバーの Prepare メソッドを呼び出します。このプリペアドステートメントは、その後の ExecQuery 呼び出しで再利用されます。

コネクションプール (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.ErrBadConnputConn に渡された場合、そのコネクションは再利用されないようにマークされていましたが、明示的にクローズされるわけではありませんでした。これにより、不良なコネクションがプール内に残り続け、プリペアドステートメントがそれを取得しようとするたびに、無駄な試行が発生していました。

具体的には、プリペアドステートメントは、内部的に Stmt 構造体として表現され、その StmtDB に関連付けられています。Stmt が実行される際には、DB から利用可能なコネクションを取得しようとします。もし取得したコネクションが不良であれば、そのコネクションは破棄され、別のコネクションが試行されます。このプロセスが繰り返されることで、多数の不良コネクションがプールに存在すると、有効なコネクションを見つけるまでに時間がかかり、パフォーマンスが低下していました。

このコミットでは、DB.putConn メソッド内で driver.ErrBadConn が検出された際に、該当する driverConn オブジェクトに対して Close() メソッドを呼び出すように変更されました。これにより、不良なコネクションは即座に閉じられ、プールから完全に削除されるため、将来的に再利用されることがなくなります。

また、DB.connIfFree メソッド内の wanted.inUse のチェックが移動されています。これは、wanted.dbmuClosed のチェックの後に移動されており、論理的な順序を改善し、コネクションが既に閉じられているかどうかのチェックを優先することで、より効率的な処理を可能にしています。ただし、この変更自体が直接的に不良コネクションの蓄積問題を解決するわけではなく、コードの堅牢性と可読性を向上させるためのものです。

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

このコミットでは、主に src/pkg/database/sql/sql.gosrc/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 の変更点

  1. 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 チェックを行うことを避けることができます。

  2. 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が修正されたため、以前は既知のバグのためにスキップされていたこのテストが、正常に実行されるようになったことを意味します。このテストは、修正が正しく機能していることを検証するために使用されます。

関連リンク

参考にした情報源リンク