golang SQLパケット接続プール分析

golang MySQLを使用する場合は、出会いのアイデアが解決された後の問題を避けるために、どのようなプロセス全体と要求の詳細を整理する必要がある、ブラックボックスではそれで、毎回データベース/ SQLライブラリを使用します。

読書の前にいくつかの質問

  • どのようにメンテナンスのSQL接続プール?
  • どのようExecは、クエリに参加を取得/クエリを実行するには?
  • 接続プールを解放するには?

いくつかの重要な構造

DB構造体

のは、コア構造のSQLパッケージでDB構造、見てみましょう。DBは、データベース、ゼロまたはそれ以上の基になる接続プール、同時安全を処理しています。

SQLパッケージを自動的に作成し、接続を解除することができ、それが空き接続プールを維持します。データベースは、各接続状態の概念を持っている場合は、それだけで確実にトランザクションで、この状態で観察することができます。DB.Beginを呼び出した後、Txが単一の接続にバインドするために戻ります。コミットまたはトランザクションと接続の問題にロールバック呼び出した後にアイドル状態のDB接続プールに戻ります。SetMaxIdleConnsは、接続プールのサイズを制御するために使用します。

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構造体

driverConnを使用して、すべての中に保持されたミューテックスdriver.Connパッケージ構造は、コネチカットを呼び出し(コネチカットによって返さインターフェースへの呼び出しを含む、例えば、テキサス州、のstmtに呼び出し、結果、行)の

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)
}

バインディング登録ドライブ

私たちは、指定されたデータベースを使用するときに使用する必要があるimport _ "github.com/go-sql-driver/mysql"のinit()関数を実行します。INIT()関数は、主にドライバのSQL変数のタイプをマップする指定されたデータベース・ドライバを登録するために使用されます。

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{})
}

接続

接続のセットアッププロセス
8573331-96acfb3f69fab8ee.png
接続を作成します。

これは、ゴルーチンがブロックされて読み始めますsql.Open時間を呼び出す見ることができますdb.openerChこのopenerCh信号を受信した場合、プロセスは、接続を作成するために開始され、接続を作成するための接続方法を提供するドライブを作成するために呼び出します。あなたが変更に成功し、優先接続を作成した場合db.connRequests、使用するブロッキング要求内をブロックされていない場合、要求はにこの新しい接続入れるdb.freeConn保留中の要求を。

关键方法
// 调用驱动的 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. 現在の接続数がオーバーランするかどうかを決定します。オーバーランした場合、要求はconnRequestsに利用可能な接続を待ってブロックされています。あなたは、次のステップをオーバーランしていない場合。
    3. 新しい接続を作成します。
  • 接続プールの回復/解放を接続する方法は?
    1. パッシブ回復/リリース。エラーがクエリによって返され、他の操作をしたときに実行されるreleaseConn機能回復の接続を。条件が満たされたときに再生されますconnectionCleaner、無効な接続プールの接続をクリーンアップゴルーチン。
    2. アクティブ回復/リリース。設定db.SetConnMaxLifetime時間は、トリガされますconnectionCleanerゴルーチンは、接続プールをクリーンアップします。

あなたは、生産者が、それはまだ非常に複雑で、接続プールの実装機構のSQLデータベースを参照することができconnectionOpener、接続プールへの接続を作成openerChモニターを遮断ゴルーチン。リクエストが来た場合、最初のチェックは接続プールアイドル状態の接続が存在しないアイドル状態の接続が作成されませんがある場合は、要求Queryクラスは、バック接続プールの再利用に実行し続けます。コールは、接続プールのクリーンアップする必要が接続されている場合connectionCleanerゴルーチンを。再度分析、将来的に出会いの問題は、より高速な処理になります。

ます。https://www.jianshu.com/p/45ea39e79de3で再現

おすすめ

転載: blog.csdn.net/weixin_34411563/article/details/91241879