GORM 之 for (rows.Next) 提前退出別忘了 Close

singee發表於2020-01-07

近期一同事負責的線上模組,總是時不時的返回一下 504,檢查發現,這個服務的記憶體使用異常的大,pprof 分析後,發現有上萬個 goroutine,排查分析之後,是沒有規範使用 gorm 包導致的,那麼具體是什麼原因呢,會不會也像 《Go Http 包解析:為什麼需要 response.Body.Close ()》 文中一樣,因為沒有釋放連線導致的呢?

demo

首先我們先來看一個示例,然後,猜測一下列印的結果

package main

import (
    "log"
    "net/http"
    _ "net/http/pprof"
    "time"

    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

var (
    db *gorm.DB
)

type User struct {
    ID    int64  `gorm:"column:id;primary_key" json:"id"`
    Name  string `gorm:"column:name" json:"name"`
}

func (user *User) TableName() string {
    return "ranger_user"
}

func main() {
    go func() {
        log.Println(http.ListenAndServe(":6060", nil))
    }()
    for true {
        GetUserList()
        time.Sleep(time.Second)
    }
}

func GetUserList() ([]*User, error) {
    users := make([]*User, 0)
    db := open()
    rows, err := db.Model(&User{}).Where("id > ?", 1).Rows()
    if err != nil {
        panic(err)
    }
  // 為了試驗而寫的特殊邏輯
    for rows.Next() {
        user := &User{}
        err = db.ScanRows(rows, user)
        return nil, err
    }
    return users, nil
}

func open() *gorm.DB {
  if db != nil {
        return db
    }
    var err error
    db, err = gorm.Open("mysql",
     "user:pass@(ip:port)/db?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        panic(err)
    }
    return db
}

分析

我們先看一下上面的 demo,貌似沒有什麼問題,我們就執行一段時間看看

GORM 之 for (rows.Next) 提前退出別忘了 Close

GORM 之 for (rows.Next) 提前退出別忘了 Close

有點尷尬,我就一簡單的查詢返回,怎麼會有那麼多 goroutine?

繼續看一下都是哪些函式產生了 goroutine

GORM 之 for (rows.Next) 提前退出別忘了 Close

startWatcher.func1 是個什麼鬼

func (mc *mysqlConn) startWatcher() {
    watcher := make(chan mysqlContext, 1)
    mc.watcher = watcher
    finished := make(chan struct{})
    mc.finished = finished
    go func() {
        for {
            var ctx mysqlContext
            select {
            case ctx = <-watcher:
            case <-mc.closech:
                return
            }

            select {
            case <-ctx.Done():
                mc.cancel(ctx.Err())
            case <-finished:
            case <-mc.closech:
                return
            }
        }
    }()
}

猜測驗證

startWatcher 這個函式的呼叫者,只有 MySQLDriver.Open 會呼叫,也就是建立新的連線的時候,才會去建立一個監控者的 goroutine

根據 《Go Http 包解析:為什麼需要 response.Body.Close ()》 中的分析結果,可以大膽猜測,有可能是 mysql 每次去查詢的時候,獲取一個連線,沒有空閒的連線,則建立一個新的,查詢完成後釋放連線到連線池,以便下一個請求使用,而由於沒有呼叫 rows.Close (), 導致拿了連線之後,沒有再放回連線池複用,導致每個請求過來都建立一個新的請求,從而導致產生了大量的 goroutine 去執行 startWatcher.func1 監控新建立的連線 。所以我們類似於 response.Close 一樣,進行一下 rows.Close () 是不是就 ok 了,接下來驗證一下

對上面的測試程式碼增加一行 rows.Close ()

defer rows.Close()
    for rows.Next() {
        user := &User{}
        err = db.ScanRows(rows, user)
        return nil, err
    }

繼續觀察 goroutine 的變化

GORM 之 for (rows.Next) 提前退出別忘了 Close

goroutine 不再上升,貌似問題就解決了

疑問

  1. 我們一般寫程式碼的時候,都不會呼叫 rows.Close() 的,很多情況下並沒有出現 goroutine 的暴增,這是為什麼

照例,還是先把可能用到的結構體提前放出來,混個眼熟

rows

// Rows is the result of a query. Its cursor starts before the first row
// of the result set. Use Next to advance from row to row.
type Rows struct {
    dc          *driverConn // owned; must call releaseConn when closed to release
    releaseConn func(error) // driverConn.releaseConn, 在query的時候,會傳遞過來
    rowsi       driver.Rows
    cancel      func()      // called when Rows is closed, may be nil.
    closeStmt   *driverStmt // if non-nil, statement to Close on close

    // closemu prevents Rows from closing while there
    // is an active streaming result. It is held for read during non-close operations
    // and exclusively during close.
    //
    // closemu guards lasterr and closed.
    closemu sync.RWMutex
    closed  bool
    lasterr error // non-nil only if closed is true

    // lastcols is only used in Scan, Next, and NextResultSet which are expected
    // not to be called concurrently.
    lastcols []driver.Value
}s

建立連線、scope 結構體、Model、Where 方法的邏輯就不再贅述了,上一篇文章《GORM 之 ErrRecordNotFound 採坑記錄》已經粗略講過了,直接進入 Rows 函式的解析

Rows

// Rows return `*sql.Rows` with given conditions
func (s *DB) Rows() (*sql.Rows, error) {
    return s.NewScope(s.Value).rows()
}

func (scope *Scope) rows() (*sql.Rows, error) {
    defer scope.trace(scope.db.nowFunc())

    result := &RowsQueryResult{}
  // 設定 row_query_result,供 callback 函式使用
    scope.InstanceSet("row_query_result", result)
    scope.callCallbacks(scope.db.parent.callbacks.rowQueries)

    return result.Rows, result.Error
}

感覺這裡很快就進入了 callback 的回撥

根據上一篇文章的經驗,rowQueries 所註冊的回撥函式,可以在 callback_row_query.go 中的 init () 函式中找到

func init() {
    DefaultCallback.RowQuery().Register("gorm:row_query", rowQueryCallback)
}

// queryCallback used to query data from database
func rowQueryCallback(scope *Scope) {
  // 對應 上面函式裡面的 scope.InstanceSet("row_query_result", result)
    if result, ok := scope.InstanceGet("row_query_result"); ok {
    // 組裝出來對應的sql語句,eg: SELECT * FROM `ranger_user`  WHERE (id > ?)
        scope.prepareQuerySQL()
        if str, ok := scope.Get("gorm:query_option"); ok {
            scope.SQL += addExtraSpaceIfExist(fmt.Sprint(str))
        }

        if rowResult, ok := result.(*RowQueryResult); ok {
            rowResult.Row = scope.SQLDB().QueryRow(scope.SQL, scope.SQLVars...)
        } else if rowsResult, ok := result.(*RowsQueryResult); ok {
      // result 對應的結構體是 RowsQueryResult,所以執行到這裡,繼續跟進這個函式
            rowsResult.Rows, rowsResult.Error = scope.SQLDB().Query(scope.SQL, scope.SQLVars...)
        }
    }
}

上面可以看到,rowQueryCallback 僅僅是組裝了一下 sql,然後又去呼叫 go 提供的 sql 包,來進行查詢

sql.Query

// Query executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
// query是sql語句,args則是sql中? 所代表的值
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
    return db.QueryContext(context.Background(), query, args...)
}

// QueryContext executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) {
    var rows *Rows
    var err error
  // maxBadConnRetries = 2
    for i := 0; i < maxBadConnRetries; i++ {
    // cachedOrNewConn 則是告訴query 去使用快取的連線或者建立一個新的連線
        rows, err = db.query(ctx, query, args, cachedOrNewConn)
        if err != driver.ErrBadConn {
            break
        }
    }
  // 如果嘗試了maxBadConnRetries次後,連線還是有問題的,則建立一個新的連線去執行sql
    if err == driver.ErrBadConn {
        return db.query(ctx, query, args, alwaysNewConn)
    }
    return rows, err
}

func (db *DB) query(ctx context.Context, query string, args []interface{}, 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)
}

上面的邏輯理解不難,這裡有兩個變數,解釋一下

cachedOrNewConn: connReuseStrategy 型別,本質是 uint8 型別,值是 1,這個標誌會傳遞給下面的 db.conn 函式,告訴這個函式,返回連線的策略

 1. 如果連線池中有空閒連線,返回一個空閒的
 2. 如果連線池中沒有空的連線,且沒有超過最大建立的連線數,則建立一個新的返回
 3. 如果連線池中沒有空的連線,且超過最大建立的連線數,則等待連線釋放後,返回這個空閒連線

alwaysNewConn:

  1. 每次都返回一個新的連線

獲取連線

// conn returns a newly-opened or cached *driverConn.
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
    db.mu.Lock()
    if db.closed {
        db.mu.Unlock()
        return nil, errDBClosed
    }
    // Check if the context is expired.
  // 校驗一下ctx是否過期了
    select {
    default:
    case <-ctx.Done():
        db.mu.Unlock()
        return nil, ctx.Err()
    }
    lifetime := db.maxLifetime

    // Prefer a free connection, if possible.
    numFree := len(db.freeConn)
    if strategy == cachedOrNewConn && numFree > 0 {
    // 如果選擇連線的策略是 cachedOrNewConn,並且有空閒的連線,則嘗試獲取連線池中的第一個連線
        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.
    // 判斷連線的lastErr,確保連線是被重置過的
        conn.Lock()
        err := conn.lastErr
        conn.Unlock()
        if err == driver.ErrBadConn {
            conn.Close()
            return nil, driver.ErrBadConn
        }
        return conn, nil
    }

    // Out of free connections or we were asked not to use one. If we're not
    // allowed to open any more connections, make a request and wait.
  // 走到這裡說明沒有獲取到空閒連線,判斷建立的連線數量是否超過最大允許的連線數量
    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.
    // 建立一個chan,用於接收釋放的空閒連線
        req := make(chan connRequest, 1)
    // 建立一個key
        reqKey := db.nextRequestKeyLocked()
    // 將key 和chan繫結,便於根據key 定位所對應的chan
        db.connRequests[reqKey] = req
        db.waitCount++
        db.mu.Unlock()

        waitStart := time.Now()

        // Timeout the connection request with the context.
        select {
        case <-ctx.Done():
            // Remove the connection request and ensure no value has been sent
            // on it after removing.
      // 如果ctx失效了,則這個空閒連線也不需要了,刪除剛剛建立的key,防止這個連線被移除後再次為這個key獲取連線
            db.mu.Lock()
            delete(db.connRequests, reqKey)
            db.mu.Unlock()

            atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))

            select {
            default:
            case ret, ok := <-req:
        // 如果獲取到了空閒連線,則放回連線池裡面
                if ok && ret.conn != nil {
                    db.putConn(ret.conn, ret.err, false)
                }
            }
            return nil, ctx.Err()
        case ret, ok := <-req:
      // 此時拿到了空閒連線,且ctx沒有過期,則判斷連線是否有效
            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
            }
            // Lock around reading lastErr to ensure the session resetter finished.
      // 判斷連線的lastErr,確保連線是被重置過的
            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
        }
    }
    // 上面兩個都不滿足,則建立一個新的連線,也就是 獲取連線的策略是 alwaysNewConn 的時候
    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
}

在上面的邏輯中,可以看到,獲取連線的策略跟我們上面解釋 cachedOrNewConn 和 alwaysNewConn 時是一樣的,但是,這裡面有兩個問題

  1. 建立的連線數量超過最大允許的連線數量,則等待一個空閒的連線,這時候為 db.connRequests 這個 map 新增加了一個 key,這個 key 對應一個 chan,然後直接等待這個 chan 吐出來連線,既然是等待釋放空閒連線,那麼這個 chan 裡面插入的 連線,應該是在 freeconn 函式裡面,freeconn 的邏輯又是怎麼樣的呢
  2. 建立新連線失敗後,會呼叫 db.maybeOpenNewConnections, 這個函式又不返回連線,那麼它做了什麼

釋放連線

釋放連線主要依靠 putconn 來完成的,在 conn 函式的下面程式碼中

            case ret, ok := <-req:
        // 如果獲取到了空閒連線,則放回連線池裡面
                if ok && ret.conn != nil {
                    db.putConn(ret.conn, ret.err, false)
                }
            }

也呼叫了,把獲取到但不再需要的連線放回池子裡,下面看一下釋放連線的過程

putConn

// putConn adds a connection to the db's free pool.
// err is optionally the last error that occurred on this connection.
func (db *DB) putConn(dc *driverConn, err error, resetSession bool) {
    db.mu.Lock()
  // 釋放一個正在用的連線,panic
    if !dc.inUse {
        panic("sql: connection returned that was never out")
    }
    dc.inUse = false

  // 省略部分無關程式碼...

    if err == driver.ErrBadConn {
        // Don't reuse bad connections.
        // Since the conn is considered bad and is being discarded, treat it
        // as closed. Don't decrement the open count here, finalClose will
        // take care of that.
    // maybeOpenNewConnections 這個函式又見到了,它到底幹了什麼
        db.maybeOpenNewConnections()
        db.mu.Unlock()
        dc.Close()
        return
    }

  ...

  if db.closed {
        // Connections do not need to be reset if they will be closed.
        // Prevents writing to resetterCh after the DB has closed.
        resetSession = false
    }
    if resetSession {
        if _, resetSession = dc.ci.(driver.SessionResetter); resetSession {
            // Lock the driverConn here so it isn't released until
            // the connection is reset.
            // The lock must be taken before the connection is put into
            // the pool to prevent it from being taken out before it is reset.
            dc.Lock()
        }
    }
  // 把連線放回連線池中,也是這個函式的核心邏輯
    added := db.putConnDBLocked(dc, nil)
    db.mu.Unlock()
  // 如果釋放連線失敗,則關閉連線
    if !added {
        if resetSession {
            dc.Unlock()
        }
        dc.Close()
        return
    }
    if !resetSession {
        return
    }
  // 嘗試將連線放回resetterCh chan裡面,如果失敗,則標識連線異常
    select {
    default:
        // If the resetterCh is blocking then mark the connection
        // as bad and continue on.
        dc.lastErr = driver.ErrBadConn
        dc.Unlock()
    case db.resetterCh <- dc:
    }
}

putConnDBLocked

func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
    if db.closed {
        return false
    }
  // 已經超出最大的連線數量了,不需要再放回了
    if db.maxOpen > 0 && db.numOpen > db.maxOpen {
        return false
    }
  // 如果有其他等待獲取空閒連線的協程,則
    if c := len(db.connRequests); c > 0 {
        var req chan connRequest
        var reqKey uint64
    // connRequests 獲取一個 chan,並把這個連線返回到這個 chan裡面
        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 這個slice裡面
        if db.maxIdleConnsLocked() > len(db.freeConn) {
            db.freeConn = append(db.freeConn, dc)
            db.startCleanerLocked()
            return true
        }
        db.maxIdleClosed++
    }
    return false
}

梳理完釋放連線的邏輯,我們可以看出連線複用的大致流程

  1. 一個新的請求過來,需要獲取一個新的連線
  2. 首先判斷是否有空閒連線,如果沒有且沒有超過允許建立的最大連線數,則建立一個
  3. 多個請求之後,連線數量已經超過了設定的最大連線數,則等待釋放空閒連線
  4. 此時,第一個請求完成了,準備釋放連線,去看一下有沒有等待空閒連線的請求,如果有的話,則把這個連線通過 chan 直接傳過去,否則,把這個連線放到空閒的連線池裡面
  5. 此時,後面等待空閒連線的請求,拿到了第一個請求傳遞過來的連線,繼續處理請求
  6. 以上,迴圈往復

maybeOpenNewConnections

這個函式,在上面的分析中已經出現了兩次了,先分析一下 這個函式到底做了什麼

func (db *DB) maybeOpenNewConnections() {
  // 計算需要建立的連線數,總共建立的有效連線數不能超過設定的最大連線數
    numRequests := len(db.connRequests)
    if db.maxOpen > 0 {
        numCanOpen := db.maxOpen - db.numOpen
        if numRequests > numCanOpen {
            numRequests = numCanOpen
        }
    }
    for numRequests > 0 {
        db.numOpen++ // optimistically
        numRequests--
        if db.closed {
            return
        }
    // 往 openerCh 這個chan裡面插入一條資料
        db.openerCh <- struct{}{}
    }
}

在前面的分析中,如果在獲取連線時,發現產生的連線數 >= 最大允許的連線數,則在 db.connRequests 這個 map 中建立一個唯一的 key value,用於接收釋放的空閒連線,但是如果在釋放連線的過程中,發現這個連線失效了,這個連線就無法複用,這時候就會走到這個函式,嘗試建立一個新的連線,給其他等待的請求使用

這裡就會發現一個問題: 為什麼 db.openerCh <- struct{}{} 這樣一條簡單的命令就能建立一個連線,接下來就需要分析 db.openerCh 的接收方了

connectionOpener

這個函式在 db 結構體建立的時候,就會開始執行了,一個常駐的 goroutine

// Runs in a separate goroutine, opens new connections when requested.
func (db *DB) connectionOpener(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        case <-db.openerCh:
      // 這邊接收到資料後,就開始建立一個新的連線
            db.openNewConnection(ctx)
        }
    }
}

openNewConnection

// Open one new connection
func (db *DB) openNewConnection(ctx context.Context) {
    // maybeOpenNewConnctions has already executed db.numOpen++ before it sent
    // on db.openerCh. This function must execute db.numOpen-- if the
    // connection fails or is closed before returning.
  // 呼叫 sql driver 庫來建立一個連線
    ci, err := db.connector.Connect(ctx)
    db.mu.Lock()
    defer db.mu.Unlock()
  // 如果db已經關閉,則關閉連線並返回
    if db.closed {
        if err == nil {
            ci.Close()
        }
        db.numOpen--
        return
    }
    if err != nil {
    // 建立連線失敗了,重新呼叫 maybeOpenNewConnections 再建立一次
        db.numOpen--
        db.putConnDBLocked(nil, err)
        db.maybeOpenNewConnections()
        return
    }
    dc := &driverConn{
        db:        db,
        createdAt: nowFunc(),
        ci:        ci,
    }
  // 走到 putConnDBLocked,把連線交給等待的請求方或者連線池中
    if db.putConnDBLocked(dc, err) {
        db.addDepLocked(dc, dc)
    } else {
        db.numOpen--
        ci.Close()
    }
}

Connect

這裡是連線資料庫的主要邏輯

func (t dsnConnector) Connect(_ context.Context) (driver.Conn, error) {
    return t.driver.Open(t.dsn)
}

func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
    var err error

    // New mysqlConn
    mc := &mysqlConn{
        maxAllowedPacket: maxPacketSize,
        maxWriteSize:     maxPacketSize - 1,
        closech:          make(chan struct{}),
    }
  // 解析dsn
    mc.cfg, err = ParseDSN(dsn)
    if err != nil {
        return nil, err
    }
    mc.parseTime = mc.cfg.ParseTime

    // Connect to Server
  // 找到對應網路連線型別(tcp...) 的連線函式,並建立連線
    dialsLock.RLock()
    dial, ok := dials[mc.cfg.Net]
    dialsLock.RUnlock()
    if ok {
        mc.netConn, err = dial(mc.cfg.Addr)
    } else {
        nd := net.Dialer{Timeout: mc.cfg.Timeout}
        mc.netConn, err = nd.Dial(mc.cfg.Net, mc.cfg.Addr)
    }
    if err != nil {
        return nil, err
    }

    // Enable TCP Keepalives on TCP connections
  // 開啟Keepalives
    if tc, ok := mc.netConn.(*net.TCPConn); ok {
        if err := tc.SetKeepAlive(true); err != nil {
            // Don't send COM_QUIT before handshake.
            mc.netConn.Close()
            mc.netConn = nil
            return nil, err
        }
    }

    // Call startWatcher for context support (From Go 1.8)
  // 這裡呼叫startWatcher,開始對連線進行監控,及時釋放連線
    if s, ok := interface{}(mc).(watcher); ok {
        s.startWatcher()
    }

    // 下面一些設定與分析無關,忽略...

    return mc, nil
}

startWatcher

這個函式主要是對連線進行監控

func (mc *mysqlConn) startWatcher() {
    watcher := make(chan mysqlContext, 1)
    mc.watcher = watcher
    finished := make(chan struct{})
    mc.finished = finished
    go func() {
        for {
            var ctx mysqlContext
            select {
            case ctx = <-watcher:
            case <-mc.closech:
                return
            }

            select {
      // ctx 過期的時候,關閉連線,這時候會關閉mc.closech
            case <-ctx.Done():
                mc.cancel(ctx.Err())
            case <-finished:
      // 關閉連線
            case <-mc.closech:
                return
            }
        }
    }()
}

建立連線的邏輯

  1. 首先嚐試建立一個連線,如果失敗,則再次呼叫 maybeOpenNewConnections 函式,再度嘗試建立一個新的連線,直到建立成功或者沒有請求方需要等待連線位置
  2. 新連線建立時,會呼叫 startWatcher 函式,一個常駐的 goroutine,來對連線進行監控,及時的關閉
  3. 連線建立成功後,通過 putConnDBLocked,把連線交給等待連線的請求方或者放到連線池中

至此,基本上連線建立及複用的流程大概清晰了,至此,對於我們最開始遇到的問題也有了一個明確的解釋:

  • 呼叫 Rows () 函式進行查詢的時候,需要獲取一個連線
  • 此時沒有新的或空閒的連線,所以,需要建立一個新的連線
  • 建立連線是,建立一個 startWatcher 的 goroutine 來進行監控
  • 由於 查詢完成後,沒有呼叫 rows.Close () 及時釋放連線,導致此連線一直沒有放回連線池或被複用,所以每次請求,都會建立一個新的連線
  • 多次請求下來,就會建立很多的 startWatcher 的 goroutine,最終產生了遇到的現象

Rows.Close

func (rs *Rows) Close() error {
    return rs.close(nil)
}

func (rs *Rows) close(err error) error {
    rs.closemu.Lock()
    defer rs.closemu.Unlock()
  // ...
  rs.closed = true

  // 相關欄位的一些設定, 忽略 ....
    rs.releaseConn(err)
    return err
}

// 通過putConn 把連線釋放
func (dc *driverConn) releaseConn(err error) {
    dc.db.putConn(dc, err, true)
}

rs.releaseConn 所對應的函式,可以在 queryDC 這個方法裡面找到,這裡就直接列出來了

可以看到,rows.Close () 最後就是通過 putConn 把當前的連線釋放以便複用

Rows.Next

Next 為 scan 方法準備下一條記錄,以便 scan 方法讀取,如果沒有下一行的話,或者準備下一條記錄的時候出錯了,就會返回 false

func (rs *Rows) Next() bool {
    var doClose, ok bool
    withLock(rs.closemu.RLocker(), func() {
    // 準備下一條記錄
        doClose, ok = rs.nextLocked()
    })
    if doClose {
    // 如果 doClose 為true,說明沒有記錄了,或者準備下一條記錄的時候,出錯了,此時關閉連線
        rs.Close()
    }
    return ok
}

func (rs *Rows) nextLocked() (doClose, ok bool) {
  // 如果 已經關閉了,就不要讀取下一條了
    if rs.closed {
        return false, false
    }

    // Lock the driver connection before calling the driver interface
    // rowsi to prevent a Tx from rolling back the connection at the same time.
    rs.dc.Lock()
    defer rs.dc.Unlock()

    if rs.lastcols == nil {
        rs.lastcols = make([]driver.Value, len(rs.rowsi.Columns()))
    }
    // 獲取下一條記錄,並放到lastcols裡面
    rs.lasterr = rs.rowsi.Next(rs.lastcols)
    if rs.lasterr != nil {
        // Close the connection if there is a driver error.
    // 讀取出錯,返回true,以便後面關閉連線
        if rs.lasterr != io.EOF {
            return true, false
        }
        nextResultSet, ok := rs.rowsi.(driver.RowsNextResultSet)
        if !ok {
      // 沒有獲取到記錄了,返回true,以便後面關閉連線
            return true, false
        }
        // The driver is at the end of the current result set.
        // Test to see if there is another result set after the current one.
        // Only close Rows if there is no further result sets to read.
        if !nextResultSet.HasNextResultSet() {
            doClose = true
        }
        return doClose, false
    }
    return false, true
}

Next () 的邏輯:

  1. 在呼叫 Next () 的時候,準備下一條記錄,以便 scan 讀取
  2. 如果在準備資料的時候出錯或者沒有下一條記錄的時候,返回 false
  3. 如果 Next () 在準備資料的時候,拿到了 false,則呼叫 rows.Close () 把連線放回池子或者交給其他請求等待著,以便複用連線

所以,也就是為什麼一下的 demo 並不會出現問題一樣

    for rows.Next() {
        user := &User{}
        err = db.ScanRows(rows, user)
        if err != nil {
            continue
        }
    }

走到這裡,開頭提出的問題應該已經有了明確的答案了: rows.Next () 在獲取到最後一條記錄之後,會呼叫 rows.Close () 將連線放回連線池或交給其他等待的請求方,所以不需要手動呼叫 rows.Close (),

而出問題的 demo 中,由於 rows.Next () 沒有執行到最後一條記錄處,也沒有呼叫 rows.Close (), 所以在獲取到連線後一直沒有被放回進行復用,導致了每來一個請求建立一個新的連線,產生一個新的監控者 startWatcher.func1, 最終導致了記憶體爆炸?

本文來源於 https://segmentfault.com/a/119000002149346... ,僅對於格式有所修改。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章