ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Go 的 database/sql 连接池实战:SetMaxOpenConns 怎么配、连接泄漏怎么查

Go 的 database/sql 连接池实战:SetMaxOpenConns 怎么配、连接泄漏怎么查 Go 的 database/sql 连接池实战:SetMaxOpenConns 怎么配、连接泄漏怎么查线上服务跑着跑着突然大面积报Error 1040: Too many connections,或者数据库 CPU 不高但接口全在等,十有八九是database/sql的连接池没配好。很多人以为sql.Open拿到的是一条连接,其实它返回的是一个连接池,池子怎么开、怎么回收、怎么防泄漏,全靠你自己设。这篇把连接池的几个关键参数和最常见的泄漏场景一次讲透。先纠正一个误解:sql.Open 不建立连接db,err:sql.Open(mysql,dsn)iferr!nil{log.Fatal(err)}sql.Open只做参数校验,不会真正连数据库。第一次执行查询时才会惰性建立连接。所以想在启动时就确认数据库通不通,得手动 Ping:db,err:sql.Open(mysql,dsn)iferr!nil{log.Fatal(err)}// 启动即验证,连不上直接 fail-fast,别等第一个请求进来才发现iferr:db.PingContext(context.Background());err!nil{log.Fatalf(db 不可达: %v,err)}还有一个高频错误:在每次请求里sql.Open。*sql.DB是并发安全的,整个进程共用一个就够了。每请求 Open 一次会不断新建池子,连接数瞬间打爆。// 错误:每个 handler 里都 Open,连接池根本没复用funchandler(w http.ResponseWriter,r*http.Request){db,_:sql.Open(mysql,dsn)// 每次新池子!deferdb.Close()// ...}正确做法是进程启动时 Open 一次,存成全局或依赖注入进去。三个核心参数:池子多大、闲多久、活多久db.SetMaxOpenConns(50)// 池子最多同时开 50 条连接db.SetMaxIdleConns(10)// 空闲时最多保留 10 条待命db.SetConnMaxLifetime(30*time.Minute)// 一条连接最多活 30 分钟就丢弃重建db.SetConnMaxIdleTime(5*time.Minute)// 空闲超过 5 分钟的连接主动关掉逐个说清楚它们的作用和踩坑点:SetMaxOpenConns —— 最大打开连接数。默认是 0(无限制),这是最危险的默认值。无限制意味着高并发下 Go 会疯狂新建连接,直到把数据库的max_connections顶穿。设一个上限后,超出的查询会排队等待空闲连接,而不是压垮数据库。数值怎么定?看数据库端max_connections减去其他服务占用,再除以你的实例数,留点余量。SetMaxIdleConns —— 最大空闲连接数。请求高峰过后,池子里会留几条连接待命,避免下次请求又要重新握手。这个值如果比MaxOpenConns小很多,会出现一个隐蔽的性能问题:高峰期开了 50 条,峰值一过只留 2 条空闲,其余 48 条被关掉;下一波流量来了又得重新建 48 条连接。建议 MaxIdleConns 设成和 MaxOpenConns 相近(或至少不要差太多),让连接能被稳定复用。SetConnMaxLifetime —— 连接最大存活时间。这个参数专门治一类诡异的报错:invalid connection或driver: bad connection。原因是数据库端(或中间的 LVS/云数据库代理)会主动断开长时间的连接,但 Go 的池子不知道,下次拿这条已经死掉的连接去查就报错。设一个比数据库wait_timeout略小的 Lifetime,让 Go 主动淘汰老连接,永远不会用到被服务端悄悄掐断的连接。// 假设 MySQL wait_timeout3600s,那 Lifetime 设小于它,比如 30 分钟db.SetConnMaxLifetime(30*time.Minute)连接泄漏:Rows 忘了 Close,池子被慢慢抽干这是database/sql最经典、最难查的坑。看这段代码:// 有泄漏!funclistUsers(db*sql.DB)([]string,error){rows,err:db.Query(SELECT name FROM users)iferr!nil{returnnil,err}varnames[]stringforrows.Next(){varnamestringiferr:rows.Scan(name);err!nil{returnnil,err// 提前 return,rows 没关!}namesappend(names,name)}returnnames,nil// 正常路径也没关 rows}db.Query会从池子里借走一条连接,这条连接要等rows.Close()才归还。上面代码里 Scan 出错提前 return、以及正常返回时都没有关 rows,每次调用都漏掉一条连接。请求量一大,MaxOpenConns很快被占满,后续查询全部卡在等连接,表现就是「接口超时但数据库很闲」。正确写法:defer rows.Close()紧跟在拿到 rows 之后,并且循环结束后检查rows.Err():funclistUsers(db*sql.DB)([]string,error){rows,err:db.Query(SELECT name FROM users)iferr!nil{returnnil,err}deferrows.Close()// 拿到 rows 立刻 defer,任何路径都归还连接varnames[]stringforrows.Next(){varnamestringiferr:rows.Scan(name);err!nil{returnnil,err// 现在提前 return 也安全}namesappend(names,name)}// rows.Next() 返回 false 可能是遍历完,也可能是中途出错,必须查iferr:rows.Err();err!nil{returnnil,err}returnnames,nil}补一个细节:QueryRow单行查询不用手动 Close,Scan完会自动释放连接;但如果你QueryRow之后没有调用 Scan,连接同样会泄漏。所以QueryRow(...).Scan(...)要连着写。用池子状态监控揪出泄漏光靠肉眼 review 不可靠,db.Stats()能实时告诉你池子的健康度,把它接到监控里:funcmonitorPool(db*sql.DB){ticker:time.NewTicker(10*time.Second)deferticker.Stop()forrangeticker.C{s:db.Stats()log.Printf(open%d inUse%d idle%d waitCount%d waitDuration%s,s.OpenConnections,// 当前打开的连接总数s.InUse,// 正在被使用(没归还)的连接数s.Idle,// 空闲待命的连接数s.WaitCount,// 累计有多少次查询在等空闲连接s.WaitDuration,// 累计等待时长)}}判断依据很直接:如果InUse长期贴着MaxOpenConns下不来,而流量并不高,基本就是有地方没关 rows/连接。如果WaitCount和WaitDuration持续涨,说明池子开小了或者被泄漏抽干,查询都在排队。事务里更要小心:Tx 不 Rollback/Commit 会一直占着连接事务会独占一条连接直到 Commit 或 Rollback。中途 return 忘了收尾,这条连接就永久泄漏了:functransfer(db*sql.DB,from,toint,amountint)(errerror){tx,err:db.Begin()iferr!nil{returnerr}// defer 里根据是否出错决定提交还是回滚,保证连接一定归还deferfunc(){iferr!nil{tx.Rollback()// 出错回滚}else{errtx.Commit()// 没错则提交,把 Commit 的错也带出去}}()if_,errtx.Exec(UPDATE account SET balancebalance-? WHERE id?,amount,from);err!nil{returnerr// 直接 return,defer 会 Rollback}if_,errtx.Exec(UPDATE account SET balancebalance? WHERE id?,amount,to);err!nil{returnerr}returnnil}这里用命名返回值err配合 defer 是关键:无论从哪个分支 return,defer 都能根据err是否为 nil 决定 Commit 还是 Rollback,连接一定归还。小结sql.Open返回的是连接池且惰性连接,进程内只 Open 一次并共享*sql.DB,启动时用PingContext验证连通。四个参数:MaxOpenConns设上限防打爆数据库;MaxIdleConns设大点让连接稳定复用;ConnMaxLifetime设成小于数据库wait_timeout,躲开被服务端掐断的死连接;ConnMaxIdleTime回收长期空闲连接。泄漏根源永远是「借了不还」:Query后立刻defer rows.Close()并检查rows.Err();QueryRow一定接Scan;事务用命名返回值 defer 保证 Commit/Rollback。用db.Stats()把InUse/WaitCount接进监控,InUse长期打满就是有泄漏。一句话记忆点:database/sql 管的是池子不是连接,所有诡异的超时和 too many connections,先去查有没有 rows 或 tx 借了没还。
返回列表