作者:张洛丹 原爱可生DBA团队成员,现陆金所DBA团队成员,对技术执著有追求! 本文来源:原创投稿 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。背景 某天晚上,数据库hang住,现象是:应用报错org。apache。commons。dbcp。SQLNestedException:Cannotgetaconnection,poolerrorTimeoutwaitingforidleobject无法登录,输入登录命令就卡着不动,无法响应 无奈之下通过强制kill掉进程,重启数据库恢复。 这里暂且不说hang住的原因,仅分析数据库hang住,但是MHA未触发切换。结论 先说下结论,MHA默认使用长连接对数据库做ping健康检测(执行select1asValue),4次无法连接MySQL则触发切换。前面数据库hang住只是新的连接无法建立,但是老连接却没有影响,且MHA的健康检测语句很简单,只在server层进行了检测,不涉及到InnoDB层,所以MHA认为MySQL是健康的,并没有作出任何决策。解决 MHA从0。53版本开始支持pingtype参数设置如何检查master的可用性。支持3个value:select:使用长连接连接到MySQL执行select1asValue,这个长连接被重复使用,但检查过于简单,无法发现更多故障。connect:在每次执行select1asValue前后创建和断开连接,可以发现更多TCP连接级别的故障。 注意:此种情况,MHA监控进程会fork出一个子进程进行检测insert:基于一个到MySQL已经存在的连接执行insert语句,可以更好检测到数据库因磁盘空间耗尽或磁盘IO资源耗尽导致的故障。 通过将pingtype修改设置为connect,MHA每次进程状态检测,需要新建连接,新链接无法成功建立,就触发了切换。 三种检测机制代码:如果获取分布式锁失败返回2,正常返回0,异常返回1subpingconnect(){mylogself{logger};myrc1;mymaxretries2;eval{mypingstart〔gettimeofday〕;连接maxretries次,连接失败则退出while(!self{dbh}maxretries){eval{rcselfconnect(1,self{interval},0,0,1);};if(!self{dbh}){dieif(!maxretries);}}调用pingselectrcselfpingselect();Toholdadvisorylockforsomeperiodsoftimeselfsleepuntil(pingstart,self{interval}1。5);selfdisconnectif();};if(){mymsgGoterroronMySQLconnectping:;msg。DBI::errif(DBI::err);msg。(DBI::errstr)if(DBI::errstr);logwarning(msg)if(log);rc1;}return2if(self{alreadymonitored});}正常返回0,异常返回1subpingselect(){mylogself{logger};mydbhself{dbh};my(query,sth,href);eval{dbh{RaiseError}1;sthdbhprepare(SELECT1AsValue);sthexecute();if(!defined(href)!defined(href{Value})href{Value}!1){}};if(){mymsgGoterroronMySQLselectping:;msg。DBI::errif(DBI::err);msg。(DBI::errstr)if(DBI::errstr);logwarning(msg)if(log);return1;}return0;}正常返回0,异常返回1subpinginsert(){mylogself{logger};mydbhself{dbh};my(query,sth,href);eval{dbh{RaiseError}1;dbhdo(CREATEDATABASEIFNOTEXISTSinfra);dbhdo(CREATETABLEIFNOTEXISTSinfra。chkmasterha(keytinyintNOTNULLprimarykey,valint(10)unsignedNOTNULLDEFAULT0));dbhdo(INSERTINTOinfra。chkmasterhavalues(1,unixtimestamp())ONDUPLICATEKEYUPDATEvalunixtimestamp());};if(){mymsgGoterroronMySQLinsertping:;msg。DBI::errif(DBI::err);msg。(DBI::errstr)if(DBI::errstr);logwarning(msg)if(log);return1;}return0;}测试 MHA配置文件〔serverdefault〕managerlogDatamhalogworkdirmy3306tst。logmanagerworkdirDatamhaworkdirmy3306tstremoteworkdirDatamysqlmy3306mhamasterbinlogdirDatamysqlmy3306logpasswordxxxpinginterval5replpasswordxxxrepluserxxxsshusermysqlsshportxxxusermhamasteriponlinechangescriptusrlocalbinmasteriponlinechangemasteripfailoverscriptmasteripfailover〔server1〕hostnamexxxport3306candidatemaster1〔server2〕hostnamexxxport3306candidatemaster1 注意:在测试的时候将pinginterval设置成5,便于快速观测到切换,实际生产中,可根据业务对故障的容忍能力进行调整。 模拟服务器CPU满负载,数据库无法建立新连接编写一个简单的c程序,如下:includestdio。hintmain(){while(1);return0;} 编译:gccoouttestcpu。c 执行:forininseq1(catproccpuinfogrepphysicalidwcl);do。outdone 另外再跑两个mysqlslap压测程序:mysqlslapc30000i100detach1queryselect1uxxxpxxxSxxxxxxx。sockpingtypeconnect时,4次连接失败触发切换此时,在MHA切换日志中可以看到连接数据库报错的输出如下:GoterroronMySQLconnect:2013(LostconnectiontoMySQLserveratwaitingforinitialcommunicationpacket,systemerror:110)pingtypeselect时,未触发切换 有兴趣的同学可自行测试一下MHA健康检测机制 调用链路:MasterMonitor。pmMHA::MasterMonitor::main()MasterMonitor。pmMHA::MasterMonitor::waituntilmasterisdead()MasterMonitor。pmMHA::MasterMonitor::waituntilmasterisunreachable()MHA::HealthCheck::waituntilunreachable();HealthCheck。pmMHA::HealthCheck::pingselect(或者)HealthCheck。pmMHA::HealthCheck::pinginsert(或者)HealthCheck。pmMHA::HealthCheck::pingconnect(或者) MHA监控进程启动后,会持续监控主节点的状态,主要的健康检测函数是waituntilunreachable()。 PS:MHA监控进程启动过程中,会读取配置文件,对配置文件中的服务器进行一系列检查,包括存活状态、版本信息、从库配置(readonly,relaylogpurge,logbin,复制过滤等),ssh状态等,若检查不通过,则无法启动 在这个函数中会有一个死循环,持续地进行健康检测 1。首先,测试连接,连接正确返回0,否则返回1。如果连接MySQL成功,则获取分布式锁,如果获取分布式锁失败,返回状态值为1如果连接MySQL失败,则返回状态值1和连接失败的报错,对于连接失败的下面几种情况(常见的有1040连接数满和1045权限拒绝)MHA会认为MySQL进程是正常的,并不会触发切换,而是一直进行连接检测ourALIVEERRORCODES(1040,ERCONCOUNTERROR1042,ERBADHOSTERROR1043,ERHANDSHAKEERROR1044,ERDBACCESSDENIEDERROR1045,ERACCESSDENIEDERROR1129,ERHOSTISBLOCKED1130,ERHOSTNOTPRIVILEGED1203,ERTOOMANYUSERCONNECTIONS1226,ERUSERLIMITREACHED1251,ERNOTSUPPORTEDAUTHMODE1275,ERSERVERISINSECUREAUTHMODE); 2。测试连接成功后,则进行健康状态检测(前面说的3种方式);如果连续4次连接失败,则在第4次的时候会使用第二脚本进行检测(如果定义了的话),如果检测通过,则认为master挂掉 关键函数waituntilunreachable()代码:mainfunctionsubwaituntilunreachable(){mylogself{logger};mysshreachable2;myerrorcount0;mymasterisdown0;eval{while(1){self{tstart}〔gettimeofday〕;判断是否需要建立连接if(self{needreconnect}){my(rc,mysqlerr)selfconnect(undef,undef,undef,undef,undef,errorcount);if(rc){if(mysqlerr){错误代码在ALIVEERRORCODES中时,不触发切换,常见的有用户密码不正确,不会切换if(grep(mysqlerr,MHA::ManagerConst::ALIVEERRORCODES)0){loginfo(GotMySQLerrormysqlerr,butthisisnotaMySQLcrash。Continuehealthcheck。。);next直接进入下次循环selfsleepuntil();}}logwarning(Connectionfailederrorcounttime(s)。。);selfhandlefailing();if(errorcount4){sshreachableselfissshreachable();返回1表示主库down,0表示主库没有downmasterisdown1if(selfissecondarydown());主库down则跳出循环lastif(masterisdown);errorcount0;}selfsleepuntil();}connectionokself{needreconnect}0;loginfo(Ping(self{pingtype})succeeded,waitinguntilMySQLdoesntrespond。。);}如果pingtype为connect,则断开连接selfdisconnectif()if(self{pingtype}eqMHA::ManagerConst::PINGTYPECONNECT);Parentprocessforksonechildprocess。ThechildprocessqueriesfromMySQLeveryintervalseconds。Thechildprocessmayhangonexecutingqueries。DBD::mysql4。022orearlierdoesnothaveanoptiontosetreadtimeout,executingqueriesmighttakeforever。Toavoidthis,theparentprocesskillsthechildprocessifitwontexitwithinintervalseconds。eval{调用检测函数if(self{pingtype}eqMHA::ManagerConst::PINGTYPECONNECT){childexitcodeselfforkexec(sub{selfpingconnect()},MySQLPing(self{pingtype}));}elsif(self{pingtype}eqMHA::ManagerConst::PINGTYPESELECT){childexitcodeselfforkexec(sub{selfpingselect()},MySQLPing(self{pingtype}));}elsif(self{pingtype}eqMHA::ManagerConst::PINGTYPEINSERT){childexitcodeselfforkexec(sub{selfpinginsert()},MySQLPing(self{pingtype}));}else{dieNotsupportedpingtype!;}};if(){mymsgUnexpectederrorheppenedwhenpinging!;logerror(msg);childexitcode1;}if(childexitcode0){pingokping成功的话,则更新状态,并将计数器置为0selfupdatestatusok();if(errorcount0){errorcount0;}selfkillseccheck();selfkillsshcheck();}elsif(childexitcode2){self{alreadymonitored}1;}else{创建连接失败self{needreconnect}1;selfhandlefailing();}selfsleepuntil();}logwarning(Masterisnotreachablefromhealthchecker!);};if(){mymsgGoterrorwhenmonitoringmaster:;logwarning(msg);return2if(self{alreadymonitored});return1;}return1unless(masterisdown);return(0,sshreachable);}1;