版本情况dbutils:1.1; pymysql:0.9.3; python:2.7.13
线上情景
最近线上维护时,由于只需要更改数据库配置,所以就重启了数据库,而python应用没有重启。在重启数据库后,日志显示正常,也能成功入库。后来接到反馈表示有部分数据没有入库,紧急重启python应用,后续数据入库正常。而我则负责找出原因以及修复bug的工作。
调研原因
在排查完其他问题后,最异常的是对于有部分请求,日志显示处理成功了,但是却没入库,排查了好几天找不到原因。为此写了demo来帮助排查,为了可以自动commit,采用的是setsession=[“set autocommit=1”]方式设置每个底层的连接为自动提交。在测试demo期间,数据库重启后之后的sql就无法入库。demo代码如下:
1 | # -*- coding: utf-8 -*- |
同时分析dbutils的关键源码:
- PooledDB:代表线程池,负责控制连接的数量、达到上限时是否阻塞、取出连接、放回连接等连接管理层面的工作。
- PooledDedicatedDBConnection:池专用连接的辅助代理类,调用pooledDB.connection()时,返回的就是这个链接。它保存了底层连接SteadyDBConnection,调用PooledDedicatedDBConnection的任何方法,除了close,都会直接调用SteadyDBConnection对应的方法。
- SteadyDBConnection:稳定数据库连接,负责封装驱动层面(如pymql)的数据库连接、创建数据库连接、执行数据库连接的ping方法、执行execute方法。
SteadyDBConnection的_ping_check()
方法有重连机制,对这部分源码添加debug信息帮助排查问题:
1 | def _ping_check(self, ping=1, reconnect=True): |
分别修改myreconnect的值进行测试:
- 测试1:my_reconnect=True(对于pymysql的ping默认值为True),运行demo期间重启数据库。
1 | 异常开始: |
- 测试2:my_reconnect=False,运行demo期间重启数据库。
1 | 异常开始: |
经过调试发现,pymysql的ping方法默认情况会进行重连,而不是dbutils的重连。所以在dbuitils的_ping_check
方法的重连机制有几率会不执行,因为pymysql的ping已经重连了,从而导致 setsession 中的配置没有在拿连接的时候设置进去。
原因总结:
- pymysl的ping有个默认参数reconect,并且为true,即reconnect=True
- dbutils的ping机制依赖于pymysql原生的ping,并且默认不设置任何参数,即在目前版本的dbutil下其ping默认会自动reconnect
- dbutils的ping默认调用时机:只要从pool中拿连接就会进行ping
- 始化PooledDB时,通过setsession参数的方式,设置自动commit,即 setsession=[“set autocommit=1”]。因此只要是从dbutil拿连接的时候,都会预先配置该session,即执行业务sql前,先执行setsession的内容
- 在连接丢失或者其他异常时,由于pymysql的ping默认进行重连,故dbutils层面无法感知已经重连,setsession也不会再次执行,故后续该连接执行的sql不会进行commit。
- 这是个bug,已经提issue给dbutils的作者,在最近的版本会修复这个bug。问题的重现以及修复方法详见:When use pymsql driver, the setsession params for PooledDB is not work after mysql server restart
解决方案
在dbutils未修复该bug前,可以通过PoolDB的kwargs参数透传{“autocommit”:True}到pymysql中,这样即使通过pymysql的ping方法重连的连接也会保留自动提交的功能。