一、背景 近期,公司RDS云产品的MySQLServer版本进行升级,由目前使用的5。7。26版本升级到最新版本5。7。31;升级后测试同学发现:在MySQL创建用户后,5。7。31版本重新启动集群会出现启动失败的现象;而5。7。26版本在相同测试场景下是正常启动的。这到底是为什么呢? 二、问题复现 2。1实验环境CentOS7。5MySQL5。7。31 2。2操作步骤 按照测试同学的测试步骤,首先创建一个用户:mysqlcreateusertestidentifiedby1234;QueryOK,0rowsaffected(0。01sec) 然后关闭这里需要介绍一下,我们集群的关闭方式是如下方式:shellkubectldeletepodyuelei57f3b62000nqfusionadmin 这种方式的内部实现类似于kill9模式。所以我在线下环境使用kill9的方式来复现,操作如下:shellkill9(pidofmysqld) 然后重启mysqld,操作如下:shellmysqldsafedefaultsfileetcmysqlmy。cnf 此时问题复现了,mysqld启动失败,我们查看了下error日志,信息如下:。。。20200818T10:13:55。50641508:000〔ERROR〕usrlocalmysqlbinmysqld:Table。mysqluserismarkedascrashedandshouldberepaired20200818T10:13:55。50655308:000〔ERROR〕Fatalerror:Cantopenandlockprivilegetables:Table。mysqluserismarkedascrashedandshouldberepaired20200818T10:13:55。50665808:000〔ERROR〕Fatalerror:FailedtoinitializeACLgranttimezonesstructuresorfailedtoremovetemporarytablefiles。20200818T10:13:55。50678908:000〔ERROR〕Aborting。。。 根据报错信息可以看出:MySQL的权限系统表发生了损坏,导致了mysqld启动失败;由于在MySQL5。7及其之前版本该表是MyISAM引擎,且该引擎不支持事务,所以在mysqld异常崩溃会导致该类型引擎表的损坏;但在mysqld启动时是有参数控制MyISAM引擎的恢复模式,且该参数在我们产品中也配置到了my。cnf中,如下所示:〔mysqld〕myisamrecoveroptionsBACKUP,FORCE 2。3参数解析 对于该参数的官方文档的解释如下: 设置MyISAM存储引擎恢复模式。选项值是OFF、DEFAULT、BACKUP、FORCE或QUICK的值的任意组合。如果指定多个值,请用逗号分隔。指定不带参数的选项与指定DEFAULT相同,指定显式值将禁用恢复(与OFF值相同)。如果启用了恢复,则mysqld每次打开MyISAM表时,都会检查该表是否标记为已崩溃或未正确关闭。(只有在禁用外部锁定的情况下运行,最后一个选项才起作用。)在这种情况下,mysqld在表上运行检查。如果表已损坏,mysqld将尝试对其进行修复。OFF:Norecovery。DEFAULT:Recoverywithoutbackup,forcing,orquickchecking。BACKUP:Ifthedatafilewaschangedduringrecovery,saveabackupofthetblname。MYDfileastblnamedatetime。BAK。FORCE:Runrecoveryevenifwewouldlosemorethanonerowfromthe。MYDfile。QUICK:Donotchecktherowsinthetableiftherearenotanydeleteblocks。 服务器自动修复表之前,它将有关修复的注释写到错误日志中。如果您希望能够在无需用户干预的情况下从大多数问题中恢复,则应使用选项BACKUP,FORCE。即使某些行将被删除,这也会强制修复表,但是它将旧的数据文件保留为备份,以便您以后可以检查发生了什么。 全局变量,只读变量,默认为OFF。 三、问题修复 这类MySQL用户表损耗的问题解决方式也是有多种,我这里列举其中一种: (1)my。cnf中的〔mysqld〕标签下添加skipgranttables,启动时跳过加载系统字典。〔mysqld〕skipgranttables (2)重启mysqld,然后修复mysqlschema下的所有表。shellmysqlcheckurootpxxxmysqlautorepair。。。mysql。userwarning:1clientisusingorhasntclosedthetableproperlystatus:OK (3)在〔mysqld〕标签下注释或删除掉skipgranttables,然后重启mysqld。shellmysqldsafedefaultsfileetcmysqlmy。cnf 此时mysqld是可以正常启动的,无异常。 四、深入排查 在产品化中,以上修复方式很不优雅,只是作为临时的解决方案;并且也存在一些令人疑惑的点:通常情况下在my。cnf中设置myisamrecoveroptionsBACKUP,FORCE时,启动时会自动修复MyISAM损坏的表;在上述场景中为什么没有自动修复呢?相同环境下,5。7。26版本mysqld启动是正常的,但是5。7。31版本mysqld启动出现如上述现象;难道是MySQL做了改动吗? 带着这些疑问,我们继续排查出现该现象的原因;此时Google也没有找到一些有效的信息,那么只能通过MySQL源代码来寻找一些答案。 首先需要下载mysql5。7。31版本的源代码,并搭建mysqldebug环境;具体步骤可以自动Google搜索一下,本文就不再赘述了。 在源代码中搜索一下关键词,用于打断点的位置,然后进行调试:确认MySQL版本shellcatVERSIONMYSQLVERSIONMAJOR5MYSQLVERSIONMINOR7MYSQLVERSIONPATCH31MYSQLVERSIONEXTRA搜索一些关键信息shellgreprinitializeACLgranttimemysqltestrgrantdebug。result:PatternFatalerror:FailedtoinitializeACLgranttimezonesstructuresorfailedtoremovetemporarytablefiles。foundmysqltesttgrantdebug。test:letSEARCHPATTERNFatalerror:FailedtoinitializeACLgranttimezonesstructuresorfailedtoremovetemporarytablefiles。;sqlmysqld。cc:sqlprinterror(Fatalerror:FailedtoinitializeACLgranttimezones 搜索得出的结果很多,我们需要对此进行过滤。mysqltest目录是mysql的测试用例代码,我们可以直接忽略;需要关注的是sqlmysqld。cc的文件,因为mysqld启动时调用mainmysqldmain。。。,而mysqldmain函数是在sqlmysqld。cc目录下,此时我们查看具体的代码(sqlmysqld。cc:4958):if(mysqlrmtmptables()aclinit(optnoacl)mytzinit((THD)0,defaulttzname,optbootstrap)grantinit(optnoacl)){sqlprinterror(Fatalerror:FailedtoinitializeACLgranttimezonesstructuresorfailedtoremovetemporarytablefiles。);deletepidfile(MYF(MYWME));uniregabort(MYSQLDABORTEXIT);} 定位到相关代码,大概是sqlmysqld。cc的4958行,且存在if条件判断,此时我们开始调试:shellgdbusrlocalmysqlbinmysqld(gdb)bsqlmysqld。cc:4958Breakpoint1at0xe6f8d0:fileoptmysqlserversqlmysqld。cc,line4958。(gdb)rdefaultsfileetcmysqlmy。cnf(gdb)pmysqlrmtmptables()1000(gdb)paclinit(optnoacl)2101(gdb)pmytzinit((THD)0,defaulttzname,optbootstrap)3000(gdb)pgrantinit(optnoacl)4false 通过以上调试信息,可以判断出aclinit函数返回的值为真;此时我们查看该函数的代码(sqlauthsqlauthcache。cc:1365):Initializestructuresresponsibleforuserdblevelprivilegecheckingandloadprivilegeinformationforthemfromtablesinthemysqldatabase。SYNOPSISaclinit()dontreadacltablesTRUEifwewanttoskiploadingdatafromprivilegetablesanddisableprivilegechecking。NOTESThisfunctionismostlyresponsibleforpreparatorysteps,mainworkoninitializationandgrantsloadingisdoneinaclreload()。RETURNVALUES0ok1Couldnotinitializegrantsmyboolaclinit(booldontreadacltables){THDDBUGENTER(aclinit);。。。 根据该函数的注释发现:该函数是初始化负责用户数据库级特权检查的结构,并从mysqlschema中的表中为其加载特权信息;且return值为1代表的是初始化权限失败。 此后开始逐步调试,观察return相关信息,当调试到locktablenames函数时,我们发现在Phase3时return值为true,且根据代码注释发现true代表是F具体代码如下(sqlsqlbase。cc:5549):Acquirestrong(SRO,SNW,SNRW)metadatalocksontablesusedbyLOCKTABLESorbyaDDLstatement。AcquirelockSontablebeingcreatedinCREATETABLEstatement。noteUnderLOCKTABLES,wecanttakenewlocks,souseopentablescheckupgradablemdl()instead。paramthdThreadcontext。paramtablesstartStartoflistoftablesonwhichlocksshouldbeacquired。paramtablesendEndoflistoftables。paramlockwaittimeoutSecondstowaitbeforetimeout。paramflagsBitmapofflagstomodifyhowthetableswillbeopen,seeopentable()descriptionfordetails。retvalfalseSuccess。retvaltrueFailure(e。g。connectionwaskilled)boollocktablenames(THDthd,TABLELISTtablesstart,TABLELISTtablesend,ulonglockwaittimeout,uintflags){MDLTABLELISTMDLHashsetTABLELIST,schemasetgetkeyschemaset(PSIINSTRUMENTME);。。。Phase3:Acquirethelockswhichhavebeenrequestedsofar。if(thdmdlcontext。acquirelocks(mdlrequests,lockwaittimeout))Nowwhenwehaveprotectionagainstconcurrentchangeofreadonlyoptionwecansafelyrecheckitsvalue。SkipthecheckforFLUSHTABLES。。。WITHREADLOCKandFLUSHTABLES。。。FOREXPORTastheyarenotsupposedtobeaffectedbyreadonlymodes。if(needglobalreadlockprotection!(flagsMYSQLOPENSKIPSCOPEDMDLLOCK)!(flagsMYSQLLOCKIGNOREGLOBALREADONLY)checkreadonly(thd,true))。。。 调试信息如下:(gdb)pflags70(gdb)pneedglobalreadlockprotection8true(gdb)pcheckreadonly(thd,true)9true 可以看到flags的值为0,而MYSQLOPENSKIPSCOPEDMDLLOCK为宏定义值0x1000,与flags的值做按位与操作,结果自然也是0,当然MYSQLLOCKIGNOREGLOBALREADONLY也是如此;needglobalreadlockprotection是bool类型值,代表是否需要全局读锁的保护,这个值是在tablemdlrequest。type不为MDLSHAREDREADONLY发生改变;checkreadonly函数相关信息下面概述。 此时也查看了下MySQL5。7。26版本代码作为对比,发现locktablenames函数下的Phase3后的部分代码是在5。7。29版本后新增的。如果是gitclone的MySQL代码可以用gitblame命令查询文件变化的信息:shellgitblameL5637,10sqlsqlbase。cc05824063(NishaGopalakrishnan2019091113:06:4005305637)Nowwhenwehaveprotectionagainstconcurrentchangeofreadonly05824063(NishaGopalakrishnan2019091113:06:4005305638)optionwecansafelyrecheckitsvalue。0405ebee(NishaGopalakrishnan2019091219:24:4505305639)SkipthecheckforFLUSHTABLES。。。WITHREADLOCKand0405ebee(NishaGopalakrishnan2019091219:24:4505305640)FLUSHTABLES。。。FOREXPORTastheyarenotsupposedtobeaffected0405ebee(NishaGopalakrishnan2019091219:24:4505305641)byreadonlymodes。05824063(NishaGopalakrishnan2019091113:06:4005305642)05824063(NishaGopalakrishnan2019091113:06:4005305643)if(needglobalreadlockprotection0405ebee(NishaGopalakrishnan2019091219:24:4505305644)!(flagsMYSQLOPENSKIPSCOPEDMDLLOCK)05824063(NishaGopalakrishnan2019091113:06:4005305645)!(flagsMYSQLLOCKIGNOREGLOBALREADONLY)05824063(NishaGopalakrishnan2019091113:06:4005305646)checkreadonly(thd,true)) 上述展示的信息中,最左侧的列值为commitid为05824063和0405ebee,有兴趣的同学可以详细看下。 此功能解决的问题是BUG28438114:SETREADONLY1SOMETIMESDOESNTBLOCKCONCURRENTDDL。;当然这个代码的变更功能也在5。7ReleaseNotes中有所体现,如下所示(https:dev。mysql。comdocrelnotesmysql5。7ennews5729。html):Undercertainconditions,enablingthereadonlyorsuperreadonlysystemvariabledidnotblockconcurrentDDLstatementsexecutedbyuserswithouttheSUPERprivilege。(Bug28438114,Bug91852) 最后我们再查看下checkreadonly函数,该函数是基于readonly和superreadonly状态执行标准化检查,是禁止(TRUE)还是允许(FALSE)操作。代码如下(sqlauthsqlauthorization。cc:489):briefPerformsstandardizedcheckwhethertoprohibit(TRUE)orallow(FALSE)operationsbasedonreadonlyandsuperreadonlystate。paramthdThreadhandlerparamerrifreadonlyBooleanindicatingwhetherornottoaddtheerrortothethreadcontextifreadonlyisviolated。returnsStatuscoderetvalTRUETheoperationshouldbeprohibited。retvalFALSETheoperationshouldbeallowed。boolcheckreadonly(THDthd,boolerrifreadonly){DBUGENTER(checkreadonly);readonlyOFF,donotprohibitoperation:if(!optreadonly)DBUGRETURN(FALSE);。。。 此时第一反应就是去检查my。cnf中是否包含readonly相关参数,检查之后发现确实是使用了该参数,如下:〔mysqld〕readonly1 此时注释掉该参数,然后再次启动mysqld,发现MyISAM表可以自动修复,且正常启动;errorlog信息如下:。。。20200818T11:52:26。77555308:000〔ERROR〕usrlocalmysqlbinmysqld:Table。mysqluserismarkedascrashedandshouldberepaired20200818T11:52:26。77621708:000〔Warning〕Checkingtable:。mysqluser20200818T11:52:26。77627308:000〔ERROR〕1clientisusingorhasntclosedthetableproperly20200818T11:52:26。88253708:000〔Note〕Failedtostartslavethreadsforchannel20200818T11:52:26。90601808:000〔Note〕EventScheduler:Loaded0events20200818T11:52:26。90648008:000〔Note〕usrlocalmysqlbinmysqld:readyforconnections。Version:5。7。31debuglogsocket:varlibmysqlsockmysql。sockport:3306Sourcedistribution 由于docker一些限制,我们在mysqld启动会涉及两次;所以解决该问题的方式为:第一次mysqld的启动时先关闭readonly参数,第二次启动时开启readonly参数。之所以选择默认开启readonly参数,是为了避免在mysqld启动后,选主逻辑未完成时的保护措施;当然选主完成后,会自动对master执行setglobalreadonly0操作。 五、总结MySQL小版本的升级也会有变化,一定要做好升级前的度测试工作。MySQL源代码量很多,想要全部了解也很困难;此时我们通过对比不同版本之间的现象差异分别进行调试,找出差异点后再深入探索到某些函数,效果事半功倍。 六、附录 调试的栈帧信息如下,有兴趣的小伙伴可以研究下:(gdb)bt0checkreadonly(thd0xcde33f0,errifreadonlytrue)atoptmysqlserversqlauthsqlauthorization。cc:49110x0000000001486262inlocktablenames(thd0xcde33f0,tablesstart0xcdc9100,tablesend0x0,lockwaittimeout31536000,flags0)atoptmysqlserversqlsqlbase。cc:564620x0000000001484be5inOpentablecontext::recoverfromfailedopen(this0x7fffffffcad0)atoptmysqlserversqlsqlbase。cc:478930x0000000001486758inopentables(thd0xcde33f0,start0x7fffffffcb90,counter0x7fffffffcbd4,flags2048,prelockingstrategy0x7fffffffcc10)atoptmysqlserversqlsqlbase。cc:589140x0000000001487851inopenandlocktables(thd0xcde33f0,tables0x7fffffffccd0,flags2048,prelockingstrategy0x7fffffffcc10)atoptmysqlserversqlsqlbase。cc:658350x0000000000ea2611inopenandlocktables(thd0xcde33f0,tables0x7fffffffccd0,flags2048)atoptmysqlserversqlsqlbase。h:48660x0000000000e9c2cfinaclreload(thd0xcde33f0)atoptmysqlserversqlauthsqlauthcache。cc:209170x0000000000e9a2fdinaclinit(dontreadacltablesfalse)atoptmysqlserversqlauthsqlauthcache。cc:142980x0000000000e6f8f2inmysqldmain(argc136,argv0x2c136a8)atoptmysqlserversqlmysqld。cc:495890x0000000000e66cddinmain(argc2,argv0x7fffffffe458)atoptmysqlserversqlmain。cc:32 熟悉MySQL体系结构和innodb存储引擎工作原理;以及MySQL备份恢复、复制、数据迁移等技术;专注于MySQL、MariaDB开源数据库,喜好开源技术。 原文链接:https:www。heapdump。cnarticles