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

[インデックス 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)はいつでも再作成できるリソースであり(ただし速度のためにキャッシュされる)、Stmts)が生きている(通常は長期間、例えば永久に生きている)という理由だけで、依存関係の参照カウントによって永久に保持されるべきではない。

パッチの大部分は新しいテストである。実際の問題を修正する中で、fakedb_test.go の「開いている fakeStmt がある間に fakeConn を閉じることに対する厳格なチェック」のために多くのテストが失敗した。これを無視することもできたが、それは過去に他のバグの原因となっていた。

代わりに、接続ごとに開いているステートメントを追跡し、接続が閉じるときにそれらを閉じるようにした。これを適切に行うには *driverStmtfinalCloser にして 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.Conndriver.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.ConnStmt から不適切に参照され続け、参照カウントがゼロにならずにリークしていたことが問題でした。

finalCloser インターフェース

database/sql パッケージにおける finalCloser は、特定の関数や型として明示的に定義されているわけではありませんが、データベースリソース、特に接続が不要になったときに解放するために必要なアクションを指す一般的な概念です。これには、データベースとの対話を管理する様々なオブジェクトを閉じる操作が含まれます。

  • sql.DB.Close(): アプリケーション終了時に、基盤となるすべてのデータベース接続を適切に閉じ、関連するリソースを解放します。
  • Rows.Close(): クエリが返す sql.Rows オブジェクトの処理が完了した後、またはエラーが発生した場合に呼び出す必要があります。これにより、接続がプールに解放されます。
  • Stmt.Close(): db.Prepare() で作成された sql.Stmt オブジェクトが不要になったときに呼び出す必要があります。これにより、プリペアドステートメントに関連付けられたリソースが解放されます。

このコミットのメッセージでは、「*driverStmtfinalCloser にして 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 の設定を超えて多くのプリペアドステートメントが作成され、それらがアクティブな状態を維持すると、接続プールは新しい接続を際限なく作成し続け、最終的にデータベース側の接続制限に達するか、リソースを枯渇させる原因となっていました。

このコミットの解決策は以下の通りです。

  1. 不適切な依存関係の削除: Stmtdriver.Conn に依存関係を追加する s.db.addDep(dc, s) の行を削除しました。これがリークの根本原因でした。
  2. driverConn による driver.Stmt の追跡: driverConn 構造体に openStmt map[driver.Stmt]bool というフィールドを追加し、その接続で開かれているすべてのプリペアドステートメントを追跡するようにしました。
  3. 接続クローズ時のステートメントクローズ: driverConn.finalClose() メソッド(接続が最終的に閉じられる際に呼び出される)内で、openStmt マップに記録されているすべての driver.Stmt を明示的に Close() するように変更しました。これにより、接続が閉じられる際には、その接続に関連付けられたすべてのプリペアドステートメントも確実に閉じられるようになります。
  4. 新しいテストの追加: この修正の正しさを検証するために、多数の新しいテストが追加されました。特に、fakedb_test.go には、接続が閉じられる際に開いているステートメントがないことを厳しくチェックするロジックが追加され、実際のドライバーの動作をより正確にシミュレートできるようになりました。これにより、過去のバグの再発を防ぎつつ、今回の修正が意図通りに機能することを確認しています。
  5. TODOの追加: 理想的な解決策として *driverStmtfinalCloser にして dep メカニズムを使用することが考えられましたが、これはより広範囲な変更を伴うため、Go 1.1のリリースサイクルには間に合わないと判断され、将来の改善点としてTODOコメントが追加されました。また、ドライバーが driver.Conn クローズ前に driver.Stmt クローズを気にしないようにするオプトアウトメカニズムも将来のTODOとして言及されています。

この修正により、database/sql パッケージは、プリペアドステートメントを使用するアプリケーションにおいても、接続プールが適切に機能し、接続リークが発生しないようになりました。

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

このコミットにおける主要なコード変更は、src/pkg/database/sql/sql.gosrc/pkg/database/sql/sql_test.go に集中しています。

src/pkg/database/sql/sql.go

  1. 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 を管理できるようになりました。

  2. driverConn.removeOpenStmt メソッドの追加:

    func (dc *driverConn) removeOpenStmt(si driver.Stmt) {
    	dc.Lock()
    	defer dc.Unlock()
    	delete(dc.openStmt, si)
    }
    

    driver.Stmt が閉じられた際に、driverConnopenStmt マップからそのステートメントを削除するためのヘルパーメソッドです。

  3. 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 マップに記録するように変更されました。これにより、接続が閉じられる際に、関連するすべてのステートメントを確実に閉じることができます。

  4. 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() するロジックが追加されました。

  5. 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
     }
    

    これがリークの根本原因であった、Stmtdriver.Conn に依存関係を追加する行です。この行の削除が、参照カウントの不具合を修正する核心部分です。

  6. 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 はプリペアドステートメントを表し、通常はアプリケーションのライフサイクルを通じて長期間保持されることがあります。

問題は、Stmtdriver.Conn に依存しているとマークされることで、Stmt が閉じられない限り、その driver.Conn はプールに返却されても、addDep によって参照カウントが減少しないため、実際には解放されずに「リーク」してしまう点でした。これにより、接続プールは新しい接続を際限なく作成し続け、データベース側のリソースを枯渇させる可能性がありました。

このコミットでは、この根本原因を修正するために、以下の変更が行われました。

  1. db.addDep(dc, stmt) の削除: DB.prepare メソッドからこの行を削除することで、Stmtdriver.Conn のライフサイクルを不必要に延長するのを防ぎました。これにより、driver.ConnStmt のアクティブ状態に関わらず、接続プールのポリシーに従って適切に解放されるようになります。

  2. driverConn による openStmt の追跡とクローズ: 代わりに、driverConn 自身が、その接続上で開かれているすべての driver.StmtopenStmt マップで追跡するように変更されました。

    • driverConn.prepareLocked メソッドが追加され、driver.Conn がプリペアドステートメントを作成する際に、そのステートメントを openStmt マップに追加します。
    • driverConn.finalClose メソッド(driver.Conn が最終的に閉じられる際に呼び出される)内で、openStmt マップをイテレートし、そこに記録されているすべての driver.Stmt を明示的に Close() するロジックが追加されました。

この新しいアプローチにより、driver.Conn が閉じられる際には、その接続に関連付けられたすべてのプリペアドステートメントも確実に閉じられるようになります。これにより、データベース側のリソースも適切に解放され、接続リークが防止されます。

この変更は、database/sql パッケージの接続管理モデルを、より堅牢で予測可能なものに再構築する上で非常に重要でした。

関連リンク

参考にした情報源リンク