golang sql packet connection pool analysis

golang when using mysql will use the database / sql library, each time with it in the black box, it is necessary to sort out what the whole process and details of the request, in order to avoid problems after the encounter ideas will be solved.

A few questions before reading

  • Sql connection pool of how maintenance?
  • How to Query / Exec Gets join query?
  • How to release the connection pool?

Several important structural

DB struct

Let's look at DB structure, which is the core structure sql package. DB is a database handles zero or more underlying connection pool, concurrent safe.

sql package can automatically create and release the connection, it maintains a free connection pool. If the database has a concept of each connection state, it can only be reliably observed in this state in the transaction. After calling DB.Begin, Tx return to bind to a single connection. After calling Commit or Rollback on matters connected with the transaction will return to the idle DB connection pool. SetMaxIdleConns used to control the size of the connection pool.

type DB struct {
    driver driver.Driver // 数据库驱动
    dsn    string // 数据库连接参数,比如 username,hostname,password 等等
    numClosed uint64 // numClosed 是一个原子计数器,表示已关闭连接的总数。Stmt.openStmt 在清除 Stmt.css 中的已关闭连接之前对其进行检查。

    mu           sync.Mutex // 保护下面的字段
    freeConn     []*driverConn // 空闲连接
    connRequests map[uint64]chan connRequest // 阻塞请求队列。当达到最大连接数时,后续请求将插入该队列来等待可用连接
    nextRequest  uint64 // connRequests 的下一个 key
    numOpen      int    // 已连接或者正等待连接的数量

    // 一个创建新连接的信号,
    // 运行connectionOpener()的goroutine读取此chan,maybeOpenNewConnections发送此chan(每个需要的连接发送一次)
    // 它在db.Close()时关闭,并通知connectionOpener goroutine退出。
    openerCh    chan struct{} 
    closed      bool
    dep         map[finalCloser]depSet
    lastPut     map[*driverConn]string // 用于 debug
    maxIdle     int                    // 最大空闲连接数, 0等价于 defaultMaxIdleConns 常量(代码中值为2),负数等价于0
    maxOpen     int                    // 数据库的最大连接数,0 等价于不限制最大连接数
    maxLifetime time.Duration          // 连接的最大生命周期
    cleanerCh   chan struct{} // 用于释放连接池中过期的连接的信号
}
driverConn struct

driverConn using a mutex driver.Conn package structure, held on during all calls Conn (including any calls to the interface returned by the Conn, for example, a call to Tx, Stmt, Result, Rows) of

type driverConn struct {
    db        *DB
    createdAt time.Time

    sync.Mutex  // 保护下面的字段
    ci          driver.Conn
    closed      bool
    finalClosed bool // ci.Close 已经被调用则为 true
    openStmt    map[*driverStmt]bool

    // 下面的字段被 db.mu 保护
    inUse      bool
    onPut      []func() // 下次返回 conn 时运行的代码
    dbmuClosed bool     // 与 closed 字段相同,但由 db.mu 保护,用于 removeClosedStmtLocked
}

// driver.Conn 是具体的接口 用来支持不同的数据库
// Conn 是与数据库的连接,不是 gotoutines 安全的。
// Conn 被认为是有状态的。
type Conn interface {
    // Prepare 返回绑定到该连接的就绪语句 Stmt。
    Prepare(query string) (Stmt, error)

    // Close 使当前就绪的语句和事务无效并可能停止,将此连接标记为不再使用。
    //
    // 因为sql包维护一个空闲的连接池,并且只有在空闲连接过剩时才调用Close,所以驱动不需要做自己的连接缓存。
    Close() error

    // Begin 启动并返回一个新的事务 Tx
    Begin() (Tx, error)
}

// 可以看到driverConn的这个方法,看名字就知道是释放连接的 调用了DB 的 putConn 方法,这里先留个印象
func (dc *driverConn) releaseConn(err error) {
    dc.db.putConn(dc, err)
}

Binding registration drive

We need to use when using the specified database import _ "github.com/go-sql-driver/mysql"to perform the init () function. The init () function is mainly used to register the specified database driver to map a type of drivers sql variable.

mysql/driver.go

// 该方法注册到驱动,也就是db.Open的调用,返回的mc是实现的driver.Conn接口的结构,dsn 为连接该数据库的配置
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
    // New mysqlConn
    mc := &mysqlConn{
        maxAllowedPacket: maxPacketSize,
        maxWriteSize:     maxPacketSize - 1,
        closech:          make(chan struct{}),
    }
    ...
    return mc, nil
}

// 注册驱动 MySQLDriver 结构实现了 driver.Driver 接口
func init() {
    sql.Register("mysql", &MySQLDriver{})
}

connection

Connection setup process
8573331-96acfb3f69fab8ee.png
Create a connection

It can be seen calling sql.Open time will start to read a goroutine been blocked db.openerCh. When this openerCh signal is received, the process will start to create a connection, calls for creating a drive to provide a method of connection to create a connection. If you create a successful, priority connection to the change db.connRequestsin the blocking request to use, if not blocked the request put this new connection into db.freeConnthe pending requests.

关键方法
// 调用驱动的 Open 方法创建新连接
func (db *DB) openNewConnection() {
    // 创建新连接
    ci, err := db.driver.Open(db.dsn)
    db.mu.Lock()
    defer db.mu.Unlock()
    if db.closed {
        if err == nil {
            ci.Close()
        }
        db.numOpen--
        return
    }
    if err != nil {
        db.numOpen--
        db.putConnDBLocked(nil, err)
        db.maybeOpenNewConnections()
        return
    }
    dc := &driverConn{
        db:        db,
        createdAt: nowFunc(),
        ci:        ci,
    }
    // 直接给阻塞的请求使用 或者 放入连接池
    if db.putConnDBLocked(dc, err) {
        db.addDepLocked(dc, dc)
    } else {
        db.numOpen--
        ci.Close()
    }
}

// 给 阻塞在 connRequest 队列的请求分配连接
func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
    if db.closed {
        return false
    }
    // 如果超过最大连接,直接返回false,connRequest 队列的请求继续阻塞
    if db.maxOpen > 0 && db.numOpen > db.maxOpen {
        return false
    }
    // 把连接分配给 connRequests 阻塞的请求
    if c := len(db.connRequests); c > 0 {
        var req chan connRequest
        var reqKey uint64
        // 取第一个
        for reqKey, req = range db.connRequests {
            break
        }
        // 阻塞的请求得到连接 删除 connRequests 的记录
        delete(db.connRequests, reqKey)
        // 标记该连接正在使用
        if err == nil {
            dc.inUse = true
        }
        // 通过 chan 把该连接发送给请求
        req <- connRequest{
            conn: dc,
            err:  err,
        }
        return true
    // 如果空闲连接数小于最大连接限制 把该连接放到 freeConn 中
    } else if err == nil && !db.closed && db.maxIdleConnsLocked() > len(db.freeConn) {
        db.freeConn = append(db.freeConn, dc)
        // 根据 db.maxLifetime 起一个 goroutine 清除freeConn中过期的连接 
        db.startCleanerLocked()
        return true
    }
    return false
}

查询如何获取连接

先来看提供的两个基本的查询的方法 Query / Exec

// 使用方法
db.Query("SELECT * FROM table")
db.Exec("INSERT INTO table VALUES (1)")
  • Query:执行需要返回 rows 的操作,例如 SELECT)不释放连接,但在调用后仍然保持连接,即放回 freeConn。
  • Exec:执行没有返回 rows 的操作,例如 INSERT, UPDATE,DELETE)在调用后自动释放连接。


    8573331-375104ed181be944.png
    Query 查询
关键方法
// conn 返回新打开的连接,或者从连接池freeConn中取
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
    ...
    // 检查上下文是否被取消
    select {
    default:
    case <-ctx.Done():
        db.mu.Unlock()
        return nil, ctx.Err()
    }
    lifetime := db.maxLifetime

    // cachedOrNewConn 模式获取连接
    numFree := len(db.freeConn)
    if strategy == cachedOrNewConn && numFree > 0 {
        conn := db.freeConn[0]
        copy(db.freeConn, db.freeConn[1:])
        db.freeConn = db.freeConn[:numFree-1]
        conn.inUse = true
        db.mu.Unlock()
        if conn.expired(lifetime) {
            conn.Close()
            return nil, driver.ErrBadConn
        }
        return conn, nil
    }

    // 如果连接数已经超过限制,将该请求放入connRequest中阻塞,直到有空闲连接
    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.mu.Unlock()

        // 上下文判断请求超时
        select {
        case <-ctx.Done():
            // 删除 connRequests 中阻塞的请求
            db.mu.Lock()
            delete(db.connRequests, reqKey)
            db.mu.Unlock()
            select {
            default:
            case ret, ok := <-req:
                if ok {
                    // 如果收到了连接,由于超时了,回收该连接
                    db.putConn(ret.conn, ret.err)
                }
            }
            return nil, ctx.Err()
        // 获取到了连接,返回处理
        case ret, ok := <-req:
            if !ok {
                return nil, errDBClosed
            }
            if ret.err == nil && ret.conn.expired(lifetime) {
                ret.conn.Close()
                return nil, driver.ErrBadConn
            }
            return ret.conn, ret.err
        }
    }

    // 连接池中没有连接,且打开的连接数没有超限,创建新连接
    db.numOpen++ // optimistically
    db.mu.Unlock()
    ci, err := db.driver.Open(db.dsn)
    if err != nil {
        db.mu.Lock()
        db.numOpen-- // correct for earlier optimism
        db.maybeOpenNewConnections()
        db.mu.Unlock()
        return nil, err
    }
    db.mu.Lock()
    dc := &driverConn{
        db:        db,
        createdAt: nowFunc(),
        ci:        ci,
    }
    db.addDepLocked(dc, dc)
    dc.inUse = true
    db.mu.Unlock()
    return dc, nil
}

连接的回收或释放

被动回收或释放

我们沿着上面的 Query 请求分析下来,在 queryConn 的方法中会看到一个 releaseConn 的方法,它调用了putConn方法去处理这个 dc 连接。

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

再来看看 putConn 方法的定义

// 把 dc 连接放回连接池 freeConn 或者释放
func (db *DB) putConn(dc *driverConn, err error) {
    db.mu.Lock()
    // 回收一个没被使用的连接 会panic
    if !dc.inUse {
        if debugGetPut {
            fmt.Printf("putConn(%v) DUPLICATE was: %s\n\nPREVIOUS was: %s", dc, stack(), db.lastPut[dc])
        }
        panic("sql: connection returned that was never out")
    }
    if debugGetPut {
        db.lastPut[dc] = stack()
    }
    // 将该连接置为 未使用
    dc.inUse = false

    // 执行完该连接的函数
    for _, fn := range dc.onPut {
        fn()
    }
    dc.onPut = nil
    // 不重用无效的连接
    if err == driver.ErrBadConn {
        // 该函数会判断 阻塞在 connRequest 的请求数量,然后在不超限的情况下,通过 openerCh 唤醒 connectionOpener goroutine 创建新连接处理请求。
        db.maybeOpenNewConnections()
        db.mu.Unlock()
        // 释放连接
        dc.Close()
        return
    }
    if putConnHook != nil {
        putConnHook(db, dc)
    }
    // 如果是有效的连接,将该连接给 阻塞在 connRequest 的请求使用,或者放回连接池
    added := db.putConnDBLocked(dc, nil)
    db.mu.Unlock()

    // 改连接没被回收,释放
    if !added {
        dc.Close()
    }
}
主动回收或释放

除了上述的连接回收释放方式,还有没有其他地方回收释放呢。当我们设置 db.SetConnMaxLifetime 也就是设置连接的最大存活时间时,都会调起一个 goroutine 负责处理连接池中过期的连接。同 openerCh 信号一样,释放也用到了一个 cleanerCh 用于通知该 goroutine 处理任务。

func (db *DB) SetConnMaxLifetime(d time.Duration) {
    if d < 0 {
        d = 0
    }
    db.mu.Lock()
    // 当缩小 maxLifetime 的时候,直接清理不符的连接
    if d > 0 && d < db.maxLifetime && db.cleanerCh != nil {
        select {
        case db.cleanerCh <- struct{}{}:
        default:
        }
    }
    db.maxLifetime = d
    // 该方法会起一个 goroutine  负责释放过期连接
    db.startCleanerLocked()
    db.mu.Unlock()
}

// 满足条件开启一个 goroutine 维护过期的连接
func (db *DB) startCleanerLocked() {
    if db.maxLifetime > 0 && db.numOpen > 0 && db.cleanerCh == nil {
        db.cleanerCh = make(chan struct{}, 1)
        go db.connectionCleaner(db.maxLifetime)
    }
}

// 核心的逻辑
func (db *DB) connectionCleaner(d time.Duration) {
    const minInterval = time.Second

    if d < minInterval {
        d = minInterval
    }
    t := time.NewTimer(d)

    // 阻塞等待 cleanerCh 信号
    for {
        select {
        case <-t.C:
        case <-db.cleanerCh: // maxLifetime 修改 或者 db 关闭会发送该信号
        }

        db.mu.Lock()
        d = db.maxLifetime
        if db.closed || db.numOpen == 0 || d <= 0 {
            db.cleanerCh = nil
            db.mu.Unlock()
            return
        }

        expiredSince := nowFunc().Add(-d)
        var closing []*driverConn
        // 从连接池 freeConn 获取过期连接
        for i := 0; i < len(db.freeConn); i++ {
            c := db.freeConn[i]
            if c.createdAt.Before(expiredSince) {
                closing = append(closing, c)
                last := len(db.freeConn) - 1
                db.freeConn[i] = db.freeConn[last]
                db.freeConn[last] = nil
                db.freeConn = db.freeConn[:last]
                i--
            }
        }
        db.mu.Unlock()
        // 释放连接
        for _, c := range closing {
            c.Close()
        }

        if d < minInterval {
            d = minInterval
        }
        t.Reset(d)
    }
}

小结

现在回过头来看看开始的三个问题,基本就有解了。

  • sql 的连接池的连接怎么维护的?
    有效的连接存储在连接池 freeConn 中。启用一个connectionOpener goroutine 通过接受 openerCh 信号负责调用驱动的 Open 方法创建连接。当用db.SetConnMaxLifetime 设置 MaxLifetime 或者调用 putConnDBLocked方法满足条件时候会启用一个 connectionCleanergoroutine 通过接受cleanerCh信号负责清理连接。
  • Query / Exec 如何获取查询的连接?
    1. 先查看 freeConn 是否有可用的连接,如果有就从连接池取。如果没有进入下一步。
    2. Determining whether the number of current connections overrun. If the overrun, the request is blocked waiting for an available connection into connRequests. If you do not overrun the next step.
    3. Create a new connection
  • How to connect the connection pool recovery / release?
    1. Passive recovery / release. An error is returned by the query and other operations will be executed when the releaseConnfunction recovery connection. It will play when a condition is satisfied connectionCleanergoroutine clean up invalid connection pool connection.
    2. Active recovery / release. Setting db.SetConnMaxLifetimetime will trigger a connectionCleanergoroutine clean up the connection pool.

You can see the connection pool implementation mechanism sql database it is still quite complex, producers connectionOpenergoroutine blocking monitor openerCh create a connection into the connection pool. When requests come, first check there is no connection pool idle connections, if there is no idle connection is created, the request Query class continues to run back into the connection pool reuse. When the call is connected need to clean up the connection pool connectionCleanergoroutine. Analyzed again, encounter problems in the future will be faster processing.

Reproduced in: https: //www.jianshu.com/p/45ea39e79de3

Guess you like

Origin blog.csdn.net/weixin_34411563/article/details/91241879