分析golang sql连接池大量的time wait问题

前言:

slack报警提示同事的线上系统爆出了大量的tcp time_wait。通过netstat可以判断出是postgreSQl引起的。初步分析像是连接池没有得到复用,一直在new pg连接和close连接。
我们的pg驱动是golang内置的database/sql库,该库不仅实现了sql基于使用,而且还自带连接池。database/sql库兼容mysql和postgre。

该文章后续仍在不断的更新修改中, 请移步到原文地 http://xiaorui.cc/?p=5771

排除问题

首先我们要知道谁去主动关闭连接,那么谁就会产生2ms时间的time wait。出问题的服务只有grpc server和postgre数据库两个类型的连接。
既然通过netstat可以看出问题出在postgre, 那么问题要么是我们database/sql,要么出在我们的用法上。
database/sql作为golang的标准库,不应该会有这类低级问题。

那么看我们的配置 maxIdle, maxConn, maxConn可以理解为最大的连接数,maxIdle是最大的空闲数。 我曾经看过一些框架的连接池实现,maxIdle的实现略有不同。有些框架会严格控制maxIdle,有些会放到free里,等待expire后淘汰。
那么database/sql是如何实现的? 研究下 database/sql的go源代码吧。

源代码分析

当我们使用query和begin事务操作sql的时候,会调用db.query()方法,该方法会return一个连接。

调用db.conn()时,申请连接会传递一个参数, 该参数用来说明始终实例化新连接还是从缓存中获取。 sql默认就是一个连接池,开始会先用cachedOrNewConn来获取连接。
如果总是返回ErrBadConn,那么就再通过alwaysNewConn来申请。

申请的连接的过程, 简单的说,配置了maxOpen,而且当前已经存在的连接超过maxOpen,那么就一直阻塞等待,等待别的协程来释放sql连接。不匹配前面的条件,那么就实例化新的连接。

上面是申请连接,下面是释放和归还连接到连接池,当我们执行Query()和手动commit和rollback的时候,都会触发db.releaseConn()方法。

当归还连接到连接池的时候,发现当前的连接数已经大于我们自己配置的maxOpen,那么就直接close连接。如果正常连接池,那么观察下db.connRequests是否有人等待连接。
如果在等待,那么就把连接通过channel传递过去。如果没有人正在等待获取sql连接,那么就把连接放到db.freeConn里。

总结:

在分析go databse/sql的源代码的过程中,我们可以看到了tcp time wait的原因。我们的maxConn有100个, 而maxIdle确只有20个。
那么当用户请求上来的时候,每个协程都会从连接池获取连接,拿不到就实例化一个新的pg连接,用完了连接后尝试归还连接池不成功,那么就主动close连接。 谁先close,谁就有time wait。所以,原因找到了。


解决方法很简单,就是把maxIdle调大一点,其实我还是更喜欢go redis的连接池实现,不会立马把大于maxConn和小于maxIdle的连接给关掉,而是给个释放时间,相当于给一个缓冲池。不然每次都要实例化连接,然后close,调用耗时被延长。


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc