从“gorm建立mysql数据库连接,偶现tcp: i/o timeout问题”简单看gorm.DB源码

昨天被同事问到一个问题:

go服务,gorm建立mysql数据库连接,偶现tcp: i/o timeout问题是怎么回事?

说实话,我平时没怎么用过gorm,也只是简单了解它的用法。

考虑到问题是连接mysql偶现tcp: i/o timeout, 第一反应是怀疑mysql的负载太高,比如连接数太高导致排队。

于是看了下代码对gorm的调用代码:

使用gorm建立连接的场景

这里看起来没什么问题,连接池的参数也都有配上。

// NewDB 连接db
func NewDB(dbKey string) (*gorm.DB, error) {
	db, err := gorm.Open("mysql", buildDsn(c, dbKey))
	if err != nil {
		log.Errorf("Err_Open_Db: dbKey= %s , err= %s", dbKey, err.Error())
		return nil, err
	}
	// 设置一些连接池参数
	db.DB().SetConnMaxLifetime(time.Duration(c.GetInt("connMaxLifetime", 600)) * time.Second)
	db.DB().SetMaxIdleConns(c.GetInt("maxIdleConns", 10))
	db.DB().SetMaxOpenConns(c.GetInt("maxOpenConns", 200))
	return db, nil
}

再上层调用NewDB方法的场景:

这里貌似是有问题的,grpc的Handler是来一个请求,调一次methodHandler的。这个业务方法频繁的创建了gorm.DB实例

func (*ContentServiceImpl) MultyPickContent(ctx context.Context, in *pb.ContentRequest) (*pb.ContentReply, error) {
	// 这里get了Db
	db, err := NewDB(dbKey)
	if err != nil {
		return nil, err
	}
	// 这里是其他的业务逻辑......
	return &pb.ContentReply{Message: xxxxxx}, nil
}

我们看下gorm.DB的源码:

其实是包装了一个sql.DB的实例

// Open initialize a new db connection, need to import driver first, e.g:
func Open(dialect string, args ...interface{}) (db *DB, err error) {
	// 省略其他没关系的代码...
	switch value := args[0].(type) {
	case string:
		var driver = dialect
		if len(args) == 1 {
			source = value
		} else if len(args) >= 2 {
			driver = value
			source = args[1].(string)
		}
        // 这里是核心代码:调用sql.Open得到sql.DB实例
		dbSQL, err = sql.Open(driver, source)
		ownDbSQL = true
	default:
		return nil, fmt.Errorf("invalid database source: %v is not a valid type", value)
	}

	// 省略其他没关系的代码...
	return
}

sql.DB的Open方法

sql.Open() 注释很清楚,并发安全&&自带连接池,建议初始化一次,退出时close

// The returned DB is safe for concurrent use by multiple goroutines
// and maintains its own pool of idle connections. Thus, the Open
// function should be called just once. It is rarely necessary to
// close a DB.
func Open(driverName, dataSourceName string) (*DB, error) {
	driversMu.RLock()
	driveri, ok := drivers[driverName]
	driversMu.RUnlock()
	if !ok {
		return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
	}

	if driverCtx, ok := driveri.(driver.DriverContext); ok {
		connector, err := driverCtx.OpenConnector(dataSourceName)
		if err != nil {
			return nil, err
		}
		return OpenDB(connector), nil
	}

	return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

现在问题就很清楚了:

每个请求过来,service通过创建gorm.DB创建了一个sql.DB实例,之后connection_pool会创建一个长连接到mysql,但这个connection在请求结束后会被连接池短暂cover住,没有立即释放(因为Handler里面也没调用db.close方法关闭连接池)

那么并发量稍微上去,这里就会耗尽mysql连接数,导致新的请求无法连上mysql服务器。通过show processlist就能确认连接数。

至于为什么是偶发timeout,目测是该服务的并发量其实并没有特别高哈哈

你可能感兴趣的:(go,mysql,go,gorm)