完了,MySQL 复制卡死!!!

2022-12-19 19:29:35 浏览数 (1)

背景

真事!有一个 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

0 人点赞