Skip to content

Commit 190d0d3

Browse files
bradfitzgopherbot
authored andcommitted
database/sql: optimize connection request pool
This replaces a map used as a set with a slice. We were using a surprising amount of CPU in this code, making mapiters to pull out a random element of the map. Instead, just rand.IntN to pick a random element of the slice. It also adds a benchmark: │ before │ after │ │ sec/op │ sec/op vs base │ ConnRequestSet-8 1818.0n ± 0% 452.4n ± 0% -75.12% (p=0.000 n=10) (whether random is a good policy is a bigger question, but this optimizes the current policy without changing behavior) Updates #66361 Change-Id: I3d456a819cc720c2d18e1befffd2657e5f50f1e7 Reviewed-on: https://go-review.googlesource.com/c/go/+/572119 Reviewed-by: Cherry Mui <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Emmanuel Odeke <[email protected]> Reviewed-by: David Chase <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Brad Fitzpatrick <[email protected]>
1 parent 1ed85ee commit 190d0d3

File tree

3 files changed

+234
-38
lines changed

3 files changed

+234
-38
lines changed

src/database/sql/sql.go

Lines changed: 128 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"errors"
2222
"fmt"
2323
"io"
24+
"math/rand/v2"
2425
"reflect"
2526
"runtime"
2627
"sort"
@@ -497,9 +498,8 @@ type DB struct {
497498

498499
mu sync.Mutex // protects following fields
499500
freeConn []*driverConn // free connections ordered by returnedAt oldest to newest
500-
connRequests map[uint64]chan connRequest
501-
nextRequest uint64 // Next key to use in connRequests.
502-
numOpen int // number of opened and pending open connections
501+
connRequests connRequestSet
502+
numOpen int // number of opened and pending open connections
503503
// Used to signal the need for new connections
504504
// a goroutine running connectionOpener() reads on this chan and
505505
// maybeOpenNewConnections sends on the chan (one send per needed connection)
@@ -814,11 +814,10 @@ func (t dsnConnector) Driver() driver.Driver {
814814
func OpenDB(c driver.Connector) *DB {
815815
ctx, cancel := context.WithCancel(context.Background())
816816
db := &DB{
817-
connector: c,
818-
openerCh: make(chan struct{}, connectionRequestQueueSize),
819-
lastPut: make(map[*driverConn]string),
820-
connRequests: make(map[uint64]chan connRequest),
821-
stop: cancel,
817+
connector: c,
818+
openerCh: make(chan struct{}, connectionRequestQueueSize),
819+
lastPut: make(map[*driverConn]string),
820+
stop: cancel,
822821
}
823822

824823
go db.connectionOpener(ctx)
@@ -922,9 +921,7 @@ func (db *DB) Close() error {
922921
}
923922
db.freeConn = nil
924923
db.closed = true
925-
for _, req := range db.connRequests {
926-
close(req)
927-
}
924+
db.connRequests.CloseAndRemoveAll()
928925
db.mu.Unlock()
929926
for _, fn := range fns {
930927
err1 := fn()
@@ -1223,7 +1220,7 @@ func (db *DB) Stats() DBStats {
12231220
// If there are connRequests and the connection limit hasn't been reached,
12241221
// then tell the connectionOpener to open new connections.
12251222
func (db *DB) maybeOpenNewConnections() {
1226-
numRequests := len(db.connRequests)
1223+
numRequests := db.connRequests.Len()
12271224
if db.maxOpen > 0 {
12281225
numCanOpen := db.maxOpen - db.numOpen
12291226
if numRequests > numCanOpen {
@@ -1297,14 +1294,6 @@ type connRequest struct {
12971294

12981295
var errDBClosed = errors.New("sql: database is closed")
12991296

1300-
// nextRequestKeyLocked returns the next connection request key.
1301-
// It is assumed that nextRequest will not overflow.
1302-
func (db *DB) nextRequestKeyLocked() uint64 {
1303-
next := db.nextRequest
1304-
db.nextRequest++
1305-
return next
1306-
}
1307-
13081297
// conn returns a newly-opened or cached *driverConn.
13091298
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
13101299
db.mu.Lock()
@@ -1352,8 +1341,7 @@ func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn
13521341
// Make the connRequest channel. It's buffered so that the
13531342
// connectionOpener doesn't block while waiting for the req to be read.
13541343
req := make(chan connRequest, 1)
1355-
reqKey := db.nextRequestKeyLocked()
1356-
db.connRequests[reqKey] = req
1344+
delHandle := db.connRequests.Add(req)
13571345
db.waitCount++
13581346
db.mu.Unlock()
13591347

@@ -1365,16 +1353,26 @@ func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn
13651353
// Remove the connection request and ensure no value has been sent
13661354
// on it after removing.
13671355
db.mu.Lock()
1368-
delete(db.connRequests, reqKey)
1356+
deleted := db.connRequests.Delete(delHandle)
13691357
db.mu.Unlock()
13701358

13711359
db.waitDuration.Add(int64(time.Since(waitStart)))
13721360

1373-
select {
1374-
default:
1375-
case ret, ok := <-req:
1376-
if ok && ret.conn != nil {
1377-
db.putConn(ret.conn, ret.err, false)
1361+
// If we failed to delete it, that means something else
1362+
// grabbed it and is about to send on it.
1363+
if !deleted {
1364+
// TODO(bradfitz): rather than this best effort select, we
1365+
// should probably start a goroutine to read from req. This best
1366+
// effort select existed before the change to check 'deleted'.
1367+
// But if we know for sure it wasn't deleted and a sender is
1368+
// outstanding, we should probably block on req (in a new
1369+
// goroutine) to get the connection back.
1370+
select {
1371+
default:
1372+
case ret, ok := <-req:
1373+
if ok && ret.conn != nil {
1374+
db.putConn(ret.conn, ret.err, false)
1375+
}
13781376
}
13791377
}
13801378
return nil, ctx.Err()
@@ -1530,13 +1528,7 @@ func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
15301528
if db.maxOpen > 0 && db.numOpen > db.maxOpen {
15311529
return false
15321530
}
1533-
if c := len(db.connRequests); c > 0 {
1534-
var req chan connRequest
1535-
var reqKey uint64
1536-
for reqKey, req = range db.connRequests {
1537-
break
1538-
}
1539-
delete(db.connRequests, reqKey) // Remove from pending requests.
1531+
if req, ok := db.connRequests.TakeRandom(); ok {
15401532
if err == nil {
15411533
dc.inUse = true
15421534
}
@@ -3529,3 +3521,104 @@ func withLock(lk sync.Locker, fn func()) {
35293521
defer lk.Unlock() // in case fn panics
35303522
fn()
35313523
}
3524+
3525+
// connRequestSet is a set of chan connRequest that's
3526+
// optimized for:
3527+
//
3528+
// - adding an element
3529+
// - removing an element (only by the caller who added it)
3530+
// - taking (get + delete) a random element
3531+
//
3532+
// We previously used a map for this but the take of a random element
3533+
// was expensive, making mapiters. This type avoids a map entirely
3534+
// and just uses a slice.
3535+
type connRequestSet struct {
3536+
// s are the elements in the set.
3537+
s []connRequestAndIndex
3538+
}
3539+
3540+
type connRequestAndIndex struct {
3541+
// req is the element in the set.
3542+
req chan connRequest
3543+
3544+
// curIdx points to the current location of this element in
3545+
// connRequestSet.s. It gets set to -1 upon removal.
3546+
curIdx *int
3547+
}
3548+
3549+
// CloseAndRemoveAll closes all channels in the set
3550+
// and clears the set.
3551+
func (s *connRequestSet) CloseAndRemoveAll() {
3552+
for _, v := range s.s {
3553+
close(v.req)
3554+
}
3555+
s.s = nil
3556+
}
3557+
3558+
// Len returns the length of the set.
3559+
func (s *connRequestSet) Len() int { return len(s.s) }
3560+
3561+
// connRequestDelHandle is an opaque handle to delete an
3562+
// item from calling Add.
3563+
type connRequestDelHandle struct {
3564+
idx *int // pointer to index; or -1 if not in slice
3565+
}
3566+
3567+
// Add adds v to the set of waiting requests.
3568+
// The returned connRequestDelHandle can be used to remove the item from
3569+
// the set.
3570+
func (s *connRequestSet) Add(v chan connRequest) connRequestDelHandle {
3571+
idx := len(s.s)
3572+
// TODO(bradfitz): for simplicity, this always allocates a new int-sized
3573+
// allocation to store the index. But generally the set will be small and
3574+
// under a scannable-threshold. As an optimization, we could permit the *int
3575+
// to be nil when the set is small and should be scanned. This works even if
3576+
// the set grows over the threshold with delete handles outstanding because
3577+
// an element can only move to a lower index. So if it starts with a nil
3578+
// position, it'll always be in a low index and thus scannable. But that
3579+
// can be done in a follow-up change.
3580+
idxPtr := &idx
3581+
s.s = append(s.s, connRequestAndIndex{v, idxPtr})
3582+
return connRequestDelHandle{idxPtr}
3583+
}
3584+
3585+
// Delete removes an element from the set.
3586+
//
3587+
// It reports whether the element was deleted. (It can return false if a caller
3588+
// of TakeRandom took it meanwhile, or upon the second call to Delete)
3589+
func (s *connRequestSet) Delete(h connRequestDelHandle) bool {
3590+
idx := *h.idx
3591+
if idx < 0 {
3592+
return false
3593+
}
3594+
s.deleteIndex(idx)
3595+
return true
3596+
}
3597+
3598+
func (s *connRequestSet) deleteIndex(idx int) {
3599+
// Mark item as deleted.
3600+
*(s.s[idx].curIdx) = -1
3601+
// Copy last element, updating its position
3602+
// to its new home.
3603+
if idx < len(s.s)-1 {
3604+
last := s.s[len(s.s)-1]
3605+
*last.curIdx = idx
3606+
s.s[idx] = last
3607+
}
3608+
// Zero out last element (for GC) before shrinking the slice.
3609+
s.s[len(s.s)-1] = connRequestAndIndex{}
3610+
s.s = s.s[:len(s.s)-1]
3611+
}
3612+
3613+
// TakeRandom returns and removes a random element from s
3614+
// and reports whether there was one to take. (It returns ok=false
3615+
// if the set is empty.)
3616+
func (s *connRequestSet) TakeRandom() (v chan connRequest, ok bool) {
3617+
if len(s.s) == 0 {
3618+
return nil, false
3619+
}
3620+
pick := rand.IntN(len(s.s))
3621+
e := s.s[pick]
3622+
s.deleteIndex(pick)
3623+
return e.req, true
3624+
}

src/database/sql/sql_test.go

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"math/rand"
1515
"reflect"
1616
"runtime"
17+
"slices"
1718
"strings"
1819
"sync"
1920
"sync/atomic"
@@ -2983,7 +2984,7 @@ func TestConnExpiresFreshOutOfPool(t *testing.T) {
29832984
return
29842985
}
29852986
db.mu.Lock()
2986-
ct := len(db.connRequests)
2987+
ct := db.connRequests.Len()
29872988
db.mu.Unlock()
29882989
if ct > 0 {
29892990
return
@@ -4803,3 +4804,104 @@ func BenchmarkGrabConn(b *testing.B) {
48034804
release(nil)
48044805
}
48054806
}
4807+
4808+
func TestConnRequestSet(t *testing.T) {
4809+
var s connRequestSet
4810+
wantLen := func(want int) {
4811+
t.Helper()
4812+
if got := s.Len(); got != want {
4813+
t.Errorf("Len = %d; want %d", got, want)
4814+
}
4815+
if want == 0 && !t.Failed() {
4816+
if _, ok := s.TakeRandom(); ok {
4817+
t.Fatalf("TakeRandom returned result when empty")
4818+
}
4819+
}
4820+
}
4821+
reset := func() { s = connRequestSet{} }
4822+
4823+
t.Run("add-delete", func(t *testing.T) {
4824+
reset()
4825+
wantLen(0)
4826+
dh := s.Add(nil)
4827+
wantLen(1)
4828+
if !s.Delete(dh) {
4829+
t.Fatal("failed to delete")
4830+
}
4831+
wantLen(0)
4832+
if s.Delete(dh) {
4833+
t.Error("delete worked twice")
4834+
}
4835+
wantLen(0)
4836+
})
4837+
t.Run("take-before-delete", func(t *testing.T) {
4838+
reset()
4839+
ch1 := make(chan connRequest)
4840+
dh := s.Add(ch1)
4841+
wantLen(1)
4842+
if got, ok := s.TakeRandom(); !ok || got != ch1 {
4843+
t.Fatalf("wrong take; ok=%v", ok)
4844+
}
4845+
wantLen(0)
4846+
if s.Delete(dh) {
4847+
t.Error("unexpected delete after take")
4848+
}
4849+
})
4850+
t.Run("get-take-many", func(t *testing.T) {
4851+
reset()
4852+
m := map[chan connRequest]bool{}
4853+
const N = 100
4854+
var inOrder, backOut []chan connRequest
4855+
for range N {
4856+
c := make(chan connRequest)
4857+
m[c] = true
4858+
s.Add(c)
4859+
inOrder = append(inOrder, c)
4860+
}
4861+
if s.Len() != N {
4862+
t.Fatalf("Len = %v; want %v", s.Len(), N)
4863+
}
4864+
for s.Len() > 0 {
4865+
c, ok := s.TakeRandom()
4866+
if !ok {
4867+
t.Fatal("failed to take when non-empty")
4868+
}
4869+
if !m[c] {
4870+
t.Fatal("returned item not in remaining set")
4871+
}
4872+
delete(m, c)
4873+
backOut = append(backOut, c)
4874+
}
4875+
if len(m) > 0 {
4876+
t.Error("items remain in expected map")
4877+
}
4878+
if slices.Equal(inOrder, backOut) { // N! chance of flaking; N=100 is fine
4879+
t.Error("wasn't random")
4880+
}
4881+
})
4882+
}
4883+
4884+
func BenchmarkConnRequestSet(b *testing.B) {
4885+
var s connRequestSet
4886+
for range b.N {
4887+
for range 16 {
4888+
s.Add(nil)
4889+
}
4890+
for range 8 {
4891+
if _, ok := s.TakeRandom(); !ok {
4892+
b.Fatal("want ok")
4893+
}
4894+
}
4895+
for range 8 {
4896+
s.Add(nil)
4897+
}
4898+
for range 16 {
4899+
if _, ok := s.TakeRandom(); !ok {
4900+
b.Fatal("want ok")
4901+
}
4902+
}
4903+
if _, ok := s.TakeRandom(); ok {
4904+
b.Fatal("unexpected ok")
4905+
}
4906+
}
4907+
}

src/go/build/deps_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,9 @@ var depsRules = `
321321
# databases
322322
FMT
323323
< database/sql/internal
324-
< database/sql/driver
325-
< database/sql;
324+
< database/sql/driver;
325+
326+
database/sql/driver, math/rand/v2 < database/sql;
326327
327328
# images
328329
FMT, compress/lzw, compress/zlib

0 commit comments

Comments
 (0)