龙空技术网

故障分析 | 数据库故障 MHA 未切换

爱可生 222

前言:

目前小伙伴们对“mysql1251错误如何解决”大致比较珍视,大家都想要分析一些“mysql1251错误如何解决”的相关文章。那么小编同时在网上汇集了一些关于“mysql1251错误如何解决””的相关文章,希望朋友们能喜欢,你们快快来了解一下吧!

作者:张洛丹

原爱可生 DBA 团队成员,现陆金所 DBA 团队成员,对技术执著有追求!

本文来源:原创投稿

*爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

背景

某天晚上,数据库 hang 住,现象是:

应用报错org.apache.commons.dbcp.SQLNestedException: Cannot get a connection,pool error Timeout waiting for idle object无法登录,输入登录命令就卡着不动,无法响应

无奈之下通过强制 kill 掉进程,重启数据库恢复。

这里暂且不说 hang 住的原因,仅分析数据库 hang 住,但是 MHA 未触发切换。

结论

先说下结论,MHA 默认使用长连接对数据库做 ping 健康检测(执行select 1 as Value),4次无法连接 MySQL 则触发切换。 前面数据库 hang 住只是新的连接无法建立,但是老连接却没有影响,且 MHA 的健康检测语句很简单,只在 server 层进行了检测,不涉及到 InnoDB 层,所以 MHA 认为 MySQL 是健康的,并没有作出任何决策。

解决

MHA 从 0.53 版本开始支持 ping_type 参数设置如何检查 master 的可用性。支持3个 value :

select:使用长连接连接到 MySQL 执行select 1 as Value,这个长连接被重复使用,但检查过于简单,无法发现更多故障。connect:在每次执行select 1 as Value前后创建和断开连接,可以发现更多 TCP 连接级别的故障。

注意:此种情况,MHA 监控进程会 fork 出一个子进程进行检测

insert:基于一个到 MySQL 已经存在的连接执行 insert 语句,可以更好检测到数据库因磁盘空间耗尽或磁盘 IO 资源耗尽导致的故障。

通过将 ping_type 修改设置为connect,MHA 每次进程状态检测,需要新建连接,新链接无法成功建立,就触发了切换。

三种检测机制代码:

##如果获取分布式锁失败返回2,正常返回0,异常返回1sub ping_connect($) {  my $self = shift;  my $log  = $self->{logger};  my $dbh;  my $rc          = 1;  my $max_retries = 2;  eval {    my $ping_start = [gettimeofday];    # 连接max_retries次,连接失败则退出    while ( !$self->{dbh} && $max_retries-- ) {      eval { $rc = $self->connect( 1, $self->{interval}, 0, 0, 1 ); };      if ( !$self->{dbh} && $@ ) {        die $@ if ( !$max_retries );      }    }    # 调用ping_select    $rc = $self->ping_select();    # To hold advisory lock for some periods of time    $self->sleep_until( $ping_start, $self->{interval} - 1.5 );    $self->disconnect_if();  };  if ($@) {    my $msg = "Got error on MySQL connect ping: $@";    undef $@;    $msg .= $DBI::err if ($DBI::err);    $msg .= " ($DBI::errstr)" if ($DBI::errstr);    $log->warning($msg) if ($log);    $rc = 1;  }  return 2 if ( $self->{_already_monitored} );  return $rc;}# 正常返回0,异常返回1sub ping_select($) {  my $self = shift;  my $log  = $self->{logger};  my $dbh  = $self->{dbh};  my ( $query, $sth, $href );  eval {    $dbh->{RaiseError} = 1;    $sth = $dbh->prepare("SELECT 1 As Value");    $sth->execute();    $href = $sth->fetchrow_hashref;    if ( !defined($href)      || !defined( $href->{Value} )      || $href->{Value} != 1 )    {      die;    }  };  if ($@) {    my $msg = "Got error on MySQL select ping: ";    undef $@;    $msg .= $DBI::err if ($DBI::err);    $msg .= " ($DBI::errstr)" if ($DBI::errstr);    $log->warning($msg) if ($log);    return 1;  }  return 0;}# 正常返回0,异常返回1sub ping_insert($) {  my $self = shift;  my $log  = $self->{logger};  my $dbh  = $self->{dbh};  my ( $query, $sth, $href );  eval {    $dbh->{RaiseError} = 1;    $dbh->do("CREATE DATABASE IF NOT EXISTS infra");    $dbh->do("CREATE TABLE IF NOT EXISTS infra.chk_masterha (`key` tinyint NOT NULL primary key,`val` int(10) unsigned NOT NULL DEFAULT '0')"    );    $dbh->do("INSERT INTO infra.chk_masterha values (1,unix_timestamp()) ON DUPLICATE KEY UPDATE val=unix_timestamp()"    );  };  if ($@) {    my $msg = "Got error on MySQL insert ping: ";    undef $@;    $msg .= $DBI::err if ($DBI::err);    $msg .= " ($DBI::errstr)" if ($DBI::errstr);    $log->warning($msg) if ($log);    return 1;  }  return 0;}
测试

MHA 配置文件

[server default]manager_log=/Data/mha/log/workdir/my3306tst.logmanager_workdir=/Data/mha/workdir/my3306tstremote_workdir=/Data/mysql/my3306/mhamaster_binlog_dir=/Data/mysql/my3306/logpassword=xxxping_interval=5repl_password=xxxrepl_user=xxxssh_user=mysqlssh_port=xxxuser=mhamaster_ip_online_change_script="/usr/local/bin/master_ip_online_change"master_ip_failover_script="master_ip_failover"[server1]hostname=xxxport=3306candidate_master=1[server2]hostname=xxxport=3306candidate_master=1

注意:在测试的时候将ping_interval设置成5,便于快速观测到切换,实际生产中,可根据业务对故障的容忍能力进行调整。

模拟服务器CPU满负载,数据库无法建立新连接 编写一个简单的c程序,如下:

# include <stdio.h>int main(){	while(1);	return 0;}

编译:

gcc -o out test_cpu.c

执行:

for in in `seq 1 $(cat /proc/cpuinfo | grep "physical id" | wc -l)`; do ./out & done

另外再跑两个 mysqlslap 压测程序:

mysqlslap -c 30000 -i 100 --detach=1 --query="select 1 from dual" --delimiter=";" -uxxx -pxxx -S /xxxx/xxx.sock
ping_type=connect 时,4次连接失败触发切换 此时,在 MHA 切换日志中可以看到连接数据库报错的输出如下:
Got error on MySQL connect: 2013 (Lost connection to MySQL server at 'waiting for initial communication packet',system error: 110)
ping_type=select时,未触发切换

有兴趣的同学可自行测试一下

MHA健康检测机制

调用链路:

MasterMonitor.pm|MHA::MasterMonitor::main() -->MasterMonitor.pm|MHA::MasterMonitor::wait_until_master_is_dead() -->MasterMonitor.pm|MHA::MasterMonitor::wait_until_master_is_unreachable() --> MHA::HealthCheck::wait_until_unreachable();-->HealthCheck.pm|MHA::HealthCheck::ping_select(或者)HealthCheck.pm|MHA::HealthCheck::ping_insert(或者)HealthCheck.pm|MHA::HealthCheck::ping_connect(或者)

MHA 监控进程启动后,会持续监控主节点的状态,主要的健康检测函数是 wait_until_unreachable()。

PS:MHA 监控进程启动过程中,会读取配置文件,对配置文件中的服务器进行一系列检查,包括存活状态、版本信息、从库配置(read_only,relay_log_purge,log-bin,复制过滤等),ssh状态等,若检查不通过,则无法启动

在这个函数中会有一个死循环,持续地进行健康检测

1.首先,测试连接,连接正确返回0,否则返回1。

如果连接 MySQL 成功,则获取分布式锁, 如果获取分布式锁失败,返回状态值为 1如果连接 MySQL 失败,则返回状态值1和连接失败的报错,对于连接失败的下面几种情况(常见的有1040连接数满和1045权限拒绝)MHA 会认为 MySQL 进程是正常的,并不会触发切换,而是一直进行连接检测

our @ALIVE_ERROR_CODES = (  1040,    # ER_CON_COUNT_ERROR  1042,    # ER_BAD_HOST_ERROR  1043,    # ER_HANDSHAKE_ERROR  1044,    # ER_DBACCESS_DENIED_ERROR  1045,    # ER_ACCESS_DENIED_ERROR  1129,    # ER_HOST_IS_BLOCKED  1130,    # ER_HOST_NOT_PRIVILEGED  1203,    # ER_TOO_MANY_USER_CONNECTIONS  1226,    # ER_USER_LIMIT_REACHED  1251,    # ER_NOT_SUPPORTED_AUTH_MODE  1275,    # ER_SERVER_IS_IN_SECURE_AUTH_MODE);

2.测试连接成功后,则进行健康状态检测(前面说的3种方式);如果连续4次连接失败,则在第4次的时候会使用第二脚本进行检测(如果定义了的话),如果检测通过,则认为 master 挂掉

关键函数 wait_until_unreachable()代码:

# main functionsub wait_until_unreachable($) {  my $self           = shift;  my $log            = $self->{logger};  my $ssh_reachable  = 2;  my $error_count    = 0;  my $master_is_down = 0;  eval {    while (1) {      $self->{_tstart} = [gettimeofday];      ## 判断是否需要建立连接      if ( $self->{_need_reconnect} ) {        my ( $rc, $mysql_err ) =          $self->connect( undef, undef, undef, undef, undef, $error_count );        if ($rc) {          if ($mysql_err) {            # 错误代码在ALIVE_ERROR_CODES中时,不触发切换,常见的有用户密码不正确,不会切换            if (              grep ( $_ == $mysql_err, @MHA::ManagerConst::ALIVE_ERROR_CODES )              > 0 )            {              $log->info("Got MySQL error $mysql_err, but this is not a MySQL crash. Continue health check.."              );              # next直接进入下次循环              $self->sleep_until();              next;            }          }          $error_count++;          $log->warning("Connection failed $error_count time(s)..");          $self->handle_failing();          if ( $error_count >= 4 ) {            $ssh_reachable = $self->is_ssh_reachable();            # 返回1表示主库down,0表示主库没有down            $master_is_down = 1 if ( $self->is_secondary_down() );            # 主库down则跳出循环            last if ($master_is_down);            $error_count = 0;          }          $self->sleep_until();          next;        }        # connection ok        $self->{_need_reconnect} = 0;        $log->info("Ping($self->{ping_type}) succeeded, waiting until MySQL doesn't respond.."        );      }      # 如果ping_type为connect,则断开连接      $self->disconnect_if()        if ( $self->{ping_type} eq $MHA::ManagerConst::PING_TYPE_CONNECT );      # Parent process forks one child process. The child process queries      # from MySQL every <interval> seconds. The child process may hang on      # executing queries.      # DBD::mysql 4.022 or earlier does not have an option to set      # read timeout, executing queries might take forever. To avoid this,      # the parent process kills the child process if it won't exit within      # <interval> seconds.      my $child_exit_code;      eval {        # 调用检测函数        if ( $self->{ping_type} eq $MHA::ManagerConst::PING_TYPE_CONNECT ) {          $child_exit_code = $self->fork_exec( sub { $self->ping_connect() },            "MySQL Ping($self->{ping_type})" );        }        elsif ( $self->{ping_type} eq $MHA::ManagerConst::PING_TYPE_SELECT ) {          $child_exit_code = $self->fork_exec( sub { $self->ping_select() },            "MySQL Ping($self->{ping_type})" );        }        elsif ( $self->{ping_type} eq $MHA::ManagerConst::PING_TYPE_INSERT ) {          $child_exit_code = $self->fork_exec( sub { $self->ping_insert() },            "MySQL Ping($self->{ping_type})" );        }        else {          die "Not supported ping_type!\n";        }      };      if ($@) {        my $msg = "Unexpected error heppened when pinging! $@";        $log->error($msg);        undef $@;        $child_exit_code = 1;      }      if ( $child_exit_code == 0 ) {        #ping ok        ## ping成功的话,则更新状态,并将计数器置为0        $self->update_status_ok();        if ( $error_count > 0 ) {          $error_count = 0;        }        $self->kill_sec_check();        $self->kill_ssh_check();      }      elsif ( $child_exit_code == 2 ) {        $self->{_already_monitored} = 1;        croak;      }      else {          ## 创建连接失败        # failed on fork_exec        $error_count++;        $self->{_need_reconnect} = 1;        $self->handle_failing();      }      $self->sleep_until();    }    $log->warning("Master is not reachable from health checker!");  };  if ($@) {    my $msg = "Got error when monitoring master: $@";    $log->warning($msg);    undef $@;    return 2 if ( $self->{_already_monitored} );    return 1;  }  return 1 unless ($master_is_down);  return ( 0, $ssh_reachable );}1;

标签: #mysql1251错误如何解决