昨天被同事问到一个问题:
go服务,gorm建立mysql数据库连接,偶现tcp: i/o timeout问题是怎么回事?
说实话,我平时没怎么用过gorm,也只是简单了解它的用法。
考虑到问题是连接mysql偶现tcp: i/o timeout, 第一反应是怀疑mysql的负载太高,比如连接数太高导致排队。
于是看了下代码对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
}
这里貌似是有问题的,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
}
其实是包装了一个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.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,目测是该服务的并发量其实并没有特别高哈哈