背景
真事!有一个 Python 程序它会周期性的从 MySQL 的备机上读数据,用于完成它的业务逻辑。大致的代码如下(已经去掉了所有与业务相关的内容),最近发现它阻塞了 DDL 语句,导致MySQL 主从复制卡住不动。
代码语言:javascript复制#!/usr/bin/env python3
# -*- coding: utf8 -*-
"""
xxx xxx xxx
"""
import time
import logging
from mysql.connector import connect
logging.basicConfig(level=logging.DEBUG)
def main():
cnx = None
cursor = None
sql = "select pk, v from tempdb.t;"
try:
# 注意这一行有问题!!!
cnx = connect(host="127.0.0.1", port=3306, user="root", password="xxxx")
cursor = cnx.cursor()
while True:
cursor.execute(sql)
for pk, v in cursor.fetchall():
logging.info(f"iter rows pk = {pk} v = {v} .")
# 业务逻辑
time.sleep(10)
except Exception as err:
logging.error(str(err))
if __name__ == "__main__":
main()
为了减少不必要的麻烦,我把业务当时的表名换成了 tempdb.t ,总的来讲不影响从技术层面讲问题。
排查
业务用的 MySQL 版本是 5.7.36 所以排查问题的工具相比 MySQL-8.0.x 来讲就要少一些。但是他们走运呀!当时备库上只有上面那段代码发起的一个连接;所以直接 kill 掉备机上面唯一的连接后, SQL 线程就恢复了。
如果事发的时候正好是业务的高峰期,几百个连接在上面对于 MySQL-5.7 来讲确实就不太好分析。 虽然现场已经没有了,但是问题到我这里了,原因还是要搞清楚才行。
复现
由于是线上环境,所以就不能在上面接直搞,我只能在自己的开发环境上看能不能复现。我把代码在测试环境上执行了下,动气来了,直接就能看到预期之外的锁(这里的开发环境是 8.0.31 ,和线上的 5.7.36 有一定的差别),元数据锁的信息如下。
代码语言:javascript复制select a.OBJECT_TYPE, a.OBJECT_SCHEMA, a.OBJECT_NAME, a.LOCK_TYPE, a.LOCK_STATUS, b.PROCESSLIST_ID from performance_schema.metadata_locks as a join performance_schema.threads as b on a.OWNER_THREAD_ID = b.THREAD_ID where a.OBJECT_SCHEMA = 'tempdb';
------------- --------------- ------------- --------------------- ------------- ----------------
| OBJECT_TYPE | OBJECT_SCHEMA | OBJECT_NAME | LOCK_TYPE | LOCK_STATUS | PROCESSLIST_ID |
------------- --------------- ------------- --------------------- ------------- ----------------
| TABLE | tempdb | t | SHARED_READ | GRANTED | 8 |
| SCHEMA | tempdb | NULL | INTENTION_EXCLUSIVE | GRANTED | 8 |
------------- --------------- ------------- --------------------- ------------- ----------------
2 rows in set (0.00 sec)
可以看到程序执行之后会一直持有 tempdb.t 这个表的共享锁,并且程序不停止这个锁就一直不释放。
现在基本上明确原因了,由于 Python 程序的共享锁没有释放,导致改表的 DDL 语句拿不到表的排他锁,所以看起来就是复制的 SQL 线程卡住,DDL 进行不下去。
到这里还没有完,有推理还要验证下推理才行。
回测
从上面的分析我们可以知道,程序一起来的时候就开启了一个事务,但是在代码中并没有看它有 begin 这样的操作。这个和客户端 autocommit 设置为 False 时的行为比较一致,所以我们要做的就是把连接的 autocommit 设置为 True ,验证下。这个只要在连接的时候多加一个参数。
代码语言:javascript复制cnx = connect(autocommit=True
... 这里省去其它连接的相关参数
)
现在程序执行起来的时候再去查元数据锁的情况,发现已经没有连接会持有 tempdb.t 的共享锁了。
代码语言:javascript复制select a.OBJECT_TYPE, a.OBJECT_SCHEMA, a.OBJECT_NAME, a.LOCK_TYPE, a.LOCK_STATUS, b.PROCESSLIST_ID from performance_schema.metadata_locks as a join performance_schema.threads as b on a.OWNER_THREAD_ID = b.THREAD_ID where a.OBJECT_SCHEMA = 'tempdb';
Empty set (0.00 sec)
到这里应该可以说是 100% 确认就是这个问题了,看来我这种资深 DBA 的直觉还可以,但是考虑到自己的开发环境是 8.0.31 ,而线上环境是 5.7.36 ;为了到时候不要被打脸,我还是去官网看一下 autocommit 的对应的文档。
代码语言:javascript复制If autocommit mode is disabled within a session with SET autocommit = 0,
the session always has a transaction open.
A COMMIT or ROLLBACK statement ends the current transaction and a new one starts.
好了,行为和文档上说的一致,那就是的了。
链接如下:MySQL :: MySQL 5.7 Reference Manual :: 14.7.2.2 autocommit, Commit, and Rollback
MySQL 官方文档也不是特别理想
这个事吧,我觉得 mysql-connector-python 这个程序的文档也不是特别好,它文档上的示例代码都是没有指定 autocommit 的;应该会有好多同学拿来就用了。还好我早年为了提升自己的 Python 功力有研究过官方这套驱动的代码,不然这个 autocommit 怎么设置我都要好好地找一下才行。
代码语言:javascript复制import mysql.connector
cnx = mysql.connector.connect(user='scott', password='password',
host='127.0.0.1',
database='employees')
cnx.close()
链接如下 MySQL :: MySQL Connector/Python Developer Guide :: 5.1 Connecting to MySQL Using Connector/Python