gin框架连接mysql数据库连接池泄露

琉璃3年前技术文章2474

1、故障爆发


12月1号上午10点出头,我们收到阿里云监控告警:客户官网探测异常,如图所示:


image.png

然后我们DBA查看了后端数据库实例,发现数据库连接已经被用尽了,导致服务出现异常,如图所示:


image.png


当时我们和客户协商之后决定对数据库会话进行kill处理,如图所示:


image.png


经过临时处理之后,应用恢复正常,如图所示:


image.png


2、再次出现故障


在14:16分,客户再次联系我们,反馈成有部分接口超时,如图所示:


image.png


由于客户的服务底层代码为Go,我们之前建议过把代码里面加入pprof进行debug,否则出现问题我们没有办法看底层的线程池和堆栈信息。但是客户还没有把pprof加入到代码中,在这种情况下,我们尝试通过tcpdump抓取网络包,用来分析go应用到底在干什么。通过wireshark解包后,我们发现了异常情况:go应用和mysql数据库之间进行了大量的TCP Keep-Alive网络包交互,但是没有正常的SQL查询交互,如图所示:


image.png


我当时就判断:应该是go应用存在数据库连接池泄露,导致应用的数据库连接池用尽,最终出现如上图的情况,只有TCP Keep-Alive,没有正常的交互SQL。但是因为没有pprof,再加上是业务高峰期,我们无法定位到泄露代码。客户临时重启应用进行修复。


3、业务低谷问题复现


加入pprof之后,我们跟踪了goroutine,根据跟踪的图,我们还是没有办法定位到故障代码,如图所示:

image.png

这种情况下,问题似乎走入了死胡同。我想了一下:我们之前网络抓包的时候,应用已经处于假死状态了,导致我们无法看到到底是因为什么SQL导致的,如果我们把应用重启,在服务刚开始的时候就进行网络抓包,应该是能抓到问题SQL。我们再次进行抓包,这次发现了异常SQL:每个连接数据库的TCP连接,最终退出的时候,都执行了开启事务。如图所示:


image.png


最终定位到问题代码为以下代码,如图所示:


image.png


我们当时建议在判断if err的时候,添加回滚操作。但是客户还是想深入排查为什么会出现此bug。


4、彻底解决bug


我再次分析了网络包,发现出现问题的TCP线程池为101个。而我们在pprof跟踪的图上面刚刚好看到对应数量的goroutine: runtime goparkunlock,这个goroutine的最上级是 sql(*Tx)awaitDone。如图所示:

image.png

我们使用谷歌搜索了:sql(*Tx)awaitDone,发现很多人也遇到这种问题,而点赞最高的回答,如图所示:

image.png

func (s Service) DoSomething() (err error) {
    tx, err := s.db.Begin()
    if err != nil {
        return
    }
    defer func() {
        if err != nil {
            tx.Rollback()
            return
        }
        err = tx.Commit()
    }()
    if _, err = tx.Exec(...); err != nil {
        return
    }
    if _, err = tx.Exec(...); err != nil {
        return
    }
    // ...
    return
}

image.png

func Transact(db *sql.DB, txFunc func(*sql.Tx) error) (err error) {
    tx, err := db.Begin()
    if err != nil {
        return
    }
    defer func() {
        if p := recover(); p != nil {
            tx.Rollback()
            panic(p) // re-throw panic after Rollback
        } else if err != nil {
            tx.Rollback() // err is non-nil; don't change it
        } else {
            err = tx.Commit() // err is nil; if Commit returns error update err
        }
    }()
    err = txFunc(tx)
    return err
}

image.png

func (s Service) DoSomething() error {
    return Transact(s.db, func (tx *sql.Tx) error {
        if _, err := tx.Exec(...); err != nil {
            return err
        }
        if _, err := tx.Exec(...); err != nil {
            return err
        }
        return nil
    })
}

最终客户采用的解决方案也是stackoverflow.com高赞回答,如图所示:


image.png


客户解决bug后通过测试回复,如图所示


image.png


5、总结


其实问题的根源原因就是在线程中开启了事务,但是在线程出现panic的时候,未做事务回滚处理。由于gin框架会自动recover发生panic的线程,最终导致数据库线程池的泄露。


相关文章

Flink 运行架构简介

Flink 运行架构简介

一、Flink简介Apache Flink 是一个开源的分布式大数据处理引擎,可对有限数据流和无限数据流进行有状态计算。它具备强一致性的计算能力、大规模的扩展性,整体性能非常卓越,同时支持SQL、Ja...

将Nexus作为容器镜像仓库的配置过程

将Nexus作为容器镜像仓库的配置过程

Docker,Helm和Kubernetes的简化容器管理Nexus Repository建立在广泛的企业存储功能之上,是适用于所有Docker镜像和Helm Chart存储库的强大的注册表。Nexu...

企业级大数据安全架构(八)

企业级大数据安全架构(八)

前面第七章详细介绍了部署FreeIPA来做kerberos认证,这节接着介绍FreeIPA高可用部署1.FreeIPA高可用配置说明:在安装完一台ipa-server之后,在另一个备份节点部署ipa-...

hbase开启thrift进程(ThriftServer服务)

hbase开启thrift进程(ThriftServer服务)

简介:Hbase是目前比较火的列存储数据库,由于Hbase是用Java写的,因此它原生地提供了Java接口,对非Java程序人员,它提供了thrift接口服务器。1.启动thrift-server要使...

trino容器对接ldap(二)

trino容器对接ldap(二)

前提:本文前提是在trino容器已经对接上hive组件,并且ldap已经部署完成的基础上进行的对接。前提文章见:helm安装部署trino对接hive(一)安装部署1、设置证书因为不确定是在哪台机器中...

 MySQL优化器特性(九)行数评估

MySQL优化器特性(九)行数评估

查询的行数在成本计算中起了很重要的作用:1、row_evaluate_cost和行数直接相关2、需要访问多少索引页面,和行数直接相关。根据页面大小和平均索引条目长度计算每个索引页面的记录数,根据记录数...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。