前段时间我在工作中遇到了一个偶发的mysql报错问题,感觉是个常见问题,所以将问题的现象和排查过程记录于此,希望可以帮助到有需要的人。
1. 问题现象
问题出现在我负责维护的一个golang语言写的服务模块中,这个模块在线上是以docker容器的方式运行的,运行在k8s平台中。
当服务通过go-sql-driver/mysql包访问mysql时,偶尔会报“invalid connection”错误:
通过kubectl logs 查看pod标准输出的日志,可以看到mysql的报错:
报错跟时间、具体的代码逻辑没有明显关系。报错的sql语句也并不固定。
2. 原因分析
通过查资料发现,可能是客户端的mysql连接一直空闲,没有清理,mysql server检测到连接空闲超过wait_timeout后,主动关闭了该连接,client端再次拿到该连接去请求数据库时,就会报unexpected EOF和invalid connection。
2.1. 本地复现步骤:
- 本地起一个mysql server;
- 设置mysql server的wait_timeout为10s:
set global wait_timeout = 10;
- 找个golang项目,将go-sql-driver/mysql的版本设置为v1.4.1 (1.5.0版本已经修复了这个问题):
go get github.com/go-sql-driver/mysql@v1.4.1
-
写个单测:
func TestDBConn(t *testing.T) { Conf = Config{ DB: DBConfig{ Driver: "mysql", Host: "127.0.0.1", Port: 3306, User: "root", Password: "***", // 改为你本地MySQL server真正的密码 Database: "test", MaxOpenConn: 1, // 只启动一个连接 MaxIdleConn: 1, ConnectTimeoutInSec: 30, MysqlDriverParams: "charset=utf8&parseTime=True&loc=Local", }, } err := InitDB() // 创建一个连接,并放入连接池 result := DB.Exec("select sleep(1)") fmt.Printf("err: %+v\n", result.Error) // 等待11s,超过wait_timeout的10s,让连接失效 time.Sleep(11 * time.Second) // 会从连接池中拿到那个已经失效的连接,查询会失败 result = DB.Exec("select sleep(1)") fmt.Printf("err: %+v\n", result.Error) }
执行单测可以发现报错:
3. 修复方法
3.1. 升级go-sql-driver版本
将go-sql-driver/mysql包版本升级为1.5.0或以上,可以在拿到连接请求数据库前自动检测一遍,如果发现连接失效,会重新拿个可用的连接或新建连接,此时虽然会打印日志,但不影响使用:
3.2. 设置connMaxLifetime
调用db.SetConnMaxLifetime,即客户端维护的连接的最大生命周期,设置成一个小于MySQL服务端wait_timeout的值,如下:
这样,拿到连接后会自动检查连接是否存在超过该时间,如果是,则直接清理该连接,重新拿个可用的连接或新建连接,此时不会打印任何日志:
当然,设置这个参数值时需要确认MySQL服务端的wait_timeout值是多少,可以跟数据库平台的相关同学确认,或和OP同学确认,如果MySQL服务端修改了wait_timeout的值,也应该同步到业务方,避免出现这类客户端连接生命周期大于服务端维持时间的问题出现。
4. 后记
当时我采用的修复方法是即升级了go-sql-driver/mysql的包版本,也加上了客户端的connMaxLifetime的限制,上线后确实不再出现“invalid connection”的问题了。
不过,现在看来,“unexpected EOF”(客户端看来连接非预期地断开)和invalid connection(客户端看来是无效的连接)这两个报错信息都很含糊,可能的原因有很多,不一定是连接的生命周期不匹配的问题,所以,当大家遇到类似的问题时,还是需要根据自己服务的实际情况排查,这篇文章只是提供一种可能的情况。