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

tyloafer發表於2020-01-05

近期一同事負責的線上模組,總是時不時的返回一下 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, 最終導致了記憶體爆炸?

相關文章