当前位置: 首页 > 科技观察

MySQL-不是MySQL问题的MySQL问题

时间:2023-03-12 19:55:09 科技观察

1.自定义函数BUG导致的问题问题是运行一个SQLupdate测试集p_id=getPid(c_id)如下;这张表只有10w条数据,但是语句总是无法完成。如果给语句加上limit,当limit为50000时可以执行,但是当limit为80000时就无法完成。而且有一个现象就是语句会一直出现在开表的状态。由于语句无法执行完成,因此需要找出无法完成的原因。先找出等待的原因,比如:锁等待?CPU满了?IO满了吗?经过排查,发现这条语句在实际执行时占用了很多CPU,于是我们将正常执行和异常情况分开收集,发现异常时正常的逻辑几乎变成了一条线,而异常逻辑占用大量CPU。CPU如下:很明显,当这条语句执行出现异常时,CPU并没有处理正常的逻辑。而它的上层调用sp_head::execute_function是执行函数的上层调用,而这里只有一个自定义函数,所以几乎可以确定是自定义函数内部逻辑有问题。然后我们使用pstack查看异常情况的执行栈,多次测试正常逻辑的pstack执行栈,发现其中一个逻辑输入参数在不断扩大,内存长度在增加(length)。当然这里都是我测试环境的搭建,不是live环境。那么我们可以确认是函数内部的拼接有问题,然后我们打开自定义函数getPid,发现里面有一个while循环,字段在循环内部进行拼接,之后的返回值拼接完成就是这个while循环。在一定条件下会出现死循环,根据pstack中作为参数输入的字符串,实际上是在拼接某个字段。该字段的值为1,由于无限循环拼接很长,所以这里为1。,1,1,1,1,1...,所以我们也得到了问题行的字段值1,我们还可以通过死循环条件判断另外一个字段的值,然后根据这两个字段,您可以通过检查表找到导致无限循环的行。当然,这只是一个想法,不方便给出这个自定义函数。死循环的问题刚好符合CPU满的情况。其次,由于自定义函数的内存中有一条select语句,遇到自定义函数的死循环,这条语句会一直循环运行,所以观察异常执行过程中观察openingtables更新声明。2、应用代码中静态变量引起的死锁问题是MySQL层的死锁,但是死锁表很简单,只有少量记录,而且只有主键,没有其他索引.这里假设主键是id,是RC隔离级别。每次执行的语句也是根据主键查询和更新的,如下:select*fromtestwhereid=1进行更新;更新测试集name='a'whereid=1;commit;死锁如下(此处删除详细数据):------------------------LATESTDETECTEDDEADLOCK------------------------2022-07-0619:48:380x7efc44162700***(1)TRANSACTION:TRANSACTION12739556,ACTIVE0secstartingindexreadmysqltablesinuse1,locked1LOCKWAIT3锁结构,堆大小1136,2行锁MySQL线程ID627119,操作系统线程句柄139619931977472,查询ID129095157192.168.1.81根更新更新测试集名称='1'***whereid=(1)HOLDSTHELOCK(S):RECORDLOCKSspaceid388279pageno4nbits152indexPRIMARYoftable`test`.`test`trxid12739556lock_modeXlocksrec但不是gapRecordlock,heapno82PHYSICALRECORD:n_fields16;紧凑格式;信息位0***(1)等待授予此锁:记录锁空间ID388279页码4n位152索引PRIMARYoftable`test`.`test`trxid12739556lock_modeXlocksrecbutnotgapwaitingRecordlock,heapno55PHYSICALRECORD:n_fields16;紧凑格式;信息位0***(2)TRANSACTION:TRANSACTION12739557,ACTIVE0秒起始索引readmysql表正在使用1,锁定1LOCKWAIT3锁结构,堆大小1136,2行锁MySQL线程ID627114,操作系统线程句柄139621354526464,查询id129095158192.168.1.81根更新更新测试集名称='o'其中id=2***(2)HOLDSTHELOCK(S):RECORDLOCKSspaceid388279pageno4nbits152indexPRIMARY表`test`.`test`trxid12739557lock_modeXlocksrec但不是gapRecordlock,heapno55PHYSICALRECORD:n_fields16;紧凑格式;信息位0***(2)等待授予此锁:记录锁空间id388279页号4n位152表`test`.`test`的主索引trxid12739557lock_modeX锁rec但不是间隙waitingRecord锁,堆号82PHYSICALRECORD:n_fields16;紧凑格式;infobits0***WEROLLBACKTRANSACTION(2)如果出现这种死锁问题,一般的分析路径是:是不是业务代码有问题,还是执行计划有问题。最后就是复现和分析MySQL本身的问题。我们分析第一点的时候,业务代码非常简单明了。就是之前的交易逻辑。老实说,这种事务似乎不太可能导致死锁,因为简单查询是查询的主键。更新的时候只是通过主键来更新一个字段的值,除了主键之外没有其他的索引。这种情况下,一般只会阻塞,不会造成死锁。然后我们在测试环境模拟死锁的时候打开generallog,发现并不是我们想象的那样。多线程的语句和事务在一个session中交替进行,很奇怪。言外之意就是多个业务线程一一对应。一个session,大致如下:begin;更新集name='o'whereid=2commit;开始;select*fromtestwhereid=1进行更新;select*fromtestwhereid=3进行更新;select*fromtestwhereid=4进行更新;更新测试集name='a'whereid=3;更新测试集name='a'whereid=1;犯罪;更新集name='o'whereid=4;看起来像多线程并发,所有的语句都堆在同一个会话中。然后进一步分析,代码变量的定义,我们发现代码中设置了connection变量的属性为static类型,开发环境当然是java。我们可以把它比作C++。在C++中,如果类变量的属性加上static,就表示它是静态的。变量,这个变量的值不存在于栈中,而是存在于静态全局区中。此类实例化的所有对象共享此静态变量。也就是说,如果一个实例化对象修改了这个Static变量,那么所有的实例化对象都会被修改,当然java/python也有类似的使用方法。主要看内存是栈内存/堆内存/全局内存。那么这个问题就变得简单了。当多个线程同时初始化并建立连接时,实际上所有线程最终都只得到一个连接。类似如下:最后写了一个测试用例进行验证(见文末),很难成功运行,因为4个线程同时使用了一个connect,感觉应该是像下面的C这样搞定result(mysql_store_result)和freeresult(mysql_free_result)可能的情况当时不知道,当然我也没有仔细研究lib库函数的使用方式。写法也可能有问题。无论如何,有各种崩溃(核心转储)。但是偶尔成功的时候,在通用日志中可以看到如下日志,这里是所有线程堆到同一个session中的语句:staticvariable:2022-07-08T07:07:50.364174Z173Queryselect12022-07-08T07:07:50.365168z173QuerySelect22022-07-08T07:07:50.365903z173QuerySelect3202222-07-08T07:07:50.3703900202222-07-07:51.367748QUERY-08T07:07:51.367903Z173Queryselect12022-07-08T07:07:51.368161Z173Queryselect3很明显这是一个173的sessionid,但实际上测试用例4个线程会连续运行select0/select1/select2/select3。但是四个线程对应同一个session,这也符合我们的实际情况。这样如果多个应用启动多线??程,混跑语句会出现如下情况:app1multithreading:begin;select*fromtestwhereid=1forupdate;select*fromtestwhereid=2进行更新;select*fromtestwhereid=3进行更新;更新集名称='a'whereid=1;updatesetname='a'whereid=2;commit;app2multithreading:begin;select*fromtestwhereid=2forupdate;select*fromtestwhereid=1forupdate;select*fromtestwhereid=3forupdate;updatesetname='a'whereid=2;更新集name='a'whereid=3;commit;事务被乱序展开,死锁的概率当然就大大增加了。这也是我们在实际环境中看到的。当然,如果测试用例使用了局部变量,就没有问题。改成局部变量后,正常执行如下:2022-07-08T07:18:22.582624Z225Queryselect02022-07-08T07:18:22.582732Z222Queryselect22022-07-08T07:18:22.582638Z223查询选择12022-07-08T07:18:22.583214Z224查询选择32022-07-08T07:18:23.583894Z225查询选择02022-07-08T07:18:23.583973Z222查询选择22022-07-07-0838:2137Z223Queryselect12022-07-08T07:18:23.584315Z224Queryselect3这里有4个线程对应4个会话,每个线程运行自己的语句。附件是C++测试用例。如果改成局部变量后4个线程对应4个session,可以正常运行没有问题如下。静态变量很容易导致各种崩溃。#include#include#include#include"/opt/mysql/mysql3306/install/include/mysql.h"#include#include使用命名空间标准;类My_Con{public:MYSQLconn_ptr;My_Con(constchar*host,constchar*user,constchar*passwd,unsignedintport){mysql_init(&conn_ptr);if(mysql_real_connect(&conn_ptr,host,user,passwd,NULL,port,NULL,0)==NULL){printf("err:mysql_real_connect()错误%s\n",mysql_error(&conn_ptr));退出(1);}}MYSQL*get_conn(){返回&conn_ptr;}//~My_Con(){mysql_close(&conn_ptr);cout<<"closeconnect"<get_conn();我的ID=我;query_res=NULL;}void*get_string(intid){sprintf(strtest,"select%d;",id);cout<