1. 问题引出
模型:应用[mysql驱动]-> sqlproxy[sql转换] -> 达梦
sqlproxy使用的是database/sql中自带的连接池,达梦数据库重启了之后,sqlproxy中的连接池无法响应任何SQL,都报了如下错误。
2. 问题分析
产线连接mysql也是用的database/sql中自带的连接池,使用了很多年并未报过此类问题,下面进行代码分析:
上面代码可以说明,连接池已经提供了完备的机制来处理连接错误,从连接池中取出的连接如果失效时会自动重试,必要时会直接创建新的连接,来保证sql请求不会因为连接失效而响应失败。
为此专门进行了对比验证,将sqlproxy改为连接到mysql,看是否存在此问题。
这里偷了个懒,没有去安装新的mysql,而是将两台sqlproxy串联起来,结构如:
应用---> sqlproxy[mysql] ---> sqlproxy[达梦] ---> 达梦
。这样对于sqlproxy[mysql]来说,
将sqlproxy[达梦】重启就等价于将数据库重启。
如上图所示,重启后能正常访问,说明sqlproxy[mysql]的连接池对DB重启是能自动响应的。同时也发现,sqlproxy[mysql]侧在控制台打印了一条异常信息:
closing bad idle connection: connection reset by peer
对应打印此日志的相关代码位置:
从这个代码可以看出,mysql驱动每次从连接池新拿出一个连接时,都会进行有效性检测,当检测到连接不可用时,会打印连接错误信息并将错误转换为driver.ErrBadConn返回。
// ErrBadConn should be returned by a driver to signal to the sql
// package that a driver.Conn is in a bad state (such as the server
// having earlier closed the connection) and the sql package should
// retry on a new connection.
var ErrBadConn = errors.New("driver: bad connection")
这个driver.ErrBadConn是go官方SDK定义的,上文提到,database/sql包的连接池通过检测此错误来处理失效连接并自动重试。
而前面连接达梦时我们获取到的错误信息为:Error 6001: 网络通信异常
,并不是go官方database/sql所定义的driver.ErrBadConn.
由此可以基本判断,是达梦驱动在遇到连接问题时未按照标准的driver.ErrBadConn返回而导致。
3. 解决方案
解法1:sqlproxy主动检测失效连接,开一个定时器,通过扫描来监听db实例的连接状态,发现异常主动重连。代码大概如下所示:
func (p *Proxy) checkDBConn() {
for {
time.Sleep(time.Minute)
for i, db := range p.dbs {
if err := db.Ping(); err != nil {
// 连接不可用,尝试重新连接或切换到其他可用的数据库
p.reconnectDB(i)
}
}
}
}
func (p *Proxy) reconnectDB(index int) {
// 尝试重新连接或切换到其他可用的数据库
// ...
}
- 优点:不依赖于具体驱动的实现。
- 缺点:在database/sql连接池已经提供了一种方法机制的基础上,这样做明显有些功能冗余。而且每个db实例都需要单独来扫,有一定有开销。
解法2:修改dm驱动。将dm驱动中相关API函数返回的Error 6001: 网络通信异常
统一改为driver.ErrBadConn再返回。
- 优点:和go官方解决此问题的方法对齐,更加合理。
- 缺点:dm驱动变成了定制,会给后续更新dm驱动带来不便。
我们还是倾向于更合理的解决方法,选择了方法2,至于相应的弊端,准备在达梦社区提交一个issue争取让达梦团队也配合修改。
4. 方案实施
由于dm驱动代码是混淆过的,我们难以确认communication error这个错误在其它地方是否有特殊用途,如果直接从定义的地方替换这个错误就会有风险。
比较稳妥的做法是在驱动对外公开的所有API方法中都替换下这个错误,虽然改动量多一些,但结果可控。具体做法为:
- 定义一个replaceError函数,如果是网络连接类错误就替换成标准的driver.ErrBadConn。
func replaceError(err error) error {
if isError(err, ECGO_COMMUNITION_ERROR) {
log.Printf("use driver.ErrBadConn intread: %s", err.Error())
return driver.ErrBadConn
}
return err
}
对于以下公开的API方法,都在返回err前替换错误码:
- Query 和 QueryContext
- Exec 和 ExecContext
- Prepare 和 PrepareContext
- Ping
- Begin 和 BeginTx
- Commit
- Rollback
代码改动示例如下:
运行后,在非事务场景下运行正常,在事务场景下偶现这个错误:
经反复测试,只有在手动mysql命令连上去第一个操作使用事务时会出现,用程序连接却没有出现此错误,看相应的错误也已经被检测到并替换为ErrBadConn,暂时不明原因,待后续跟踪。