作者:張洛丹
原愛可生 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,異常返回1
sub 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,異常返回1
sub 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,異常返回1
sub 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.log
manager_workdir=/Data/mha/workdir/my3306tst
remote_workdir=/Data/mysql/my3306/mha
master_binlog_dir=/Data/mysql/my3306/log
password=xxx
ping_interval=5
repl_password=xxx
repl_user=xxx
ssh_user=mysql
ssh_port=xxx
user=mha
master_ip_online_change_script="/usr/local/bin/master_ip_online_change"
master_ip_failover_script="master_ip_failover"
[server1]
hostname=xxx
port=3306
candidate_master=1
[server2]
hostname=xxx
port=3306
candidate_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 function
sub 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;