mysql - go-sql-driver:当 wait_timeout 默认为 8h 时获取无效连接
问题描述
一个句子
8h时出现MySQLinvalid connection
问题。MaxOpenConns
wait_timeout
详细的
我有一个脚本打算从表 A 中读取所有记录,进行一些转换,然后将结果记录写入表 B。代码以这种方式工作:
- 一个 goroutine 扫描表 A,将记录放入通道;
- 其他四个 goroutine(数量可配置)同时从上面的通道消费,累积 50 行(批量大小可配置)插入表 B,然后再累积 50 行,依此类推。
- 扫描器 goroutine 持有一个
*sql.DB
,而插入器 goroutine 共享另一个*sql.DB
- go-sql-driver:版本 1.4.1 (2018-11-14) 或版本 1.5 (2020-01-07)
(1.4.1 遇到的问题,可重现的演示,见下文,使用 1.5)
- Go 版本:go1.13.15 darwin/amd64
这个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}
试验和观察
通过在日志中打印带有 goroutine id 的每个成功/失败写入操作,当所有 4 个插入 goroutine 中的任何一个在 2 次连续写入之间的间隔超过约 45 秒时,似乎总是会发生错误。我认为在将它们插入表 B 之前累积 50 条记录只需要这么长时间。
相比之下,当我碰巧做了一个改变,让 4 个插入的 goroutine 平均写入一些(即没有一个比其他的写入间隔长得多),没有看到错误。重复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())
}
令人惊讶的是,随着这种变化,错误批次的重试不断出错并且永远不会成功......
概括
发生错误时,很明显一个(空闲)连接已断开,但我的问题是:
- MySQL
wait_timeout
设置为8h,为什么连接超时这么快? - 由于未设置 MaxOpenConns,因此不应成为限制,尤其是考虑到
OpenConnections
日志中只有 4 个。 - 还有什么要检查的潜在根本原因?
(太长了,不过希望能说清楚,多指教~)
更新
最小的、可重现的例子,包括:
- 代码
- 一个示例日志文件
- MySQL错误日志
解决方案
你不使用上下文吗?我想读取超时是由上下文超时或readTimeout
参数引起的。
MySQL 没有提供安全有效的取消机制。当 context 被取消或达到时readTimeout
,DB.ExecContext
返回而不使用连接终止。下次使用连接时会导致“无效连接”。
如果要限制长查询的执行时间,可以使用MAX_EXECUTION_TIME
提示而不是上下文。请参阅https://dev.mysql.com/doc/refman/5.7/en/optimizer-hints.html#optimizer-hints-execution-time以供参考。
推荐阅读
- javascript - 无法在 html/php/javascript 中使用 onclick 调用函数
- layout - 确定 SwiftUI 视图中 SpriteKit SKScene 的实际高度
- python - 在Python中计算数据框中最后一行和所有其他行之间的时间差
- javascript - 在 JavaScript/TypeScript 中使用数组索引创建函数数组
- javascript - 更改时将选择值传递给母版页
- download - Google 直接下载链接文件类型
- java - Spring Data JPA:如何在 JPA 规范中的 LEFT JOIN 字段上获得不同的记录
- c - 在 Windows 上从 Python 运行 C 程序
- python - 如何将存储在文本文件中的数据转换为 csv
- python-3.x - 如何聚合 Python Pandas 数据框,使变量的值对应于在 aggfunc 中选择变量的行?