LVS+keepalived の設定 4

備忘録として、DSRによるLVSkeepalivedの設定を段階的に書く。

前提は以下とする。

  • LVSの転送方式はDSR
  • ネットワークは単一セグメントで超簡単なもの
  • OSはCentOS6.5


今回はMySQLスレーブの死活監視+管理について。システム構成はLVS+keepalivedの設定 3を参照。

TCPポートの監視:TCP_CHECK

もっとも簡単な方法。LVS+keepalivedの設定 2で使った。

keepalived.confの一部を示す。

… 略 …

    real_server 192.168.1.201 3306 {
        weight 1
        inhibit_on_failure
    # サーバ監視: TCPポートにアクセスするだけの超簡易的方法
        TCP_CHECK {
            connect_port 3306
            connect_timeout 30
            nb_get_retry 3
            delay_before_retry 3
        }
    }
    real_server 192.168.1.202 3306 {
        weight 1
        inhibit_on_failure
    # サーバ監視: TCPポートにアクセスするだけの超簡易的方法
        TCP_CHECK {
            connect_port 3306
            connect_timeout 30
            nb_get_retry 3
            delay_before_retry 3
        }
    }

この方法の根本的な欠点は、障害検出が間接的ということである。
「ポートにアクセスできない=サービス停止」とは限らない訳で、サーバは稼働しているが一時的に通信ができなくて障害と判断されてしまう可能性がある。しかしもっとマズいのは、すでに過負荷でサービスは停止していたり、max_connection超えで接続できないにも関らず、3306ポートが生きていれば、LVSはサーバの障害発生を検出できないことである。

クライアントはアクセスできない状態なのに、それを認識できないのはバランサとして存在価値がないに等しい。

自前のプログラムで監視:MISC_CHECK

TCP_CHECKはあまりに低機能なので、MISC_CHECKを利用するためのチェックプログラムを自前で実装してみる。

最初にMISC_CHECKを使うためのkeepalived.confの一部を示す。

… 略 …

    real_server 192.168.1.201 3306 {
        weight 1
        inhibit_on_failure
    # サーバ監視: 
           MISC_CHECK {
		# 自前のチェックスクリプト。 Usage: mysql-check.sh host timeout
               misc_path "/usr/local/bin/mysql-check.sh 192.168.1.201 10"
		# チェックスクリプトのタイムアウト
               misc_timeout 15
            }
    }

    real_server 192.168.1.202 3306 {
        weight 1
        inhibit_on_failure
    # サーバ監視: 
           MISC_CHECK {
               misc_path "/usr/local/bin/mysql-check.sh 192.168.1.202 10"
               misc_timeout 15
    }
}

パラメータについて*1


ここでタイムアウトは大事。delay_loopで指定した周期でmisc_pathのプログラムが実行される。もしもそのプログラムの実行がdelay_loopよりもかかると、チェックプロセスがどんどん立ち上がってしまう。

よって、可能ならばチェックプログラムにもタイムアウトを指定し、次の関係が成り立つように値を調整する。

 misc_pathプログラムのタイムアウト < misc_timeout  < delay_loop

以下、順に説明する。

mysql-check.sh

以下のプログラムをLVS1とLVS2の/usr/local/bin以下に作成する。

cat /usr/local/bin/mysql-check.sh
#!/bin/bash
#
# mysql server check script
#
# Usage: mysql-check.sh host timeout

MYSQL=/usr/bin/mysql
USER=root
CONF=/root/.my.cnf
HOST=localhost
TIMEOUT=10

if [ $# -ne 2 ]; then
    exit -1
else
    HOST=$1;
    TIMEOUT=$2
fi

$MYSQL --defaults-extra-file=$CONF -h $HOST -u $USER --connect-timeout=$TIMEOUT -e 'SELECT 100' 2>&1 > /dev/null

if [ $? -eq 0 ]; then
    # check:OK
    exit 0
else
    # check:NG
    exit 1
fi


mysqlクライアントでHOSTに接続し、”SELECT 100”を実行する。これにより、少なくともTCPポート監視よりはマシな死活監視が可能である。
通信路がおかしいと接続に時間がかかる場合があるので、--connect-timeoutで明示的にタイムアウトを設定する。パスワードなどは(後述する)/root/.my.cnfに設定する。


作成したらパーミッションを設定する。

# chmod 700 /usr/local/bin/mysql-check.sh
LVS1, LVS2にmysqlパッケージインストール

mysqlクライアント(と後述するmysqladmin)を使うため。


LVS1, LVS2に/root/.my.cnfを準備

スレーブにアクセスするためのパスワードを/root/.my.cnfに設定する。

# cat /root/.my.cnf
[client]
user=root
password=XXXX

[mysqladmin]
user=root
password=XXXX

作成したらパーミッションを変更し、root以外は読み書きできないようにする。

# chmod 600 /root/.my.cnf
Slave側でユーザ登録

LVS1,LVS2からアクセスできるようにユーザを登録する。

mysql> GRANT ALL PRIVILEGES on *.* to root@192.168.1.100 identified by PASWORD(‘XXXX’);
mysql> GRANT ALL PRIVILEGES on *.* to root@192.168.1.101 identified by PASWORD(‘XXXX’);
mysql> FLUSH PRIVILEGES;

これは権限を与え過ぎだが、後で説明する”mysqladmin shutdown”のためにこうしてある。

動作確認

これでうまくいくはず。うまく行かない場合はパーミッション関係を見直すこと。

スクリプトの問題点

ところでこのmysql-check.shは、スレーブとの接続前ならconnect-timeoutでタイムアウトするが、接続後にスレーブがとても重くなるとそのまま”SELECT 100”の実行が待たされる可能性がある。
よって以下のような関係になる場合がある。

 misc_pathプログラムのタイムアウト > misc_timeout

この場合、keepalivedは接続失敗とみなすが実はスレーブは稼働しているわけで、とにかく障害検出はとてもとても難しい。


これはレアケースだから考慮する必要がないかといえば、そんなことはない。
本当にサーバが不調になった発端を検出したのかもしれないし、単に一時的な過負荷かもしれないし、ネットワーク機器の不調やリコンフィグレーションによってレスポンスが遅れているだけかもしれない。


大抵のシステムでは、何回かリトライしてN回連続失敗したら障害発生とみなすようになっている。実用上、数回ダメならそのノードは切り離した方がいいし、障害発生と見なすのは合理的だろう。


しかし、「keepalivedのMISC_CHECKではretry指定ができないから、上記レアケースを受け入れるしか無い」というのは、かなり不満が残る。
せいぜいmisc_timeoutとloop_delayの値を大きく取って、障害検出ミスを避ける=レアケースがほとんど起きる可能性がないような設定を選ぶしかない。

課題

上の問題に限らず、障害検出はとても難しい(非同期分散システムにおいて完全な障害検出は原理的に不可能)。
でもそこは1億歩譲って、上記の障害検出スクリプトが1(異常値)を返し、”何かおかしなこと”が起きたとしよう。


実は、障害検出後の処理も難しい。”何かおかしなこと”が起きた結果としてmysqlクライアントが"SELECT 100"を時間内に実行できなかったわけだが、その原因である”何かおかしなこと”はそれこそ無数に考えられるし、その原因の結果が"SELECT 100"の遅れだけとは限らないから、どんな対処をすべきかなんて、とてもじゃないが網羅できるはずがないのだ。

MySQLサーバはもちろん玩具みたいに脆いが、同様にHDDをはじめとするサーバのハードウエア、スイッチやケーブルなどのネットワーク機器もちょっとした不調は起こり得る。そもそも不調ではなく、ネットワーク管理者が他のノードの設定中に誤って通信不可になってしまうとか、設定をリセットするために軽い気持ちでスイッチやルータの電源をOFF/ONするとか、keepalivedMySQLにとっての異常事態はわりと簡単に起きる。このようなちょっとしたことが原因でどんな結果になってしまうのか、予め全てを想定するのは不可能。


ただ一つ確実なのは、なんらかの障害が検出されたということは、システムのどこかが正常でない挙動を示したということで、とにかく”何かおかしなことが少なくとも一度は起きた”のである。
すると、マスターとスレーブのデータの不一致が起きてしまった可能性も否定はできない。特にレプリケーション*2はちょっとしたことがキッカケで不調になるので油断ならない。

フェールセーフ

では、サーバの状態が障害検出されたりまた正常に戻ったりしたらどうだろう? その都度、LVSがサーバを切り離したり繋いだりしては困る。障害検出が完全でない以上、例え障害後に正常と判断しても、マスターとスレーブでデータの不一致等、不具合が発生している可能性は否定できない。

ということで、障害検出だけでは冗長化したDBクラスタを護れない。障害検出後の処理もとても重要だ。


DBを管理するLVSは「障害検出したスレーブを積極的に停止させる=安全側に倒した構成(フェールセーフ)」にすべきだろう。もちろん、過度に安全側に設定を倒しすぎると、障害に対してあまりにセンシティブになって、ちょっとしたことでサービス全停止なんて事態に行き着くのであるが。


難しいことはさておき、とりあえず障害検出したら”mysqladmin shutdown”を実行する設定とスクリプトの例を以下に示す。

shutdownスクリプト

以下のスクリプトを作成する。

cat /usr/local/bin/mysql-shutdown.sh
#!/bin/bash
#
# mysql server shutdown script
#
# Usage: mysql-shutdown.sh host

MYSQLADMIN=/usr/bin/mysqladmin
USER=root
CONF=/root/.my.cnf
HOST=localhost

if [ $# -ne 1 ]; then
    exit -1
else
    HOST=$1;
fi

$MYSQLADMIN --defaults-extra-file=$CONF -h $HOST -u $USER shutdown

exit 0


作成したらパーミッションを設定する。

# chown root:root /usr/local/bin/mysql-shutdown.sh
# chmod 700 /usr/local/bin/mysql-shutdown.sh
keepalived.confの設定

real_serverにnotify_downの設定を追加する。


… 略 …

    real_server 192.168.1.201 3306 {
        weight 1
        inhibit_on_failure
    # サーバ監視: 
           MISC_CHECK {
		# 自前のチェックスクリプト。 Usage: mysql-check.sh host timeout
               misc_path "/usr/local/bin/mysql-check.sh 192.168.1.201 10"
		# チェックスクリプトのタイムアウト
               misc_timeout 15
    	    }
    # 実サーバの障害を検出したら、mysql-shutdown.shを実行。スレーブを積極的にshutdownする
	notify_down “/usr/local/bin/mysql-shutdown.sh 192.168.1.201”    
   }

    real_server 192.168.1.202 3306 {
        weight 1
        inhibit_on_failure
    # サーバ監視: 
           MISC_CHECK {
               misc_path "/usr/local/bin/mysql-check.sh 192.168.1.202 10"
               misc_timeout 15
          }
	notify_down “/usr/local/bin/mysql-shutdown.sh 192.168.1.202”  

       }
動作確認

これで障害検出したらスレーブサーバを強制的に(mysqladminで)shutdownする。

またまた課題

そもそもの話だがmysqlクライアントで接続できないサーバに、mysqladminで接続できるのかという根本的な問題がある。


より確実にサーバを停止させるには、sshで”/etc/init.d/mysqld stop”を実行するとか、”kill -9 mysqld.pid”を実行するほうがましだろう。それでも確実ではないが。
しかし、スクリプトsshを実行させるにはパスワード入力をさけるためにssh-agentを動かさねばならない等、システム全体のセキュリティがどんどん低下してしまう。


しかも、それでもダメな場合だってある。LVS1とLVS2の障害が重なるイヤらしいシナリオとして「LVS1がスレーブ1の障害を検出した直後にダウンしたのでスレーブ1を停止できなかった。その後LVS2がマスターに昇格したときには、なぜかスレーブ1も動作していたが、実はデータがおかしくなっていた」とか。悪い状況はいくらでも想定できる*3


どこまでやるかは仕様次第というか、決めの問題。確実さを求めるほどどんどん手間が増えるし、どこまでいっても終わりはない。

参考URL

*1:今回はmisc_dynamicは無視する。misc_dynamicを有効にすると、チェックプログラムの2以上の返り値の場合、その値から2を引いた値を重みにする。例えば5を返したら3が重みになる。

*2:特にバイナリーログなんていうとんでもないものを基盤としているMySQLのレブリケーションの場合は。

*3:そして本当にマズいタイミングでそれは発生する。