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

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

このコミットは、Go言語の標準ライブラリである database/sql パッケージにおける、高並行性下での応答時間の標準偏差を改善することを目的としています。具体的には、コネクションプール管理において発生していたマップ検索のオーバーヘッドを削減し、コネクションに関連する変数を driverConn 構造体自体に移動させることで、パフォーマンスの安定性向上を図っています。

コミット

commit 4f1ef563cc7fa110937516be2fe847a48e700135
Author: James Tucker <raggi@google.com>
Date:   Wed Apr 3 11:13:40 2013 -0700

    database/sql: improve standard deviation response time under high concurrency
    
    See https://github.com/raggi/go-and-java for runtime benchmark.
    
    The patch reduces the amount of map key search, moving connection oriented
    variables onto the connection structs.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/8092045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/4f1ef563cc7fa110937516be2fe847a48e700135

元コミット内容

database/sql: improve standard deviation response time under high concurrency

See https://github.com/raggi/go-and-java for runtime benchmark.

The patch reduces the amount of map key search, moving connection oriented
variables onto the connection structs.

R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/8092045

変更の背景

この変更の背景には、Go言語の database/sql パッケージが、特に高並行性環境下でデータベースコネクションを管理する際に、パフォーマンスの不安定性、具体的には応答時間の標準偏差の増大という課題を抱えていたことがあります。

コミットメッセージに記載されている https://github.com/raggi/go-and-java のランタイムベンチマークは、GoとJavaのデータベースアクセス性能を比較したものであり、Goの database/sql パッケージが特定のシナリオでJavaに比べてパフォーマンスのばらつきが大きいことを示唆していた可能性があります。

従来の database/sql の実装では、データベースコネクションの状態(使用中かどうか、コネクション返却時に実行すべきコールバックなど)を管理するために、DB 構造体内のマップ(outConnonConnPut)を使用していました。高並行性環境では、これらのマップへのアクセスが頻繁に発生し、マップ操作を保護するためのミューテックス(sync.Mutex)の競合がボトルネックとなっていました。ミューテックスの競合は、スレッドがロックの解放を待つ時間を増加させ、結果として応答時間のばらつき(標準偏差の増大)を引き起こします。

このコミットは、このマップ検索とミューテックス競合の問題を緩和し、高負荷時でもより安定したパフォーマンスを提供することを目指しています。

前提知識の解説

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

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

主要な概念は以下の通りです。

  • DB 構造体: データベースへの接続プールを表します。複数のゴルーチンから安全に利用できるように設計されており、コネクションの取得、解放、管理を行います。
  • driver.Driver インターフェース: データベースドライバが実装すべきインターフェースです。Open メソッドを持ち、データベースへの新しいコネクション(driver.Conn)を作成します。
  • driver.Conn インターフェース: データベースへの単一の物理的なコネクションを表します。トランザクションの開始、ステートメントの準備、クエリの実行などを行います。
  • コネクションプール: database/sql パッケージは内部的にコネクションプールを管理します。これにより、データベースへの新しいコネクションを確立するオーバーヘッドを削減し、既存のコネクションを再利用することでパフォーマンスを向上させます。コネクションは使用後にプールに返却され、再利用可能になります。

sync.Mutex とミューテックス競合

sync.Mutex はGo言語における相互排他ロック(ミューテックス)の実装です。複数のゴルーチンが共有リソース(この場合は DB 構造体の内部状態、特にマップ)に同時にアクセスする際に、データ競合を防ぐために使用されます。

  • Lock(): ミューテックスをロックします。既にロックされている場合、呼び出し元のゴルーチンはロックが解放されるまでブロックされます。
  • Unlock(): ミューテックスをアンロックします。

ミューテックス競合 (Mutex Contention) は、複数のゴルーチンが同時に同じミューテックスのロックを取得しようとするときに発生します。競合が激しい場合、多くのゴルーチンがロックの解放を待つことになり、並行性が低下し、全体のスループットが減少したり、応答時間のばらつきが増大したりします。

マップ (map) の利用とパフォーマンス

Goのマップは、キーと値のペアを格納するための組み込みデータ構造です。非常に効率的ですが、マップへの同時書き込みアクセスはデータ競合を引き起こすため、sync.Mutex などのロック機構で保護する必要があります。高並行性環境でマップへのアクセスが頻繁に行われる場合、そのマップを保護するミューテックスがボトルネックとなり、パフォーマンスに影響を与える可能性があります。

応答時間の標準偏差

パフォーマンス測定において、応答時間の「標準偏差」は、応答時間のばらつきの度合いを示します。標準偏差が小さいほど、応答時間は安定しており、予測可能です。逆に、標準偏差が大きいほど、応答時間は大きく変動し、一部のリクエストが非常に遅くなる可能性があります。高並行性システムでは、安定した応答時間が求められるため、標準偏差の改善は重要な目標となります。

技術的詳細

このコミットの主要な技術的アプローチは、「マップ検索の削減」と「コネクション指向変数の driverConn 構造体への移動」です。

1. DB 構造体からのマップの削除と driverConn への移動

変更前、DB 構造体は以下のマップを持っていました。

  • outConn map[*driverConn]bool: 現在使用中のコネクションを追跡するためのマップ。キーは *driverConn、値はダミーの bool
  • onConnPut map[*driverConn][]func(): コネクションがプールに返却される際に実行すべきコールバック関数を格納するマップ。

これらのマップは、DB 構造体のミューテックス db.mu によって保護されていました。コネクションの取得 (db.conn()) や返却 (db.putConn()) のたびに、これらのマップへのアクセス(検索、追加、削除)が発生し、高並行性下では db.mu の競合が激しくなっていました。

このコミットでは、これらのマップを削除し、関連する情報を driverConn 構造体自体に移動させました。

  • DB.outConndriverConn.inUse bool に置き換えられました。
  • DB.onConnPutdriverConn.onPut []func() に置き換えられました。

driverConn 構造体は既に自身のミューテックス dc.mu を持っていますが、inUseonPut フィールドは db.mu によって保護されるようになりました。これにより、DB レベルでのマップ検索が不要になり、db.mu の競合が緩和されます。

2. コネクションの状態管理の変更

  • コネクションの使用中状態の追跡:

    • 変更前: db.outConn[conn] = true でコネクションを使用中としてマークし、delete(db.outConn, dc) で解放していました。
    • 変更後: conn.inUse = true でコネクションを使用中としてマークし、dc.inUse = false で解放します。これにより、マップのキー検索と削除のオーバーヘッドがなくなります。
  • コネクション返却時のコールバック管理:

    • 変更前: db.onConnPut[c] = append(db.onConnPut[c], func()...) でコールバックを追加し、コネクション返却時に db.onConnPut[dc] から取得して実行し、delete(db.onConnPut, dc) で削除していました。
    • 変更後: c.onPut = append(c.onPut, func()...) でコールバックを driverConn 構造体内のスライスに追加し、コネクション返却時に dc.onPut から取得して実行し、dc.onPut = nil でスライスをクリアします。これにより、マップのキー検索と削除のオーバーヘッドがなくなります。

3. テストケースの追加

sql_test.gomanyConcurrentQueries 関数が追加され、TestConcurrency および BenchmarkConcurrency で使用されるようになりました。これは、多数のゴルーチンが同時にデータベースクエリを実行するシナリオをシミュレートし、この変更が実際に高並行性下でのパフォーマンス改善に寄与するかどうかを検証するためのものです。特に、runtime.GOMAXPROCS を設定してCPUコア数を制限し、ミューテックス競合が発生しやすい状況を作り出しています。

これらの変更により、DB 構造体内のグローバルなマップへのアクセスが減少し、コネクションごとの状態管理がより局所化されるため、db.mu のロック粒度が粗くなることを防ぎ、高並行性下でのミューテックス競合が緩和され、結果として応答時間の標準偏差が改善されると期待されます。

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

src/pkg/database/sql/sql.go

DB 構造体の変更

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -192,14 +192,12 @@ type DB struct {
 	driver driver.Driver
 	dsn    string
 
-	mu        sync.Mutex           // protects following fields
-	outConn   map[*driverConn]bool // whether the conn is in use
-	freeConn  []*driverConn
-	closed    bool
-	dep       map[finalCloser]depSet
-	onConnPut map[*driverConn][]func() // code (with mu held) run when conn is next returned
-	lastPut   map[*driverConn]string   // stacktrace of last conn's put; debug only
-	maxIdle   int                      // zero means defaultMaxIdleConns; negative means 0
+	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
 }
  • outConn (使用中コネクションを追跡するマップ) と onConnPut (コネクション返却時コールバックのマップ) が削除されました。

driverConn 構造体の変更

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -212,6 +210,10 @@ type driverConn struct {
 	sync.Mutex // guards following
 	ci         driver.Conn
 	closed     bool
+
+	// guarded by db.mu
+	inUse bool
+	onPut []func() // code (with db.mu held) run when conn is next returned
 }
  • inUse (コネクションが使用中かどうかを示すブール値) と onPut (コネクション返却時に実行されるコールバック関数のスライス) が追加されました。これらは db.mu によって保護されます。

Open 関数の変更

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -341,11 +343,9 @@ func Open(driverName, dataSourceName string) (*DB, error) {
 		return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
 	}
 	db := &DB{
-		driver:    driveri,
-		dsn:       dataSourceName,
-		outConn:   make(map[*driverConn]bool),
-		lastPut:   make(map[*driverConn]string),
-		onConnPut: make(map[*driverConn][]func()),
+		driver:  driveri,
+		dsn:     dataSourceName,
+		lastPut: make(map[*driverConn]string),
 	}
 	return db, nil
 }
  • DB 構造体の初期化から outConnonConnPut マップの作成が削除されました。

conn 関数の変更

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -427,7 +427,7 @@ func (db *DB) conn() (*driverConn, error) {
 	if n := len(db.freeConn); n > 0 {
 		conn := db.freeConn[n-1]
 		db.freeConn = db.freeConn[:n-1]
-		db.outConn[conn] = true
+		conn.inUse = true
 		db.mu.Unlock()
 		return conn, nil
 	}
@@ -443,7 +443,7 @@ func (db *DB) conn() (*driverConn, error) {
 	}
 	db.mu.Lock()
 	db.addDepLocked(dc, dc)
-	db.outConn[dc] = true
+	dc.inUse = true
 	db.mu.Unlock()
 	return dc, nil
 }
  • コネクションを使用中としてマークする際に、db.outConn[conn] = true の代わりに conn.inUse = true を使用するようになりました。

connIfFree 関数の変更

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -456,7 +456,7 @@ func (db *DB) conn() (*driverConn, error) {
 func (db *DB) connIfFree(wanted *driverConn) (conn *driverConn, ok bool) {
 	db.mu.Lock()
 	defer db.mu.Unlock()
-	if db.outConn[wanted] {
+	if wanted.inUse {
 		return conn, false
 	}
 	for i, conn := range db.freeConn {
@@ -465,7 +465,7 @@ func (db *DB) connIfFree(wanted *driverConn) (conn *driverConn, ok bool) {
 		}
 		db.freeConn[i] = db.freeConn[len(db.freeConn)-1]
 		db.freeConn = db.freeConn[:len(db.freeConn)-1]
-		db.outConn[wanted] = true
+		wanted.inUse = true
 		return wanted, true
 	}
 	return nil, false
  • コネクションが使用中かどうかをチェックする際に、db.outConn[wanted] の代わりに wanted.inUse を使用するようになりました。

noteUnusedDriverStatement 関数の変更

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -480,8 +480,8 @@ var putConnHook func(*DB, *driverConn)
 func (db *DB) noteUnusedDriverStatement(c *driverConn, si driver.Stmt) {
 	db.mu.Lock()
 	defer db.mu.Unlock()
-	if db.outConn[c] {
-		db.onConnPut[c] = append(db.onConnPut[c], func() {
+	if c.inUse {
+		c.onPut = append(c.onPut, func() {
 			si.Close()
 		})
 	} else {
  • コールバックを追加する際に、db.onConnPut[c] の代わりに c.onPut を使用するようになりました。

putConn 関数の変更

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -497,7 +497,7 @@ const debugGetPut = false
 // err is optionally the last error that occurred on this connection.
 func (db *DB) putConn(dc *driverConn, err error) {
 	db.mu.Lock()
-	if !db.outConn[dc] {
+	if !dc.inUse {
 		if debugGetPut {
 			fmt.Printf("putConn(%v) DUPLICATE was: %s\n\nPREVIOUS was: %s", dc, stack(), db.lastPut[dc])
 		}
@@ -506,14 +506,12 @@ func (db *DB) putConn(dc *driverConn, err error) {
 	if debugGetPut {
 		db.lastPut[dc] = stack()
 	}
-	delete(db.outConn, dc)
+	dc.inUse = false
 
-	if fns, ok := db.onConnPut[dc]; ok {
-		for _, fn := range fns {
-			fn()
-		}
-		delete(db.onConnPut, dc)
+	for _, fn := range dc.onPut {
+		fn()
 	}
+	dc.onPut = nil
 
 	if err == driver.ErrBadConn {
 		// Don't reuse bad connections.
  • コネクションを解放する際に、delete(db.outConn, dc) の代わりに dc.inUse = false を使用するようになりました。
  • コールバックを実行する際に、db.onConnPut[dc] から取得する代わりに dc.onPut から取得し、実行後に delete(db.onConnPut, dc) の代わりに dc.onPut = nil でスライスをクリアするようになりました。

src/pkg/database/sql/sql_test.go

testOrBench インターフェースの追加

--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -36,7 +38,14 @@ const fakeDBName = "foo"\n 
 var chrisBirthday = time.Unix(123456789, 0)
 
-func newTestDB(t *testing.T, name string) *DB {
+type testOrBench interface {
+	Fatalf(string, ...interface{})
+	Errorf(string, ...interface{})
+	Fatal(...interface{})
+	Error(...interface{})
+}
+
+func newTestDB(t testOrBench, name string) *DB {
 	db, err := Open("test", fakeDBName)
 	if err != nil {
 		t.Fatalf("Open: %v", err)
  • testing.Ttesting.B の両方を受け入れられるように、testOrBench インターフェースが導入されました。

manyConcurrentQueries 関数の追加

--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -844,3 +853,63 @@ func TestCloseConnBeforeStmts(t *testing.T) {
 	t.Errorf("after Stmt Close, driverConn's Conn interface should be nil")
 }
+
+func manyConcurrentQueries(t testOrBench) {
+	maxProcs, numReqs := 16, 500
+	if testing.Short() {
+		maxProcs, numReqs = 4, 50
+	}
+	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(maxProcs))
+
+	db := newTestDB(t, "people")
+	defer closeDB(t, db)
+
+	stmt, err := db.Prepare("SELECT|people|name|")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(numReqs)
+
+	reqs := make(chan bool)
+	defer close(reqs)
+
+	for i := 0; i < maxProcs*2; i++ {
+		go func() {
+			for _ = range reqs {
+				rows, err := stmt.Query()
+				if err != nil {
+					t.Errorf("error on query:  %v", err)
+					wg.Done()
+					continue
+				}
+
+				var name string
+				for rows.Next() {
+					rows.Scan(&name)
+				}
+				rows.Close()
+
+				wg.Done()
+			}
+		}()
+	}
+
+	for i := 0; i < numReqs; i++ {
+		reqs <- true
+	}
+
+	wg.Wait()
+}
+
+func TestConcurrency(t *testing.T) {
+	manyConcurrentQueries(t)
+}
+
+func BenchmarkConcurrency(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		manyConcurrentQueries(b)
+	}
+}
  • 高並行性クエリをシミュレートするためのヘルパー関数が追加されました。
  • TestConcurrencyBenchmarkConcurrency がこの関数を利用して、並行性テストとベンチマークを実行するようになりました。

コアとなるコードの解説

このコミットの核心は、database/sql パッケージのコネクションプール管理におけるデータ構造の変更と、それに伴うミューテックス競合の緩和です。

  1. DB 構造体からのマップの削除:

    • 以前は、DB 構造体内に outConn (使用中のコネクションを管理) と onConnPut (コネクション返却時のコールバックを管理) という2つのマップがありました。
    • これらのマップは、DB 構造体全体のロックである db.mu によって保護されていました。
    • 高並行性環境では、コネクションの取得・返却のたびにこれらのマップへのアクセスが発生し、db.mu のロック競合が頻繁に起こり、これがパフォーマンスのボトルネックとなっていました。マップ操作(特に挿入や削除)は、ハッシュ計算やメモリ再割り当てを伴う可能性があり、ロック下での実行はコストが高くなります。
  2. driverConn 構造体への変数の移動:

    • outConn マップの役割は、driverConn 構造体に追加された inUse bool フィールドに引き継がれました。コネクションが使用中かどうかは、そのコネクション自身の構造体内のブール値で直接管理されるようになりました。
    • onConnPut マップの役割は、driverConn 構造体に追加された onPut []func() スライスに引き継がれました。コネクション返却時に実行すべきコールバックは、そのコネクション自身の構造体内のスライスに直接格納されるようになりました。
    • これらの新しいフィールド (inUse, onPut) は、引き続き db.mu によって保護されますが、マップのキー検索や削除といった複雑な操作が不要になります。単なるブール値の変更やスライスへの追加・クリア操作は、マップ操作よりもはるかに軽量です。
  3. パフォーマンスへの影響:

    • この変更により、db.mu のロック下で実行される処理が大幅に簡素化され、マップ操作のオーバーヘッドがなくなりました。
    • 結果として、db.mu のロック保持時間が短縮され、ミューテックス競合が緩和されます。
    • ミューテックス競合の緩和は、ゴルーチンがロックの解放を待つ時間を減らし、並行処理のスループットを向上させ、特に高負荷時における応答時間のばらつき(標準偏差)を減少させる効果があります。
    • 追加されたベンチマークテスト (manyConcurrentQueries) は、この改善を定量的に評価するために設計されており、高並行性シナリオでのパフォーマンス安定性向上を確認できます。

このコミットは、Goの database/sql パッケージが、高並行性環境下でより堅牢で予測可能なパフォーマンスを提供するための重要な最適化です。

関連リンク

  • ランタイムベンチマーク: https://github.com/raggi/go-and-java
    • このコミットの背景となったパフォーマンス課題を示すベンチマークです。
  • Go CL (Change List): https://golang.org/cl/8092045
    • Goのコードレビューシステムにおけるこの変更の元の提案ページです。

参考にした情報源リンク

  • Go言語公式ドキュメント: database/sql パッケージ
  • Go言語公式ドキュメント: sync パッケージ
  • Go言語におけるコネクションプーリングと database/sql の内部動作に関する一般的な解説記事
    • (特定の記事は参照していませんが、Goの database/sql の内部構造やコネクションプーリングの概念を理解するために一般的な情報源を参照しました。)
  • ミューテックス競合と並行処理のパフォーマンスに関する一般的な情報
    • (並行プログラミングにおけるミューテックスの役割とパフォーマンスへの影響を理解するために一般的な情報源を参照しました。)