Slide 1

Slide 1 text

database/sqlの仕組みについて Takuma Shibuya Go Conference 2022 Spring

Slide 2

Slide 2 text

自己紹介 Takuma Shibuya Twitter @sivchari GitHub sivchari Company: Cyber Agent Contributor Go Kubernetes golangci-lint Go Conference 2021 Autumn Go COnference 2022 Spring

Slide 3

Slide 3 text

内容 - database/sqlの概要 - database/sqlの内部実装 - goroutine safeなコネクションプールの管理

Slide 4

Slide 4 text

database/sqlの概要 01

Slide 5

Slide 5 text

database/sqlって何? - Goが提供するDB操作を行うための標準パッケージ - 実際に利用する際はdatabase/sqlと任意のSQL Driverを用い、database/sqlを介 して利用する - database/sqlはSQL(or SQL-Like)のためのinterfaceを提供している → MySQLのように特定の実装をしていない → 具体的な実装は各SQL Driverが実装する(interfaceを満たせばOK) (e.g. go-sql-driver(MySQL), pq(PostgreSQL), sqlite3(SQLite3)) 公式のwikiには56個掲載されている

Slide 6

Slide 6 text

ORM - GORM - GORMが提供しているDriverがdatabase/sqlを満たすように実装している - Ent - database/sqlの実装をラップして拡張している → ORMはdatabase/sqlとサポートするdriverをラップすることでinterfaceを満たしながら機 能を拡張している

Slide 7

Slide 7 text

database/sqlの内部実装 02

Slide 8

Slide 8 text

サンプルコード package main import ( "context" "database/sql" _ "github.com/go-sql-driver/mysql" ) func main() { ctx := context.Background() db, _ := sql.Open("mysql", "dsn") _ = db.PingContext(ctx) }

Slide 9

Slide 9 text

サンプルコード package main import ( "context" "database/sql" _ "github.com/go-sql-driver/mysql" ) func main() { ctx := context.Background() db, _ := sql.Open("mysql", "dsn") _ = db.PingContext(ctx) }

Slide 10

Slide 10 text

Blank import Go言語仕様 インポート宣言は、インポートするパッケージとインポートされるパッケージの依存関係を宣言 するものである。パッケージがそれ自身を直接または間接的にインポートすることや、エクス ポートされた識別子を一切参照せずにパッケージを直接インポートすることは違法です。パッ ケージの副作用(初期化)のためだけにパッケージをインポートするには、明示的なパッケージ 名として空白の識別子を使用します。

Slide 11

Slide 11 text

Blank import Go言語仕様 インポート宣言は、インポートするパッケージとインポートされるパッケージの依存関係を宣言 するものである。パッケージがそれ自身を直接または間接的にインポートすることや、エクス ポートされた識別子を一切参照せずにパッケージを直接インポートすることは違法です。 パッ ケージの副作用(初期化)のためだけにパッケージをインポートするには、明示的なパッケージ 名として空白の識別子を使用します。

Slide 12

Slide 12 text

パッケージの副作用(初期化) - Goのinitは組み込みの初期化関数 - 依存先のパッケージにinitがある場合先に依存先が解決される - initを同一パッケージで複数書いた場合上に書いてある initから順番に解決される

Slide 13

Slide 13 text

サンプルコード package main import ( "context" "database/sql" _ "github.com/go-sql-driver/mysql" ) func main() { ctx := context.Background() db, _ := sql.Open("mysql", "dsn") _ = db.PingContext(ctx) }

Slide 14

Slide 14 text

mysql/driver.go func init() { sql.Register("mysql", &MySQLDriver{}) }

Slide 15

Slide 15 text

mysql/driver.go func init() { sql.Register("mysql", &MySQLDriver{}) }

Slide 16

Slide 16 text

sql.go // Register makes a database driver available by the provided name. // If Register is called twice with the same name or if driver is nil, // it panics. // 和訳 // Registerは与えられたname引数から利用可能なdatabase driverを作成します。 // もしRegisterが同じname引数で2度呼ばれたり、nilであればpanicします。 func Register(name string, driver driver.Driver) { driversMu.Lock() defer driversMu.Unlock() if driver == nil { panic("sql: Register driver is nil") } if _, dup := drivers[name]; dup { panic("sql: Register called twice for driver " + name) } drivers[name] = driver }

Slide 17

Slide 17 text

sql.go // Register makes a database driver available by the provided name. // If Register is called twice with the same name or if driver is nil, // it panics. // 和訳 // Registerは与えられたname引数から利用可能なdatabase driverを作成します。 // もしRegisterが同じname引数で2度呼ばれたり、nilであればpanicします。 func Register(name string, driver driver.Driver) { driversMu.Lock() defer driversMu.Unlock() if driver == nil { panic("sql: Register driver is nil") } if _, dup := drivers[name]; dup { panic("sql: Register called twice for driver " + name) } drivers[name] = driver }

Slide 18

Slide 18 text

sql.go // Register makes a database driver available by the provided name. // If Register is called twice with the same name or if driver is nil, // it panics. // 和訳 // Registerは与えられたname引数から利用可能なdatabase driverを作成します。 // もしRegisterが同じname引数で2度呼ばれたり、nilであればpanicします。 func Register(name string, driver driver.Driver) { driversMu.Lock() defer driversMu.Unlock() if driver == nil { panic("sql: Register driver is nil") } if _, dup := drivers[name]; dup { panic("sql: Register called twice for driver " + name) } drivers[name] = driver }

Slide 19

Slide 19 text

sql.go // Register makes a database driver available by the provided name. // If Register is called twice with the same name or if driver is nil, // it panics. // 和訳 // Registerは与えられたname引数から利用可能なdatabase driverを作成します。 // もしRegisterが同じname引数で2度呼ばれたり、nilであればpanicします。 func Register(name string, driver driver.Driver) { driversMu.Lock() defer driversMu.Unlock() if driver == nil { panic("sql: Register driver is nil") } if _, dup := drivers[name]; dup { panic("sql: Register called twice for driver " + name) } drivers[name] = driver }

Slide 20

Slide 20 text

sql.go // Register makes a database driver available by the provided name. // If Register is called twice with the same name or if driver is nil, // it panics. // 和訳 // Registerは与えられたname引数から利用可能なdatabase driverを作成します。 // もしRegisterが同じname引数で2度呼ばれたり、nilであればpanicします。 func Register(name string, driver driver.Driver) { driversMu.Lock() defer driversMu.Unlock() if driver == nil { panic("sql: Register driver is nil") } if _, dup := drivers[name]; dup { panic("sql: Register called twice for driver " + name) } drivers[name] = driver }

Slide 21

Slide 21 text

同じname引数で2度呼んでみる package main import ( "database/sql" // initで"mysql"をkeyとしてすでにRegisterが呼ばれている。 "github.com/go-sql-driver/mysql" ) func init() { sql.Register("mysql", &mysql.MySQLDriver{}) } func main() { }

Slide 22

Slide 22 text

同一DB DriverのRegisterを防ぐ - go-mysql - go-sql-driver

Slide 23

Slide 23 text

サンプルコード package main import ( "context" "database/sql" _ "github.com/go-sql-driver/mysql" ) func main() { ctx := context.Background() db, _ := sql.Open("mysql", "dsn") _ = db.PingContext(ctx) }

Slide 24

Slide 24 text

sql.Open func Open(driverName, dataSourceName string) (*DB, error) { driversMu.RLock() driveri, ok := drivers[driverName] driversMu.RUnlock() if !ok { return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName) } if driverCtx, ok := driveri.(driver.DriverContext); ok { connector, err := driverCtx.OpenConnector(dataSourceName) if err != nil { return nil, err } return OpenDB(connector), nil } return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil }

Slide 25

Slide 25 text

sql.Open func Open(driverName, dataSourceName string) (*DB, error) { driversMu.RLock() driveri, ok := drivers[driverName] driversMu.RUnlock() if !ok { return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName) } if driverCtx, ok := driveri.(driver.DriverContext); ok { connector, err := driverCtx.OpenConnector(dataSourceName) if err != nil { return nil, err } return OpenDB(connector), nil } return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil }

Slide 26

Slide 26 text

sql.Open func Open(driverName, dataSourceName string) (*DB, error) { driversMu.RLock() driveri, ok := drivers[driverName] driversMu.RUnlock() if !ok { return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName) } if driverCtx, ok := driveri.(driver.DriverContext); ok { connector, err := driverCtx.OpenConnector(dataSourceName) if err != nil { return nil, err } return OpenDB(connector), nil } return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil }

Slide 27

Slide 27 text

OpenDB - driver.DriverContextを満たしていればDBコネクションを取得する - 実際にconnectionを取得するopenNewConnectionでは内部で抽象化されたConnect を呼ぶ → Driverの実装に依存するため、Ping/PingContextを呼ぶようにコメントされている

Slide 28

Slide 28 text

サンプルコード package main import ( "context" "database/sql" _ "github.com/go-sql-driver/mysql" ) func main() { ctx := context.Background() db, _ := sql.Open("mysql", "dsn") _ = db.PingContext(ctx) }

Slide 29

Slide 29 text

Ping func (db *DB) Ping() error { return db.PingContext(context.Background()) }

Slide 30

Slide 30 text

PingContext func (db *DB) PingContext(ctx context.Context) error { var dc *driverConn var err error var isBadConn bool for i := 0; i < maxBadConnRetries; i++ { dc, err = db.conn(ctx, cachedOrNewConn) isBadConn = errors.Is(err, driver.ErrBadConn) if !isBadConn { break } } if isBadConn { dc, err = db.conn(ctx, alwaysNewConn) } if err != nil { return err } return db.pingDC(ctx, dc, dc.releaseConn) }

Slide 31

Slide 31 text

PingContext func (db *DB) PingContext(ctx context.Context) error { var dc *driverConn var err error var isBadConn bool for i := 0; i < maxBadConnRetries; i++ { dc, err = db.conn(ctx, cachedOrNewConn) isBadConn = errors.Is(err, driver.ErrBadConn) if !isBadConn { break } } if isBadConn { dc, err = db.conn(ctx, alwaysNewConn) } if err != nil { return err } return db.pingDC(ctx, dc, dc.releaseConn) }

Slide 32

Slide 32 text

PingContext func (db *DB) PingContext(ctx context.Context) error { var dc *driverConn var err error var isBadConn bool for i := 0; i < maxBadConnRetries; i++ { dc, err = db.conn(ctx, cachedOrNewConn) isBadConn = errors.Is(err, driver.ErrBadConn) if !isBadConn { break } } if isBadConn { dc, err = db.conn(ctx, alwaysNewConn) } if err != nil { return err } return db.pingDC(ctx, dc, dc.releaseConn) }

Slide 33

Slide 33 text

PingContext func (db *DB) PingContext(ctx context.Context) error { var dc *driverConn var err error var isBadConn bool for i := 0; i < maxBadConnRetries; i++ { dc, err = db.conn(ctx, cachedOrNewConn) isBadConn = errors.Is(err, driver.ErrBadConn) if !isBadConn { break } } if isBadConn { dc, err = db.conn(ctx, alwaysNewConn) } if err != nil { return err } return db.pingDC(ctx, dc, dc.releaseConn) }

Slide 34

Slide 34 text

db.pingDC func (db *DB) pingDC(ctx context.Context, dc *driverConn, release func(error)) error { var err error if pinger, ok := dc.ci.(driver.Pinger); ok { withLock(dc, func() { err = pinger.Ping(ctx) }) } release(err) return err }

Slide 35

Slide 35 text

db.pingDC func (db *DB) pingDC(ctx context.Context, dc *driverConn, release func(error)) error { var err error if pinger, ok := dc.ci.(driver.Pinger); ok { withLock(dc, func() { err = pinger.Ping(ctx) }) } release(err) return err }

Slide 36

Slide 36 text

db.pingDC func (db *DB) pingDC(ctx context.Context, dc *driverConn, release func(error)) error { var err error if pinger, ok := dc.ci.(driver.Pinger); ok { withLock(dc, func() { err = pinger.Ping(ctx) }) } release(err) return err }

Slide 37

Slide 37 text

releaseConn func (dc *driverConn) releaseConn(err error) { dc.db.putConn(dc, err, true) }

Slide 38

Slide 38 text

goroutine safeなコネクションプー ルの管理 03

Slide 39

Slide 39 text

goroutine safe sql.DB DB is a database handle representing a pool of zero or more underlying connections. It's safe for concurrent use by multiple goroutines. sql.DBはgoroutine safeであることを保証している(e.g. net/http) もし対策していないとリクエストごとの goroutineがコネクションプールを操作するため raceが発 生する可能性がある。 E.g. https://github.com/golang/go/blob/b55a2fb3b0d67b346bac871737b862f16e5a6447/src/net/http/server. go#L3010

Slide 40

Slide 40 text

Goals of the sql and sql/driver package - 並行処理をうまく処理する。ユーザーはデータベースの接続ごとのスレッドセーフ (goroutine safe)の問題を気にするべきではなく、コネクションプールを自分で管理 すべきでもない。 - sql.DBはインスタンスを共有することが可能であるべき。 - 複数のgoroutineが余分な同期を取る必要がない。

Slide 41

Slide 41 text

登場人物 - mu sync.Mutex sql.DBをgoroutine safeにするためのMutex - freeConn []*driverConn idle状態のconnection, 空きがあればここから使う - connRequests map[uint64]chan connRequest freeConnに空きがない時の待ち行列 - nextRequest uint64 待ち行列から次に実行する connRequest - cleanerCh chan struct{} SetConnMaxIdleTime, SetConnMaxLifeTimeの値などでfreeConnをcleanにする - func (db *DB) connectionCleaner() {} cleanerCh、SetConnMaxXXXの値などで定期実行する

Slide 42

Slide 42 text

相関図 cleaner freeConn connRequests DB.conn releaseConn DB.Exec DB.Query DB.Ping Tx, Prepare etc..

Slide 43

Slide 43 text

相関図 cleaner freeConn connRequests DB.conn releaseConn DB.Exec DB.Query DB.Ping Tx, Prepare etc.. 設定値などを基にして freeConnからコネクション を取得 なければconnRequests へ

Slide 44

Slide 44 text

DB.Exec/ExecContext → DB.exec → db.conn func (db *DB) exec(ctx context.Context, quey string, args []any, strategy connReuseStrategy) (Result, error) { dc, err := db.conn(ctx, strategy) if err != nil { return nil, err } return db.execDC(ctx, dc, dc.releaseConn, query, args) }

Slide 45

Slide 45 text

DB.Query/QueryContext → DB.query → db.conn func (db *DB) query(ctx context.Context, query string, args []any, strategy connReuseStrategy) (*Rows, error) { dc, err := db.conn(ctx, strategy) if err != nil { return nil, err } return db.queryDC(ctx, nil, dc, dc.releaseConn, query, args) }

Slide 46

Slide 46 text

DB.conn if strategy == cachedOrNewConn && last >= 0 { // Reuse the lowest idle time connection so we can close // connections which remain idle as soon as possible. conn := db.freeConn[last] db.freeConn = db.freeConn[:last] conn.inUse = true if conn.expired(lifetime) { db.maxLifetimeClosed++ db.mu.Unlock() conn.Close() return nil, driver.ErrBadConn } db.mu.Unlock() // Reset the session if required. if err := conn.resetSession(ctx); errors.Is(err, driver.ErrBadConn) { conn.Close() return nil, err } return conn, nil }

Slide 47

Slide 47 text

DB.conn if strategy == cachedOrNewConn && last >= 0 { // Reuse the lowest idle time connection so we can close // connections which remain idle as soon as possible. conn := db.freeConn[last] db.freeConn = db.freeConn[:last] conn.inUse = true if conn.expired(lifetime) { db.maxLifetimeClosed++ db.mu.Unlock() conn.Close() return nil, driver.ErrBadConn } db.mu.Unlock() // Reset the session if required. if err := conn.resetSession(ctx); errors.Is(err, driver.ErrBadConn) { conn.Close() return nil, err } return conn, nil }

Slide 48

Slide 48 text

DB.conn if db.maxOpen > 0 && db.numOpen >= db.maxOpen { // Make the connRequest channel. It's buffered so that the // connectionOpener doesn't block while waiting for the req to be read. req := make(chan connRequest, 1) reqKey := db.nextRequestKeyLocked() db.connRequests[reqKey] = req db.waitCount++ db.mu.Unlock() }

Slide 49

Slide 49 text

DB.conn if db.maxOpen > 0 && db.numOpen >= db.maxOpen { // Make the connRequest channel. It's buffered so that the // connectionOpener doesn't block while waiting for the req to be read. req := make(chan connRequest, 1) reqKey := db.nextRequestKeyLocked() db.connRequests[reqKey] = req db.waitCount++ db.mu.Unlock() }

Slide 50

Slide 50 text

相関図 cleaner freeConn connRequests DB.conn releaseConn DB.Exec DB.Query DB.Ping Tx, Prepare etc.. 実行

Slide 51

Slide 51 text

相関図 cleaner freeConn connRequests DB.conn releaseConn DB.Exec DB.Query DB.Ping Tx, Prepare etc.. *driverConnのreceiver method SQL実行後に呼ばれる

Slide 52

Slide 52 text

releaseConn func (dc *driverConn) releaseConn(err error) { dc.db.putConn(dc, err, true) }

Slide 53

Slide 53 text

putConn dc.inUse = false dc.returnedAt = nowFunc() // nowFunc returns the current time; it's overridden in tests. var nowFunc = time.Now added := db.putConnDBLocked(dc, nil) db.mu.Unlock() if !added { dc.Close() return }

Slide 54

Slide 54 text

putConn dc.inUse = false dc.returnedAt = nowFunc() // nowFunc returns the current time; it's overridden in tests. var nowFunc = time.Now added := db.putConnDBLocked(dc, nil) db.mu.Unlock() if !added { dc.Close() return }

Slide 55

Slide 55 text

putConn dc.inUse = false dc.returnedAt = nowFunc() // nowFunc returns the current time; it's overridden in tests. var nowFunc = time.Now added := db.putConnDBLocked(dc, nil) db.mu.Unlock() if !added { dc.Close() return }

Slide 56

Slide 56 text

db.putConnDBLocked if db.maxOpen > 0 && db.numOpen > db.maxOpen { return false } if c := len(db.connRequests); c > 0 { var req chan connRequest var reqKey uint64 for reqKey, req = range db.connRequests { break } delete(db.connRequests, reqKey) // Remove from pending requests. if err == nil { dc.inUse = true } req <- connRequest{ conn: dc, err: err, } return true } else if err == nil && !db.closed { if db.maxIdleConnsLocked() > len(db.freeConn) { db.freeConn = append(db.freeConn, dc) db.startCleanerLocked() return true } db.maxIdleClosed++ } return false

Slide 57

Slide 57 text

db.putConnDBLocked if db.maxOpen > 0 && db.numOpen > db.maxOpen { return false }

Slide 58

Slide 58 text

db.putConnDBLocked if c := len(db.connRequests); c > 0 { var req chan connRequest var reqKey uint64 for reqKey, req = range db.connRequests { break } delete(db.connRequests, reqKey) // Remove from pending requests. if err == nil { dc.inUse = true } req <- connRequest{ // DB.connでselect で受け取っている conn: dc, err: err, } return true }

Slide 59

Slide 59 text

db.putConnDBLocked } else if err == nil && !db.closed { if db.maxIdleConnsLocked() > len(db.freeConn) { db.freeConn = append(db.freeConn, dc) db.startCleanerLocked() return true } db.maxIdleClosed++ }

Slide 60

Slide 60 text

db.putConnDBLocked } else if err == nil && !db.closed { if db.maxIdleConnsLocked() > len(db.freeConn) { db.freeConn = append(db.freeConn, dc) db.startCleanerLocked() return true } db.maxIdleClosed++ // DB.Stats用 }

Slide 61

Slide 61 text

putConn dc.inUse = false dc.returnedAt = nowFunc() // nowFunc returns the current time; it's overridden in tests. var nowFunc = time.Now added := db.putConnDBLocked(dc, nil) db.mu.Unlock() if !added { dc.Close() return }

Slide 62

Slide 62 text

相関図 cleaner freeConn connRequests DB.conn releaseConn DB.Exec DB.Query DB.Ping Tx, Prepare etc.. connRequestsがないな らfreeConnへ

Slide 63

Slide 63 text

相関図 cleaner freeConn connRequests DB.conn releaseConn DB.Exec DB.Query DB.Ping Tx, Prepare etc.. connRequestsがないな らfreeConnへ timer/cleanerChを受 け取ると実行

Slide 64

Slide 64 text

DB.startCleanerLocked // startCleanerLocked starts connectionCleaner if needed. func (db *DB) startCleanerLocked() { if (db.maxLifetime > 0 || db.maxIdleTime > 0) && db.numOpen > 0 && db.cleanerCh == nil { db.cleanerCh = make(chan struct{}, 1) go db.connectionCleaner(db.shortestIdleTimeLocked()) } }

Slide 65

Slide 65 text

DB.startCleanerLocked // startCleanerLocked starts connectionCleaner if needed. func (db *DB) startCleanerLocked() { if (db.maxLifetime > 0 || db.maxIdleTime > 0) && db.numOpen > 0 && db.cleanerCh == nil { db.cleanerCh = make(chan struct{}, 1) go db.connectionCleaner(db.shortestIdleTimeLocked()) } }

Slide 66

Slide 66 text

DB.connectionCleaner func (db *DB) connectionCleaner(d time.Duration) { for { select { case <-t.C: case <-db.cleanerCh: // maxLifetime was changed or db was closed. } d, closing := db.connectionCleanerRunLocked(d) db.mu.Unlock() for _, c := range closing { c.Close() } } }

Slide 67

Slide 67 text

DB.connectionCleaner func (db *DB) connectionCleaner(d time.Duration) { for { select { case <-t.C: case <-db.cleanerCh: // maxLifetime was changed or db was closed. } d, closing := db.connectionCleanerRunLocked(d) db.mu.Unlock() for _, c := range closing { c.Close() } } }

Slide 68

Slide 68 text

DB.connectionCleaner func (db *DB) connectionCleaner(d time.Duration) { for { select { case <-t.C: case <-db.cleanerCh: // maxLifetime was changed or db was closed. } d, closing := db.connectionCleanerRunLocked(d) db.mu.Unlock() for _, c := range closing { c.Close() } } }

Slide 69

Slide 69 text

DB.connectionCleanerRunLocked func (db *DB) connectionCleanerRunLocked(d time.Duration) (time.Duration, []*driverConn) { var closing []*driverConn if db.maxIdleTime > 0 { for i := last; i >= 0; i-- { // snip closing = db.freeConn[:i:i] } } if db.maxLifetime > 0 { for i := 0; i < len(db.freeConn); i++ { if c.createdAt.Before(expiredSince) { closing = append(closing, c) } } } return d, closing }

Slide 70

Slide 70 text

DB.connectionCleanerRunLocked func (db *DB) connectionCleanerRunLocked(d time.Duration) (time.Duration, []*driverConn) { var closing []*driverConn if db.maxIdleTime > 0 { for i := last; i >= 0; i-- { // snip closing = db.freeConn[:i:i] } } if db.maxLifetime > 0 { for i := 0; i < len(db.freeConn); i++ { if c.createdAt.Before(expiredSince) { closing = append(closing, c) } } } return d, closing }

Slide 71

Slide 71 text

DB.connectionCleaner func (db *DB) connectionCleaner(d time.Duration) { for { select { case <-t.C: case <-db.cleanerCh: // maxLifetime was changed or db was closed. } d, closing := db.connectionCleanerRunLocked(d) db.mu.Unlock() for _, c := range closing { c.Close() } } }

Slide 72

Slide 72 text

まとめ - database/sqlの拡張性の高さ - Interfaceをどのように使うか - 抽象的に書くことであらゆるDBに対応できる - Goのtips的な書き方 - time.NowをnowFuncとして置いておく - for rangeで最後のconnを取得する - Goの言語仕様への理解 - Blank import

Slide 73

Slide 73 text

Thank you for watching !