Golang database connection pool technology principle and implementation

1 Why do you need a connection pool?

If you don't use a connection pool, it is more expensive to create a connection for each request, so you need to complete 3 tcp handshakes. At the same time, in high-concurrency scenarios, since there is no limit on the maximum number of connections in the connection pool, countless connections can be created and file descriptors will be exhausted. The connection pool is to reuse some created connections.

2 Connection pool design

Basically, the connection pool will design the following parameters:

Initial number of connections : The number of connections that will be pre-created when the connection pool is initialized, if set:

  • Too Big: Potentially Wasteful

  • Too small: a new connection needs to be created when the request comes

The maximum number of idle connections maxIdle : the maximum number of cached connections in the pool, if set:

  • Too large: cause waste, you still need to control the connection. Because the overall number of connections to the database is limited, if the current process takes up too much, other processes can get less

  • Too small: unable to cope with burst traffic

The maximum number of connections maxCap :

  • If you have already used maxCap connections, when you want to apply for the maxCap+1th connection, it will generally block there until it times out or someone else returns a connection

The maximum idle time idleTimeout : When a connection is found to be idle for more than this time, it will be closed and the connection will be obtained again

  • Avoid the problem that the connection is useless for a long time and automatically expires

The connection pool provides two methods to the outside world, Get: Get a connection, Put: Return a connection. The implementation of most connection pools is similar, and the basic process is as follows:

picture

3 Golang standard library SQL connection pool

Golang's connection pool is implemented under the standard library database/sql/sql.go. When we run:

db, err := sql.Open("mysql", "xxxx")

A connection pool will be opened. You can look at the structure of the returned db:

type DB struct {
    // Atomic access only. At top of struct to prevent mis-alignment
    // on 32-bit platforms. Of type time.Duration.
    waitDuration int64 // 等待新连接的总时间,用于统计

    connector driver.Connector // 由数据库驱动实现的连接器
    // numClosed is an atomic counter which represents a total number of
    // closed connections. Stmt.openStmt checks it before cleaning closed
    // connections in Stmt.css.
    numClosed uint64 // 关闭的连接数

    mu           sync.Mutex // 锁
    freeConn     []*driverConn // 可用连接池
    connRequests map[uint64]chan connRequest // 连接请求表,key 是分配的自增键
    nextRequest  uint64 // 连接请求的自增键
    numOpen      int    // 已经打开 + 即将打开的连接数
    // Used to signal the need for new connections
    // a goroutine running connectionOpener() reads on this chan and
    // maybeOpenNewConnections sends on the chan (one send per needed connection)
    // It is closed during db.Close(). The close tells the connectionOpener
    // goroutine to exit.
    openerCh          chan struct{} // 告知 connectionOpener 需要新的连接
    resetterCh        chan *driverConn // connectionResetter 函数,连接放回连接池的时候会用到
    closed            bool
    dep               map[finalCloser]depSet
    lastPut           map[*driverConn]string // debug 时使用,记录上一个放回的连接
    maxIdle           int                    // 连接池大小,默认大小为 2,<= 0 时不使用连接池
    maxOpen           int                    // 最大打开的连接数,<= 0 不限制
    maxLifetime       time.Duration          // 一个连接可以被重用的最大时限,也就是它在连接池中的最大存活时间,0 表示可以一直重用
    cleanerCh         chan struct{} // 告知 connectionCleaner 清理连接
    waitCount         int64 // 等待的连接总数
    maxIdleClosed     int64 // 释放连接时,因为连接池已满而被关闭的连接总数
    maxLifetimeClosed int64 // 因为超过存活时间而被关闭的连接总数

    stop func() // stop cancels the connection opener and the session resetter.
}
 
 

It can be seen that freeConn, the internal storage connection structure of the DB connection pool, is not the chan used before, but []*driverConn, a connection slice.


// driverConn wraps a driver.Conn with a mutex, to
// be held during all calls into the Conn. (including any calls onto
// interfaces returned via that Conn, such as calls on Tx, Stmt,
// Result, Rows)
type driverConn struct {
    db        *DB // 数据库句柄
    createdAt time.Time

    sync.Mutex  // 锁
    ci          driver.Conn // 对应具体的连接
    closed      bool // 是否标记关闭
    finalClosed bool // 是否最终关闭
    openStmt    map[*driverStmt]bool // 在这个连接上打开的状态
    lastErr     error // connectionResetter 的返回结果

    // guarded by db.mu
    inUse      bool // 连接是否占用
    onPut      []func() // 连接归还时要运行的函数,在 noteUnusedDriverStatement 添加
    dbmuClosed bool     // 和 closed 状态一致,但是由锁保护,用于 removeClosedStmtLocked
}
 
 

Continue to look at the code, look all the way back through the query method, we can see this function:

func(db*DB)conn(ctx context.Context,strategy connReuseStrategy)(*driverConn,error)。

3.1  Get connection

// conn returns a newly-opened or cached *driverConn.
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
    // 先判断db是否已经关闭。
  db.mu.Lock()
  if db.closed {
    db.mu.Unlock()
    return nil, errDBClosed
  }
    // 注意检测context是否已经被超时等原因被取消。
    select {
    default:
    case <-ctx.Done():
        db.mu.Unlock()
        return nil, ctx.Err()
    }
    lifetime := db.maxLifetime
    // 这边如果在freeConn这个切片有空闲连接的话,就left pop一个出列。注意的是,这边因为是切片操作,所以需要前面需要加锁且获取后进行解锁操作。同时判断返回的连接是否已经过期。
    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
        }
        // Lock around reading lastErr to ensure the session resetter finished.
        conn.Lock()
        err := conn.lastErr
        conn.Unlock()
        if err == driver.ErrBadConn {
            conn.Close()
            return nil, driver.ErrBadConn
        }
        return conn, nil
    }
    // 这边就是等候获取连接的重点了。当空闲的连接为空的时候,这边将会新建一个request(的等待连接 的请求)并且开始等待
    if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
        // 下面的动作相当于往connRequests这个map插入自己的号码牌。
        // 插入号码牌之后这边就不需要阻塞等待继续往下走逻辑。
        req := make(chan connRequest, 1)
        reqKey := db.nextRequestKeyLocked()
        db.connRequests[reqKey] = req
        db.waitCount++
        db.mu.Unlock()
        waitStart := time.Now()
        // Timeout the connection request with the context.
        select {
        case <-ctx.Done():
            // context取消操作的时候,记得从connRequests这个map取走自己的号码牌。
            db.mu.Lock()
            delete(db.connRequests, reqKey)
            db.mu.Unlock()
            atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))
            select {
            default:
            case ret, ok := <-req:
                // 这边值得注意了,因为现在已经被context取消了。但是刚刚放了自己的号码牌进去排队里面。意思是说不定已经发了连接了,所以得注意归还!
                if ok && ret.conn != nil {
                    db.putConn(ret.conn, ret.err, false)
                }
            }
            return nil, ctx.Err()
        case ret, ok := <-req:
            // 下面是已经获得连接后的操作了。检测一下获得连接的状况。因为有可能已经过期了等等。
            atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))
            if !ok {
                return nil, errDBClosed
            }
            if ret.err == nil && ret.conn.expired(lifetime) {
                ret.conn.Close()
                return nil, driver.ErrBadConn
            }
            if ret.conn == nil {
                return nil, ret.err
            }
            ret.conn.Lock()
            err := ret.conn.lastErr
            ret.conn.Unlock()
            if err == driver.ErrBadConn {
                ret.conn.Close()
                return nil, driver.ErrBadConn
            }
            return ret.conn, ret.err
        }
    }
    // 下面就是如果上面说的限制情况不存在,可以创建先连接时候,要做的创建连接操作了。
    db.numOpen++ // optimistically
    db.mu.Unlock()
    ci, err := db.connector.Connect(ctx)
    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,
        inUse:     true,
    }
    db.addDepLocked(dc, dc)
    db.mu.Unlock()
    return dc, nil
}
 
 

3.2  release connection

func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
  if db.closed {
    return false
  }
  if db.maxOpen > 0 && db.numOpen > db.maxOpen {
    return false
  }
  // 这边是重点了,基本来说就是从connRequest这个map里面随机抽一个
  // 在排队等着的请求。取出来后发给他。就不用归还池子了。
  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 {
   // 既然没人排队,就看看到了最大连接数目没有。没到就归还给freeConn
    if db.maxIdleConnsLocked() > len(db.freeConn) {
      db.freeConn = append(db.freeConn, dc)
      db.startCleanerLocked()
      return true
    }
    db.maxIdleClosed++
  }
  return false
}

Therefore, the following points should be done during the development process:

Resource reuse: Database connections are reused, avoiding a large amount of performance overhead caused by frequent creation and release of connections.

Faster system response speed: During the initialization process of the database connection pool, several database connections are often created and placed in the pool for standby. For business request processing, the existing available connections are directly used to avoid the time overhead of the database connection initialization and release process, thereby reducing the overall response time of the system.

New means of resource allocation: For a system where multiple applications share the same database, the database connection pool technology can be realized through the configuration of the database connection at the application layer. Set a limit on the maximum number of database connections available for an application to prevent an application from monopolizing all database resources.

Unified connection management to avoid database connection leaks: In a relatively complete database connection pool implementation, the occupied connection can be forcibly recovered according to the pre-set connection occupation timeout setting. This avoids resource leaks that may occur during regular database connection operations.

Guess you like

Origin blog.csdn.net/ygq13572549874/article/details/131819924