MySQL数据库线程池泄露场景解决方案
在数字化时代,数据是企业的核心资产,而数据库则是存储和管理这些资产的重要仓库。本文通过云掣为某信息化管理行业客户快速解决MySQL数据库故障的案例,详细的分享了MySQL数据库出现问题时的解决思路和具体方案。以帮助广大DBA快速定位、解决MySQL数据库线程池泄露的问题,希望能为大家提供一些借鉴和启示。
方案背景
首次产生故障
2022年12月1号上午10点左右,云掣收到了阿里云的监控告警,某信息化管理行业客户的官网探测到了异常,如图所示:
我们的DBA立刻查看了客户的后端数据库实例,发现数据库连接被用尽,导致服务出现了异常。
和客户协商后决定对数据库会话进行kill处理。
经过临时处理后,应用恢复正常。
再次出现故障
当天下午14:16,客户再次联系我们,反馈部分服务接口超时,如图所示:
由于客户的服务底层代码为Go,我们曾建议在代码中加入pprof进行debug,否则出现问题时我们无法查看底层的线程池和堆栈信息,但客户尚未将pprof加入代码。
在这种情况下,我们尝试通过tcpdump抓取网络包,试图分析go应用到底在干什么。通过wireshark解包后,我们发现了异常情况:go应用和MySQL数据库之间进行了大量的TCP Keep-Alive网络包交互,但是没有正常的SQL查询交互。
初步判断为go应用存在数据库连接池泄露,导致应用的数据库连接池用尽,最终出现如上图的情况,只有TCP Keep-Alive,没有正常的交互SQL。但是因为没有pprof,再加上处于业务高峰期,我们无法快速定位到泄露代码,客户选择临时重启应用进行修复。
解决方案
问题复现
加入pprof之后,我们跟踪了goroutine,但根据跟踪的图,还是无法定位到故障代码。
在这种情况下,问题似乎走入了死胡同。
但仔细回想,之前网络抓包的时候,应用已经处于假死状态,所以我们无法看到到底是因为什么SQL导致的。如果重启应用,在服务刚开始的时候就进行网络抓包,应该就可以抓到问题SQL。
我们再次进行抓包,这次发现了异常SQL:每个连接数据库的TCP连接,最终退出的时候,都执行了开启事务,如下图所示:
最终定位到问题代码为以下代码:
最终解决
因为客户想深入排查为什么会出现此bug,所以我们再次分析了网络包,发现出现问题的TCP线程池为101个。而我们在pprof跟踪的图中看到对应数量的goroutine: runtime goparkunlock,这个goroutine的最上级是 sql(*Tx)awaitDone,如图所示:
使用谷歌搜索:sql(*Tx)awaitDone,发现很多人都遇到了相似问题,点赞最高的回答如下⬇️:
您要确保Begin()、Commit()和Rollback()出现在同一个函数中。它使交易更容易跟踪,并让您确保它们通过使用defer.
这是一个这样的例子,它根据是否返回错误来执行 Commit 或 Rollback:
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 }
这可能会有点重复,另一种方法是使用事务处理程序包装事务:
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 }
使用上面的事务处理程序,可以这样做:
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高赞回答,成功解决了bug并通过测试,如下图所示:
总结
本次BUG根源在于线程中开启了事务,但是在线程出现panic的时候,未做事务回滚处理。由于gin框架会自动recover发生panic的线程,最终导致数据库线程池的泄露。在此次故障中,云掣迅速响应了客户的需求,快速定位问题根源,尽可能的实现了业务停机时间的最小化,成功保障了数据库的安全。
展望未来,随着技术发展和业务变化,数据库运维将面临更多挑战。云掣也会持续不断地进步、创新,完善优化运维策略,应对各种挑战,为客户的数据资产提供更高效、可靠的保障。