golang服务访问MySQL报“invalid connection”问题排查

前段时间我在工作中遇到了一个偶发的mysql报错问题,感觉是个常见问题,所以将问题的现象和排查过程记录于此,希望可以帮助到有需要的人。

1. 问题现象

问题出现在我负责维护的一个golang语言写的服务模块中,这个模块在线上是以docker容器的方式运行的,运行在k8s平台中。
当服务通过go-sql-driver/mysql包访问mysql时,偶尔会报“invalid connection”错误:
报错日志
通过kubectl logs 查看pod标准输出的日志,可以看到mysql的报错:
报错日志2
报错跟时间、具体的代码逻辑没有明显关系。报错的sql语句也并不固定。

2. 原因分析

通过查资料发现,可能是客户端的mysql连接一直空闲,没有清理,mysql server检测到连接空闲超过wait_timeout后,主动关闭了该连接,client端再次拿到该连接去请求数据库时,就会报unexpected EOF和invalid connection。

2.1. 本地复现步骤:

  1. 本地起一个mysql server;
  2. 设置mysql server的wait_timeout为10s: set global wait_timeout = 10;
  3. 找个golang项目,将go-sql-driver/mysql的版本设置为v1.4.1 (1.5.0版本已经修复了这个问题): go get github.com/go-sql-driver/mysql@v1.4.1
  4. 写个单测:

    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的值,如下:
    修改客户端connMaxLifetime
    这样,拿到连接后会自动检查连接是否存在超过该时间,如果是,则直接清理该连接,重新拿个可用的连接或新建连接,此时不会打印任何日志:
    设置客户端lifetime后单测结果
    当然,设置这个参数值时需要确认MySQL服务端的wait_timeout值是多少,可以跟数据库平台的相关同学确认,或和OP同学确认,如果MySQL服务端修改了wait_timeout的值,也应该同步到业务方,避免出现这类客户端连接生命周期大于服务端维持时间的问题出现。

4. 后记

当时我采用的修复方法是即升级了go-sql-driver/mysql的包版本,也加上了客户端的connMaxLifetime的限制,上线后确实不再出现“invalid connection”的问题了。
不过,现在看来,“unexpected EOF”(客户端看来连接非预期地断开)和invalid connection(客户端看来是无效的连接)这两个报错信息都很含糊,可能的原因有很多,不一定是连接的生命周期不匹配的问题,所以,当大家遇到类似的问题时,还是需要根据自己服务的实际情况排查,这篇文章只是提供一种可能的情况。

发表评论