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

[インデックス 17440] ファイルの概要

このコミットは、Go言語の database/sql パッケージにおける重要な機能追加と改善を導入しています。主な変更点は、データベースへの同時オープン接続数を制限する SetMaxOpenConns 関数の追加、アイドル接続プールの実装変更(スライスベースからリストベースへ)、そして並行処理テストとベンチマークの拡充です。これにより、データベース接続の管理がより柔軟かつ効率的になり、特に高負荷環境下での安定性とパフォーマンスが向上しています。

コミット

commit 4572e4848364b6098b565a2ef480e9b4e8ff5977
Author: Tad Glines <tad.glines@gmail.com>
Date:   Thu Aug 29 17:20:39 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 seperate 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/4572e4848364b6098b565a2ef480e9b4e8ff5977

元コミット内容

このコミットは、以下の主要な変更を含んでいます。

  • SetMaxOpenConns の追加: データベースへのオープン接続数の上限を設定する機能が追加されました。これにより、アプリケーションが同時に開くデータベース接続の数を制御できるようになります。
  • Issue #4805 の解決: このコミットは、GoのIssueトラッカーで報告されていた問題 #4805 に対応しています。このIssueは、database/sqlパッケージにおける接続管理の改善に関するものでした。
  • Conn finalCloser のロック競合修正: ConnfinalCloserdb.mu (データベースミューテックス) がロックされた状態で呼び出されるとデッドロックが発生する可能性があった問題が修正されました。
  • ベンチマークの拡充: Exec および Query の各パスに対して、個別のベンチマークが追加されました。これにより、パフォーマンス特性のより詳細な分析が可能になります。
  • アイドル接続プールの実装変更: アイドル状態の接続を管理するためのプールが、スライスベースの実装から container/list パッケージを使用したリストベースの実装に変更されました。

変更の背景

この変更の背景には、Goの database/sql パッケージが持つデータベース接続管理の課題がありました。以前のバージョンでは、オープン接続数に明示的な上限を設定する機能がなく、アプリケーションが過剰な数の接続を開いてしまい、データベースサーバーに負荷をかけたり、リソースを枯渇させたりする可能性がありました。これは、特に高負荷なWebアプリケーションやサービスにおいて、パフォーマンスの低下や安定性の問題を引き起こす要因となっていました。

Issue #4805 は、この接続管理の改善、特にオープン接続数の制限機能の必要性を提起していました。また、既存のアイドル接続プールの実装がスライスベースであったため、接続の追加や削除の際に効率が悪いという問題も指摘されていました。スライスは要素の追加や削除が末尾以外で行われる場合に、要素の移動が発生し、パフォーマンスに影響を与える可能性があります。

このコミットは、これらの課題に対処し、database/sql パッケージの堅牢性と効率性を向上させることを目的としています。SetMaxOpenConns の導入により、開発者はデータベース接続のリソース消費をより細かく制御できるようになり、アイドル接続プールの改善は、接続の再利用効率を高め、全体的なパフォーマンスを向上させます。

前提知識の解説

Go言語の database/sql パッケージ

database/sql パッケージは、Go言語における標準的なSQLデータベース操作のための汎用インターフェースを提供します。このパッケージ自体は特定のデータベースドライバーを含まず、データベース固有の機能は database/sql/driver インターフェースを実装する外部ドライバーによって提供されます。

主要な概念:

  • DB 構造体: データベースへのオープン接続のプールを表します。この構造体は、複数のゴルーチンから安全に利用できるように設計されており、接続の確立、解放、再利用を管理します。
  • Conn (driver.Conn): データベースへの単一の物理的な接続を表すインターフェースです。
  • Stmt (driver.Stmt): プリペアドステートメントを表すインターフェースです。SQLインジェクション攻撃を防ぎ、クエリの実行効率を向上させます。
  • 接続プール: database/sql パッケージは、データベース接続の再利用を目的とした接続プールを内部的に管理します。これにより、新しい接続を確立するオーバーヘッドを削減し、パフォーマンスを向上させます。アイドル接続(使用されていない接続)はプールに保持され、必要に応じて再利用されます。

データベース接続プール

データベース接続プールは、アプリケーションがデータベースに接続する際に発生するオーバーヘッドを削減するための一般的なパターンです。接続の確立は時間とリソースを消費する操作であるため、一度確立した接続を再利用することで、アプリケーションの応答性とスケーラビリティを向上させることができます。

接続プールは通常、以下の要素を管理します。

  • 最大アイドル接続数 (MaxIdleConns): プール内でアイドル状態(使用されていない)で保持できる接続の最大数。この数を超えたアイドル接続は閉じられます。
  • 最大オープン接続数 (MaxOpenConns): プール内で同時にオープンできる接続の最大数。これには、現在使用中の接続とアイドル接続の両方が含まれます。この制限に達すると、新しい接続要求は、既存の接続が解放されるまで待機します。
  • 接続のライフタイム: 接続がプール内で保持される最大時間や、接続が再利用される前に検証される頻度など。

Goの container/list パッケージ

container/list パッケージは、Go言語で双方向リンクリストを実装するための標準ライブラリです。リンクリストは、要素が連続したメモリ位置に格納されるスライスとは異なり、各要素が次の要素(および前の要素)へのポインタを持つ構造です。

リンクリストの利点:

  • 効率的な挿入と削除: リストの任意の位置での要素の挿入や削除が、O(1) の時間計算量で行えます。これは、スライスで中間要素を削除・挿入する際に発生する要素の移動(O(N))と比較して効率的です。
  • 動的なサイズ変更: リストのサイズは動的に変更でき、事前に容量を確保する必要がありません。

リンクリストの欠点:

  • ランダムアクセスが遅い: 特定のインデックスの要素にアクセスするには、リストの先頭から順にたどる必要があるため、O(N) の時間計算量がかかります。スライスではO(1)です。
  • メモリオーバーヘッド: 各要素がポインタを持つため、スライスよりもメモリ使用量が多くなる可能性があります。

このコミットでは、アイドル接続プールをスライスからリンクリストに変更することで、接続の追加(プールへの返却)や削除(プールからの取得、または期限切れ接続のクローズ)の操作をより効率的に行うことを目指しています。

技術的詳細

SetMaxOpenConns の実装

SetMaxOpenConns 関数は、DB 構造体に maxOpen フィールドを追加することで実装されています。このフィールドは、同時にオープンできるデータベース接続の最大数を保持します。

新しい接続を確立する際、DB.conn() メソッドは db.numOpen (現在オープンしている接続数) と db.maxOpen をチェックします。db.numOpendb.maxOpen に達している場合、またはアイドル接続がない場合、接続要求は db.connRequests という新しい list.List に追加され、connectionOpener ゴルーチンによって処理されるのを待ちます。

connectionOpener は、db.openerCh チャネルからのシグナルを受け取ると、新しい接続を開こうとします。このゴルーチンは、db.maxOpen の制限と db.pendingOpens (現在開かれている途中の接続数) を考慮して、同時に開かれる接続数が制限を超えないように調整します。

アイドル接続プールの変更

以前の DB 構造体では、アイドル接続は freeConn []*driverConn というスライスで管理されていました。このコミットでは、これを freeConn *list.List に変更し、container/list パッケージの双方向リンクリストを使用するようにしました。

この変更により、putConnDBLocked (接続をプールに戻す) や conn (プールから接続を取得する) といった操作が、リストの先頭または末尾からの追加/削除として効率的に行えるようになりました。スライスベースの実装では、中間からの削除や挿入が要素の移動を伴い、パフォーマンスのボトルネックとなる可能性がありました。リンクリストを使用することで、これらの操作は定数時間 (O(1)) で実行できるようになります。

また、driverConn 構造体には listElem *list.Element フィールドが追加され、freeConn リスト内の自身の要素へのポインタを保持することで、リストからの削除を効率的に行えるようになっています。

finalCloser のロック競合修正

driverConn.finalClose() メソッドは、接続が完全にクローズされる際に呼び出されます。以前の実装では、このメソッドが db.mu (DBのミューテックス) がロックされた状態で呼び出される可能性があり、デッドロックを引き起こす原因となっていました。

このコミットでは、dc.closeDBLocked()db.mu を保持したまま dc.Unlock() を呼び出すのではなく、func() error を返すように変更されました。これにより、db.mu が解放された後に実際のクローズ処理が実行されるようになり、ロック競合が回避されます。

ベンチマークとテストの拡充

sql_test.go ファイルには、SetMaxOpenConns の動作を検証するための新しいテストケース TestMaxOpenConns が追加されました。このテストは、同時実行されるクエリの数を制御し、オープン接続数が設定された上限を超えないことを確認します。

さらに、ExecQuery の各パス、およびトランザクションやプリペアドステートメントを含む様々なシナリオでの並行処理をテストするための新しいベンチマーク関数が追加されました。これにより、database/sql パッケージの並行処理性能がより詳細に評価できるようになりました。NOSERT という新しい fakeStmt コマンドが追加され、テスト中に実際のデータベース挿入を行わずに Exec の準備作業をシミュレートできるようになっています。

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

src/pkg/database/sql/sql.go

  • DB 構造体の変更:
    • freeConn []*driverConnfreeConn *list.List に変更。
    • connRequests *list.List (接続要求キュー) を追加。
    • numOpen int (現在オープンしている接続数) を追加。
    • pendingOpens int (オープン中の接続数) を追加。
    • openerCh chan struct{} (接続オープナーゴルーチンへのシグナルチャネル) を追加。
    • maxOpen int (最大オープン接続数) を追加。
  • driverConn 構造体の変更:
    • listElem *list.Element (freeConnリスト内の自身の要素へのポインタ) を追加。
  • Open 関数の変更:
    • db.freeConndb.connRequestslist.New() で初期化。
    • db.openerCh をバッファ付きチャネルとして初期化。
    • db.connectionOpener() ゴルーチンを起動。
  • Close 関数の変更:
    • db.openerCh をクローズし、connectionOpener ゴルーチンを終了させる。
    • freeConn リスト内のすべての接続をクローズするロジックをリストベースに変更。
  • SetMaxIdleConns 関数の変更:
    • db.freeConn の操作をリストベースに変更。
    • maxOpenmaxIdle の整合性を保つロジックを追加。
  • SetMaxOpenConns 関数の追加:
    • db.maxOpen を設定し、maxIdle との整合性を調整する。
  • maybeOpenNewConnections 関数の追加:
    • db.connRequestsdb.maxOpen に基づいて、新しい接続を開く必要があるかどうかを判断し、openerCh にシグナルを送る。
  • connectionOpener 関数の追加:
    • openerCh からのシグナルを受け取り、openNewConnection を呼び出すゴルーチン。
  • openNewConnection 関数の追加:
    • 新しいデータベース接続を開き、db.numOpen を更新し、putConnDBLocked を呼び出す。
  • connRequest 型の追加:
    • 接続要求を表すチャネル型。
  • conn 関数の変更:
    • maxOpen 制限と connRequests を考慮した接続取得ロジックを追加。
    • db.freeConn からの接続取得をリストベースに変更。
  • putConn 関数の変更:
    • putConnDBLocked を呼び出すように変更。
  • putConnDBLocked 関数の追加:
    • 接続要求を満たすか、アイドルプールに接続を戻すロジック。

src/pkg/database/sql/sql_test.go

  • TestMaxOpenConns の追加:
    • SetMaxOpenConns の動作を検証するテストケース。
  • TestConcurrency の変更:
    • 様々な並行処理テストケース (concurrentDBQueryTest, concurrentDBExecTest など) を追加し、doConcurrentTest 関数で実行するように変更。
  • ベンチマーク関数の追加:
    • BenchmarkConcurrentDBExec, BenchmarkConcurrentStmtQuery など、各操作パスに対する詳細なベンチマークを追加。
  • doConcurrentTest 関数の追加:
    • 並行処理テストを実行するためのヘルパー関数。
  • concurrentTest インターフェースと関連するテスト構造体の追加:
    • 様々な種類の並行処理テストを抽象化するためのインターフェースと実装。
  • numFreeConns の変更:
    • len(db.freeConn) から db.freeConn.Len() に変更。

src/pkg/database/sql/fakedb_test.go

  • NOSERT コマンドの追加:
    • fakeConnfakeStmtNOSERT コマンドを追加。これは INSERT と同様の準備作業を行うが、実際には行を挿入しない。並行処理テストで使用される。

コアとなるコードの解説

DB 構造体と接続管理の新しいメカニズム

type DB struct {
	// ... 既存のフィールド ...
	mu           sync.Mutex // protects following fields
	freeConn     *list.List // of *driverConn (アイドル接続プール)
	connRequests *list.List // of connRequest (接続要求キュー)
	numOpen      int        // 現在オープンしている接続数
	pendingOpens int        // オープン中の接続数
	openerCh     chan struct{} // 接続オープナーゴルーチンへのシグナル
	closed       bool
	// ... その他のフィールド ...
	maxOpen  int // <= 0 means unlimited (最大オープン接続数)
}

DB 構造体には、maxOpen フィールドが追加され、オープン接続数の上限を管理します。freeConncontainer/list.List に変更され、アイドル接続の効率的な追加・削除を可能にします。connRequests は、接続が利用可能になるのを待っているリクエストをキューイングするために使用されます。numOpen は現在アクティブな接続の総数を追跡し、pendingOpens は現在確立中の接続の数を追跡します。openerCh は、新しい接続を開く必要があることを connectionOpener ゴルーチンに通知するためのチャネルです。

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) // maxOpenがmaxIdleより小さい場合、maxIdleを調整
	}
}

この関数は、db.maxOpen の値を設定します。もし maxOpen0 以下に設定された場合、接続数に制限はなくなります。また、maxOpenmaxIdleConnsLocked() (最大アイドル接続数) よりも小さくなった場合、SetMaxIdleConns を呼び出して maxIdle も調整し、maxIdlemaxOpen を超えないようにします。

conn メソッドにおける接続取得ロジック

func (db *DB) conn() (*driverConn, error) {
	db.mu.Lock()
	if db.closed {
		db.mu.Unlock()
		return nil, errDBClosed
	}

	// maxOpenが設定されており、オープン接続数が上限に達しているか、アイドル接続がない場合
	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
}

conn メソッドは、接続を取得する際の中心的なロジックを含んでいます。maxOpen が設定されている場合、numOpenmaxOpen に達しているか、アイドル接続がない場合は、接続要求を connRequests キューに入れ、connectionOpener ゴルーチンが新しい接続を開くのを待ちます。これにより、オープン接続数が上限を超えないように制御されます。アイドル接続がある場合は、freeConn リストの先頭から取得されます。それ以外の場合は、新しい接続が確立されます。

putConnDBLocked メソッド

func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
	if db.connRequests.Len() > 0 { // 接続要求がある場合
		req := db.connRequests.Front().Value.(connRequest)
		db.connRequests.Remove(db.connRequests.Front())
		if err != nil {
			req <- err // エラーを要求元に通知
		} else {
			dc.inUse = true
			req <- dc // 接続を要求元に渡す
		}
		return true
	} else if err == nil && !db.closed && db.maxIdleConnsLocked() > 0 && db.maxIdleConnsLocked() > db.freeConn.Len() {
		// アイドルプールに空きがあり、エラーがない場合、アイドルプールに戻す
		dc.listElem = db.freeConn.PushFront(dc)
		return true
	}
	return false // 接続要求がなく、アイドルプールにも戻せない場合
}

putConnDBLocked は、使用済みの接続を処理する際に呼び出されます。もし接続を待っているリクエストがあれば、そのリクエストに接続(またはエラー)を渡します。そうでなければ、アイドル接続プールに空きがあり、かつエラーがない場合に、接続をプールに戻します。これにより、接続の再利用が促進されます。

関連リンク

  • Go database/sql パッケージのドキュメント: https://pkg.go.dev/database/sql
  • Go container/list パッケージのドキュメント: https://pkg.go.dev/container/list
  • Go Issue #4805: database/sql: add SetMaxOpenConns (このコミットが解決したIssue) - 検索しても直接的なリンクは見つかりませんでしたが、コミットメッセージに記載されています。

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goのソースコード (特に src/pkg/database/sql/)
  • 一般的なデータベース接続プールの概念に関する情報
  • Goの container/list パッケージに関する情報