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

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

このコミットは、Go言語の database/sql パッケージに SetMaxOpenConns 関数を追加し、データベースへの同時オープン接続数の上限を設定できるようにするものです。これにより、アプリケーションがデータベースに確立できる接続数を制御し、リソースの過負荷を防ぐことが可能になります。また、アイドル接続プールをスライスベースからリストベース(container/list)に置き換え、Conn finalCloserdb.mu ロック中に呼び出される問題を修正しています。

コミット

commit 41c5d8d85f6c031c591c26404a5009a333d5d974
Author: Tad Glines <tad.glines@gmail.com>
Date:   Fri Aug 30 09:27:33 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 separate 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/41c5d8d85f6c031c591c26404a5009a333d5d974

元コミット内容

このコミットは、Goの database/sql パッケージに以下の主要な変更を導入します。

  • SetMaxOpenConns の追加: データベースへのオープン接続数の上限を設定する機能を追加します。
  • Issue #4805 の更新: 接続プールに関する既存の課題を解決します。
  • Conn finalCloser の修正: db.mu (データベースミューテックス) がロックされている状態で Conn finalCloser が呼び出されるケースを修正します。
  • アイドルプール実装の変更: アイドル接続プールをスライスベースからリストベース(container/list)に置き換えます。
  • ベンチマークの追加: Exec および Query の各パスに対して個別のベンチマークを追加します。

変更の背景

このコミットの背景には、Goの database/sql パッケージにおけるデータベース接続管理の課題がありました。特に、Issue #4805「Control over connection pool」で指摘されていたように、sql.Open() 関数がすぐにデータベース接続を確立せず、実際の接続が最初の操作まで遅延されること、そして接続プールに対するより明示的な制御の必要性が挙げられていました。

従来の database/sql パッケージでは、オープン接続数に上限がなかったため、アプリケーションが大量の同時リクエストを処理する際に、データベースサーバーに過剰な接続を確立し、データベースのリソースを枯渇させる可能性がありました。これは「too many connections」エラーやデータベースのパフォーマンス低下を引き起こす原因となります。

また、アイドル接続プールがスライスで実装されていたため、接続の追加や削除の際に効率が低下する可能性がありました。特に、多数の接続が頻繁にプールに出入りするような高負荷なシナリオでは、スライスの再割り当てや要素の移動がオーバーヘッドとなることが考えられます。

さらに、Conn finalCloserdb.mu ロック中に呼び出されるという問題は、デッドロックや競合状態を引き起こす可能性があり、接続のクリーンアッププロセスに悪影響を与える可能性がありました。これらの問題を解決し、より堅牢で効率的なデータベース接続管理を実現するために、このコミットが導入されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の database/sql パッケージに関する概念と、一般的な並行処理の知識が必要です。

  • database/sql パッケージ: Go言語の標準ライブラリの一部で、SQLデータベースとの対話のための汎用インターフェースを提供します。特定のデータベースドライバー(例: github.com/lib/pq for PostgreSQL)と組み合わせて使用されます。
  • データベース接続プール (Connection Pool): データベースへの接続はリソースを消費し、確立に時間がかかるため、アプリケーションは通常、接続を再利用するために接続プールを使用します。接続プールは、使用可能な接続のキャッシュを維持し、必要に応じてアプリケーションに提供します。これにより、接続の確立と切断のオーバーヘッドが削減され、パフォーマンスが向上します。
  • sql.DB: database/sql パッケージの中心的な構造体で、データベースへの抽象的なアクセスを提供します。これはデータベースへの単一の論理的な接続を表し、内部的に接続プールを管理します。sql.DB オブジェクトはアプリケーションのライフサイクル全体で維持されるべきであり、頻繁に開閉すべきではありません。
  • SetMaxIdleConns(n int): アイドル接続プールに保持されるアイドル状態の接続の最大数を設定します。n <= 0 の場合、アイドル接続は保持されません。
  • SetMaxOpenConns(n int): このコミットで追加された関数で、データベースへのオープン接続(使用中およびアイドル状態の接続の合計)の最大数を設定します。n <= 0 の場合、オープン接続数に制限はありません(デフォルト)。この制限に達すると、新しいデータベース操作は既存の接続が利用可能になるまで待機します。
  • driver.Driver インターフェース: database/sql パッケージは、特定のデータベースドライバーが実装すべきインターフェースを定義しています。driver.DriverOpen(name string) (Conn, error) メソッドを持ち、データベースへの新しい接続を確立します。
  • driver.Conn インターフェース: データベースへの単一の接続を表します。
  • sync.Mutex: Go言語の sync パッケージで提供されるミューテックス(相互排他ロック)です。共有リソースへのアクセスを同期するために使用され、複数のゴルーチンが同時に同じデータにアクセスするのを防ぎます。db.musql.DB 構造体内の共有フィールドを保護するために使用されます。
  • finalCloser: Goのランタイムがオブジェクトが不要になったときに呼び出すことができるクリーンアップ関数です。ここでは、driverConn がクローズされる際に呼び出される関数を指します。
  • container/list パッケージ: Go言語の標準ライブラリの一部で、双方向リンクリストを実装しています。要素の追加や削除が効率的で、特にリストの途中からの要素削除や、先頭・末尾への頻繁な追加・削除に適しています。スライスと比較して、要素の移動によるオーバーヘッドが少ないという利点があります。

技術的詳細

このコミットの技術的な詳細は、主に database/sql パッケージ内の DB 構造体とその関連メソッドの変更に集中しています。

  1. SetMaxOpenConns の導入:

    • DB 構造体に maxOpen int フィールドが追加されました。これはオープン接続数の上限を保持します。
    • SetMaxOpenConns(n int) メソッドが追加され、この maxOpen フィールドを設定できるようになりました。n <= 0 の場合は無制限となります。
    • SetMaxOpenConns が呼び出された際、もし maxIdleConnsmaxOpen より大きい場合、maxIdleConnsmaxOpen に合わせて自動的に削減されます。これは、アイドル接続数がオープン接続数の上限を超えることを防ぐためです。
  2. アイドル接続プールの変更 (Slice to container/list):

    • 従来の DB 構造体の freeConn フィールドは []*driverConn (スライス) でしたが、*list.List (双方向リンクリスト) に変更されました。
    • container/list を使用することで、接続の追加 (PushFront) や削除 (Remove) がより効率的になります。スライスの場合、中間要素の削除や先頭への追加は、後続の要素の移動を伴うため、パフォーマンスのボトルネックになる可能性がありました。リンクリストでは、ポインタの付け替えだけで済むため、これらの操作が定数時間で行えます。
    • driverConn 構造体には、listElem *list.Element フィールドが追加され、freeConn リスト内の自身の要素への参照を保持するようになりました。これにより、リストからの接続の削除が容易になります。
  3. 接続要求と接続オープナーの導入:

    • DB 構造体に connRequests *list.List (接続要求のキュー)、numOpen int (現在オープンしている接続数)、pendingOpens int (現在開いている途中の接続数)、openerCh chan struct{} (接続オープナーへのシグナルチャネル) が追加されました。
    • connectionOpener() という新しいゴルーチンが導入されました。このゴルーチンは openerCh からのシグナルを受け取り、新しい接続を非同期に開きます。
    • maybeOpenNewConnections() メソッドが追加され、db.mu がロックされている状態で、接続要求がある場合や接続上限に達していない場合に、openerCh にシグナルを送って新しい接続を開くように促します。
    • connRequest 型が定義され、新しい接続を待つゴルーチンが使用するチャネルを表します。
    • db.conn() メソッドが変更され、maxOpen の制限がある場合やアイドル接続がない場合に、connRequests リストに接続要求を追加し、connectionOpener が接続を開くのを待つようになりました。これにより、接続上限を超えて接続が確立されるのを防ぎ、接続が利用可能になるまでリクエストをブロックします。
  4. Conn finalCloserdb.mu ロック問題の修正:

    • driverConn.closeDBLocked() メソッドのロジックが変更されました。以前は db.mu がロックされた状態で db.removeDepLocked を直接呼び出していましたが、新しい実装では db.removeDepLocked が返す関数を db.mu のロックを解除した後に呼び出すように変更されました。
    • driverConn.finalClose() メソッドも修正され、db.mu のロックを解除した後に db.numOpen--db.maybeOpenNewConnections() を呼び出すようになりました。これにより、finalCloser の実行中に db.mu が不必要にロックされることを防ぎ、デッドロックのリスクを軽減します。
  5. putConnDBLocked の導入:

    • putConnDBLocked という新しいヘルパー関数が導入されました。この関数は、接続要求を処理するか、アイドル接続プールに接続を戻す役割を担います。これにより、接続の解放と再利用のロジックが一元化され、db.mu のロック管理がより適切に行われるようになりました。

これらの変更により、database/sql パッケージはより堅牢な接続プール管理機能を提供し、高負荷な環境でのパフォーマンスと安定性を向上させています。

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

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

  1. DB 構造体の変更:

    // Before
    type DB struct {
    	driver driver.Driver
    	dsn    string
    
    	mu       sync.Mutex // protects following fields
    	freeConn []*driverConn
    	closed   bool
    	dep      map[finalCloser]depSet
    	lastPut  map[*driverConn]string // stacktrace of last conn's put; debug only
    	maxIdle  int                    // zero means defaultMaxIdleConns; negative means 0
    }
    
    // After
    type DB struct {
    	driver driver.Driver
    	dsn    string
    
    	mu           sync.Mutex // protects following fields
    	freeConn     *list.List // of *driverConn  <-- Changed from slice to list
    	connRequests *list.List // of connRequest  <-- New
    	numOpen      int        //                 <-- New
    	pendingOpens int        //                 <-- New
    	openerCh     chan struct{} //             <-- New
    	closed       bool
    	dep          map[finalCloser]depSet
    	lastPut      map[*driverConn]string // stacktrace of last conn's put; debug only
    	maxIdle      int                    // zero means defaultMaxIdleConns; negative means 0
    	maxOpen      int                    // <= 0 means unlimited  <-- New
    }
    
  2. driverConn 構造体の変更:

    // Before (relevant part)
    type driverConn struct {
    	// ...
    }
    
    // After (relevant part)
    type driverConn struct {
    	// ...
    	listElem *list.Element // <-- New: Reference to its element in the freeConn list
    }
    
  3. Open 関数の変更: DB 構造体の初期化時に、freeConnlist.New() で初期化し、connRequestslist.New() で初期化、そして connectionOpener() ゴルーチンを起動するようになりました。

    // Before (relevant part)
    func Open(driverName, dataSourceName string) (*DB, error) {
    	// ...
    	db := &DB{
    		driver:  driveri,
    		dsn:     dataSourceName,
    		lastPut: make(map[*driverConn]string),
    	}
    	return db, nil
    }
    
    // After (relevant part)
    func Open(driverName, dataSourceName string) (*DB, error) {
    	// ...
    	db := &DB{
    		driver:   driveri,
    		dsn:      dataSourceName,
    		openerCh: make(chan struct{}, connectionRequestQueueSize), // <-- New
    		lastPut:  make(map[*driverConn]string),
    	}
    	db.freeConn = list.New()     // <-- New
    	db.connRequests = list.New() // <-- New
    	go db.connectionOpener()     // <-- New
    	return db, nil
    }
    
  4. 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) // Adjust maxIdle if it exceeds new maxOpen
    	}
    }
    
  5. connectionOpener および maybeOpenNewConnections の追加: 新しいゴルーチンとヘルパー関数が追加され、接続の非同期オープンと接続要求の処理を管理します。

  6. db.conn() メソッドの変更: 接続の取得ロジックが変更され、maxOpen の制限と connRequests を考慮するようになりました。

  7. driverConn.finalClose() の変更: db.mu のロックを解除した後に db.numOpen--db.maybeOpenNewConnections() を呼び出すようになりました。

    // Before (relevant part)
    func (dc *driverConn) finalClose() error {
    	// ...
    	dc.Unlock()
    	return err
    }
    
    // After (relevant part)
    func (dc *driverConn) finalClose() error {
    	// ...
    	dc.Unlock()
    
    	dc.db.mu.Lock() // <-- New: Lock db.mu
    	dc.db.numOpen-- // <-- New
    	dc.db.maybeOpenNewConnections() // <-- New
    	dc.db.mu.Unlock() // <-- New: Unlock db.mu
    
    	return err
    }
    
  8. putConnDBLocked ヘルパー関数の追加: 接続をアイドルプールに戻すか、接続要求を満たすための新しいロジックが追加されました。

これらの変更は、database/sql パッケージの接続管理メカニズムを根本的に改善し、より柔軟で堅牢な接続プール機能を提供します。

コアとなるコードの解説

DB 構造体の変更と接続プールの再設計

最も重要な変更は、DB 構造体に maxOpen フィールドが追加され、freeConn がスライスから *list.List に変更されたことです。

  • maxOpen int: このフィールドは、データベースへの同時にオープンできる接続の最大数を保持します。SetMaxOpenConns メソッドを通じて設定され、0 または負の値は無制限を意味します。これにより、アプリケーションはデータベースへの接続数を厳密に制御し、データベースサーバーの過負荷を防ぐことができます。
  • freeConn *list.List: アイドル状態の接続を保持するプールが、[]*driverConn (スライス) から *list.List (双方向リンクリスト) に変更されました。
    • スライスの問題点: スライスは連続したメモリブロックを使用するため、要素の追加や削除(特に中間からの削除)は、後続の要素の移動を伴い、パフォーマンスのオーバーヘッドが発生する可能性があります。接続プールでは、接続の取得(リストの先頭からの削除)や解放(リストの末尾への追加)が頻繁に行われるため、スライスの再割り当てや要素の移動がボトルネックになる可能性がありました。
    • リンクリストの利点: container/list は、各要素が前後の要素へのポインタを持つため、要素の追加や削除がポインタの付け替えだけで済み、定数時間 O(1) で実行できます。これにより、接続プールの操作がより効率的になり、高負荷な環境でのパフォーマンスが向上します。
  • connRequests *list.List: 新しく追加されたこのリストは、新しい接続を待っているゴルーチンからの要求(connRequest)をキューとして保持します。maxOpen の制限に達している場合や、アイドル接続がない場合に、接続を必要とするゴルーチンはここに要求を登録し、接続が利用可能になるまで待機します。
  • numOpen intpendingOpens int: numOpen は現在オープンしている(使用中またはアイドル状態の)接続の総数を追跡します。pendingOpens は、現在開いている途中の接続の数を追跡し、接続オープナーが新しい接続を確立している間、一時的にカウントされます。これにより、接続数の上限を正確に管理できます。
  • openerCh chan struct{}: これは、connectionOpener ゴルーチンに新しい接続を開くようにシグナルを送るためのチャネルです。maybeOpenNewConnections 関数がこのチャネルに値を送信することで、非同期的に接続の確立をトリガーします。

SetMaxOpenConns メソッド

このメソッドは、DB 構造体の maxOpen フィールドを設定し、オープン接続数の上限を定義します。

func (db *DB) SetMaxOpenConns(n int) {
	db.mu.Lock()
	db.maxOpen = n
	if n < 0 {
		db.maxOpen = 0 // 負の値は無制限を意味するため、0に設定
	}
	// maxOpenが設定され、かつmaxIdleConnsがmaxOpenを超える場合、maxIdleConnsを調整
	syncMaxIdle := db.maxOpen > 0 && db.maxIdleConnsLocked() > db.maxOpen
	db.mu.Unlock()
	if syncMaxIdle {
		db.SetMaxIdleConns(n) // maxIdleConnsをmaxOpenに合わせる
	}
}

この関数は、db.mu をロックして maxOpen を安全に更新します。また、maxIdleConns が新しい maxOpen の値を超えないように自動的に調整するロジックが含まれています。これは、アイドル接続数がオープン接続数の上限よりも多くなるという矛盾した状態を防ぐために重要です。

connectionOpener ゴルーチンと接続要求の処理

connectionOpener は、Open 関数で起動される独立したゴルーチンです。

// Runs in a seperate goroutine, opens new connections when requested.
func (db *DB) connectionOpener() {
	for _ = range db.openerCh { // openerChからのシグナルを待機
		db.openNewConnection() // 新しい接続を開く
	}
}

このゴルーチンは db.openerCh からのシグナルを継続的にリッスンし、シグナルを受け取るたびに db.openNewConnection() を呼び出して新しいデータベース接続を確立します。これにより、接続の確立が非同期で行われ、メインの処理がブロックされるのを防ぎます。

db.conn() メソッドは、接続が必要な場合にこの新しい接続管理ロジックを利用します。

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

	// If db.maxOpen > 0 and the number of open connections is over the limit
	// or there are no free connection, then make a request and wait.
	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
}

このロジックにより、maxOpen の制限が尊重され、接続が不足している場合には接続要求がキューに入れられ、connectionOpener によって非同期に処理されるようになります。

Conn finalCloserdb.mu ロック問題の修正

driverConn.finalClose() メソッドの変更は、デッドロックのリスクを軽減するために重要です。

func (dc *driverConn) finalClose() error {
	// ... (既存のクローズ処理)
	dc.Unlock() // driverConnのロックを解除

	// db.muをロックして、DBの状態を更新
	dc.db.mu.Lock()
	dc.db.numOpen-- // オープン接続数をデクリメント
	dc.db.maybeOpenNewConnections() // 新しい接続が必要かチェック
	dc.db.mu.Unlock() // db.muのロックを解除

	return err
}

以前は、finalCloserdb.mu がロックされた状態で呼び出される可能性があり、これがデッドロックの原因となることがありました。この修正では、driverConn のクローズ処理が完了し、driverConn のミューテックスが解除された後に、db.mu をロックして db.numOpen の更新と maybeOpenNewConnections の呼び出しを行うように変更されました。これにより、db.mu のロックが短時間になり、他のゴルーチンとの競合が減少します。

これらの変更は、Goの database/sql パッケージの接続管理を大幅に改善し、より堅牢でスケーラブルなデータベースアプリケーションの開発を可能にします。

関連リンク

参考にした情報源リンク