首页 > 解决方案 > go-sql-driver:当 wait_timeout 默认为 8h 时获取无效连接

问题描述

一个句子

8h时出现MySQLinvalid connection问题。MaxOpenConnswait_timeout

详细的

我有一个脚本打算从表 A 中读取所有记录,进行一些转换,然后将结果记录写入表 B。代码以这种方式工作:

(1.4.1 遇到的问题,可重现的演示,见下文,使用 1.5)

这个invalid connection问题几乎可以稳定地重现。 

具体运行情况,A表有 67227条记录,通道大小设置为100000,A表scanner(1个goroutine)一次读取1000条,表B inserter(4个goroutine)一次写入50条。最终在表 B 中有67127条记录(2*50 丢失),控制台输出 2 行错误:

[mysql] 2020/12/11 21:54:18 packets.go:36: read tcp x.x.x.x:64062->x.x.x.x:3306: read: operation timed out
[mysql] 2020/12/11 21:54:21 packets.go:36: read tcp x.x.x.x:64070->x.x.x.x:3306: read: operation timed out

(我重现时错误行的数量会有所不同,通常是1、2或3。N错误行与N*50记录插入表B失败相吻合。)

从我的日志文件中,它打印invalid connection

2020/12/11 21:54:18 main.go:135: [goroutine 56] BatchExecute: BatchInsertPlace(): SqlDb.ExecContext(): invalid connection
Stats={MaxOpenConnections:0 OpenConnections:4 InUse:3 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:14 MaxLifetimeClosed:0}
2020/12/11 21:54:21 main.go:135: [goroutine 55] BatchExecute: BatchInsertPlace(): SqlDb.ExecContext(): invalid connection
Stats={MaxOpenConnections:0 OpenConnections:4 InUse:3 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:14 MaxLifetimeClosed:0}

试验和观察

  1. 通过在日志中打印带有 goroutine id 的每个成功/失败写入操作,当所有 4 个插入 goroutine 中的任何一个在 2 次连续写入之间的间隔超过约 45 秒时,似乎总是会发生错误。我认为在将它们插入表 B 之前累积 50 条记录只需要这么长时间。

  2. 相比之下,当我碰巧做了一个改变,让 4 个插入的 goroutine 平均写入一些(即没有一个比其他的写入间隔长得多),没有看到错误。重复3次。

  3. 看起来一个错误只影响一个批次的写操作,后面的批次运行良好。那么为什么不重试出错的批次呢?我想重试一次,它会通过。不过,我不介意继续重试直到成功:

var retryExecTillSucc = func(goroutineId int, records []*MyDto) {
   err := inserter.BatchInsert(records)
   for { // retry until success. This is a workaround for 'invalid connection' issue
      if err == nil { break }
      logger.Printf("[goroutine %v] BatchExecute: %v \nStats=%+v\n", goroutineId, err, inserter.RdsClient.SqlDb.Stats())
      err = inserter.retryBatchInsert(records)
   }
   logger.Printf("[goroutine %v] BatchExecute: Success \nStats=%+v\n", goroutineId, inserter.RdsClient.SqlDb.Stats())
}

令人惊讶的是,随着这种变化,错误批次的重试不断出错并且永远不会成功......

概括

发生错误时,很明显一个(空闲)连接已断开,但我的问题是:

(太长了,不过希望能说清楚,多指教~)


更新

最小的、可重现的例子,包括:

标签: mysqlgo

解决方案


你不使用上下文吗?我想读取超时是由上下文超时或readTimeout参数引起的。

MySQL 没有提供安全有效的取消机制。当 context 被取消或达到时readTimeoutDB.ExecContext返回而不使用连接终止。下次使用连接时会导致“无效连接”。

如果要限制长查询的执行时间,可以使用MAX_EXECUTION_TIME提示而不是上下文。请参阅https://dev.mysql.com/doc/refman/5.7/en/optimizer-hints.html#optimizer-hints-execution-time以供参考。


推荐阅读