[インデックス 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
構造体内のマップ(outConn
や onConnPut
)を使用していました。高並行性環境では、これらのマップへのアクセスが頻繁に発生し、マップ操作を保護するためのミューテックス(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.outConn
はdriverConn.inUse bool
に置き換えられました。DB.onConnPut
はdriverConn.onPut []func()
に置き換えられました。
driverConn
構造体は既に自身のミューテックス dc.mu
を持っていますが、inUse
と onPut
フィールドは 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.go
に manyConcurrentQueries
関数が追加され、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
構造体の初期化からoutConn
とonConnPut
マップの作成が削除されました。
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.T
とtesting.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)
+ }
+}
- 高並行性クエリをシミュレートするためのヘルパー関数が追加されました。
TestConcurrency
とBenchmarkConcurrency
がこの関数を利用して、並行性テストとベンチマークを実行するようになりました。
コアとなるコードの解説
このコミットの核心は、database/sql
パッケージのコネクションプール管理におけるデータ構造の変更と、それに伴うミューテックス競合の緩和です。
-
DB
構造体からのマップの削除:- 以前は、
DB
構造体内にoutConn
(使用中のコネクションを管理) とonConnPut
(コネクション返却時のコールバックを管理) という2つのマップがありました。 - これらのマップは、
DB
構造体全体のロックであるdb.mu
によって保護されていました。 - 高並行性環境では、コネクションの取得・返却のたびにこれらのマップへのアクセスが発生し、
db.mu
のロック競合が頻繁に起こり、これがパフォーマンスのボトルネックとなっていました。マップ操作(特に挿入や削除)は、ハッシュ計算やメモリ再割り当てを伴う可能性があり、ロック下での実行はコストが高くなります。
- 以前は、
-
driverConn
構造体への変数の移動:outConn
マップの役割は、driverConn
構造体に追加されたinUse bool
フィールドに引き継がれました。コネクションが使用中かどうかは、そのコネクション自身の構造体内のブール値で直接管理されるようになりました。onConnPut
マップの役割は、driverConn
構造体に追加されたonPut []func()
スライスに引き継がれました。コネクション返却時に実行すべきコールバックは、そのコネクション自身の構造体内のスライスに直接格納されるようになりました。- これらの新しいフィールド (
inUse
,onPut
) は、引き続きdb.mu
によって保護されますが、マップのキー検索や削除といった複雑な操作が不要になります。単なるブール値の変更やスライスへの追加・クリア操作は、マップ操作よりもはるかに軽量です。
-
パフォーマンスへの影響:
- この変更により、
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
の内部構造やコネクションプーリングの概念を理解するために一般的な情報源を参照しました。)
- (特定の記事は参照していませんが、Goの
- ミューテックス競合と並行処理のパフォーマンスに関する一般的な情報
- (並行プログラミングにおけるミューテックスの役割とパフォーマンスへの影響を理解するために一般的な情報源を参照しました。)