Home > まめ知識 | 開発裏話 > MySQL5.5-MySQL5.6+KeepAlivedを使った、2台構成HA型準同期レプリケーションの作り方

MySQL5.5-MySQL5.6+KeepAlivedを使った、2台構成HA型準同期レプリケーションの作り方

MySQLは、レプリケーションという負荷分散にもってこいな機能があります。ただし、負荷分散はできるのですが、サーバーが故障やハングアップなどによるダウンしたときの「無停止化」をするのは、なかなか難しく、MySQLCluster等を使えば無停止化も可能なのですが、サーバー台数が多数必要であることと、join系のクエリに弱い等の問題があります。

今回は、そんなに負荷は高くない(一台で賄える)のだが、停止するのは困る、といったよくあるパターンでのレプリケーションをご紹介します。

当然ですが、使用する場合は、何の保証もしませんので、自己責任でお願いします。また、テストで使ったのはCentOS5.5 64bitです(他のでも問題ないと思います)。

通常のMySQLレプリケーションの問題点1

MySQL5.1までのレプリケーション機能では、マスタが停止した場合、クエリの損失という問題点が発生する場合がありました。次のような感じです。

1. スレーブが、マスタから最新のバイナリログ更新分を取得する。(更新クエリAは入っていない)

2. クライアントがマスタに更新クエリAを投げる。

3. マスタがクエリを実行し、クライアントに更新クエリAの終了を出す。

4. マスタが落ちる。

5. スレーブが再びマスタから最新のバイナリログを要求するが、落ちているので取得できない。

この状態で、スレーブを手動にてマスタに昇格させると、更新クエリAは当然反映されません。クライアントから見ると、更新クエリは受け付けされて実行されているはずにも係わらず、です。これが良く言われている非同期レプリケーションの問題点でした。

これを解決するのが、準同期レプリケーション(MySQL5.5から採用)です。上記の場合、次のような挙動をします。

1. スレーブが、マスタから最新のバイナリログ更新分を取得する。(更新クエリAは入っていない)

2. クライアントがマスタに更新クエリAを投げる。

3. マスタがクエリを実行し、スレーブに更新クエリAを渡しに行って、渡し終わったら、クライアントに更新クエリAの終了を出す。

4. マスタが落ちる。

準同期では、クエリのログをスレーブに渡し終わった後に、クライアントに終了を返すので、もし、この間にマスタが落ち、スレーブにクエリが渡っていなかったとすると、クライアント側からは、クエリが失敗したように見え、整合性がとれるという訳です。

今回は、これを採用し、レプリケーションにてクエリの不整合がほぼ起こらない状態での自動フェイルオーバーを組みます。

通常のMySQLレプリケーションの問題点2

そもそも、MySQLは単体でフェイルオーバーに対応していません。マスタが落ちた場合は、スレーブを手動にてマスタに昇格させるしか手がありません。これは、夜も枕を高くして寝ていたいサーバー管理者にとっては頭痛の種です。今回は、この点をKeepAlivedによってカバーします。

通常のMySQLレプリケーションの問題点3

MySQLをフェイルオーバーさせ、スレーブをマスタに昇格させた際、頭の痛い問題が「マスタの故障がなおった」場合にどうするかです。思いつく手段は、「一旦止める」ですが、うれしくありません。特に、手動にて昇格させた場合は、mysqlのマニュアルのような手(reset masterを実行して、全ロックしてスナップショットを取る)がありますが、自動フェイルオーバーされてしまったら、どうしようもありません・・。今回はこんな問題に足を踏み入れ、毎日のバックアップと連携させて無停止復旧をおこなえるようにします。

概念

こんな感じです。今回の構成では、一台はホットスタンバイ兼バックアップ作成用です。一台で参照、更新のクエリを受け付けます。(keepalivedのconfigを変更して、参照をもう一台に振る形もできますが、そっちはそのうち。今回はシンプルにいきます。)

[サーバA]:実IP 192.168.0.1 仮想IP 192.168.0.10 (MySQLマスタ)
[サーバB]:実IP 192.168.0.2(MySQLスレーブ)

仮想IPの3306ポートで、MySQLとアクセスします。間違っても実IPでアクセスしてはダメです。

サーバAがダウンすると仮想IPがKeepAlivedのVRRPにて自動的に振り変わり、次のようになります。

[サーバA(死亡)]:実IP 192.168.0.1(MySQL停止)
[サーバB]:実IP 192.168.0.2 仮想IP 192.168.0.10 (MySQLマスタ)

フェイルバックを行うと、次のようにサーバAをサーバBのスレーブにします。

[サーバB]:実IP 192.168.0.2 仮想IP 192.168.0.10 (MySQLマスタ)
[サーバA]:実IP 192.168.0.1 (MySQLスレーブ)

実装方法(mysql編)

セットアップは至って単純です。

1. MySQL5.5.8以降のMySQLを入れたサーバーを二台用意する。(サーバAとサーバBとします)

2. mysql_secure_installationとかを終わった後に、mysqlクライアントからmysqlにログイン。

3. 次の2つを走らせて、準同期レプリケーションを使えるようにする。

INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';

4. my.cnfの設定を行う。(*注意点だけ書いておきます。)

# 当然、外部アクセスさせる場合はポート指定。
port = 3306
socket = /var/lib/mysql/mysql.sock
# default-character-set=utf8は、5.5以降はエラーするので注意。
character-set-server = utf8
# よくあるおまじない。(文字化け防止)
skip-character-set-client-handshake

#ログファイルの名前は何でもいいが、今回は説明のためのデフォルト。
log-bin = mysql-bin
relay-log = relay-bin
#お好みで。
slow_query_log
#サーバ毎に一意に振る。今回の初期値は1(サーバA)と2(サーバB)(今回の作戦では、変更かけなければならない場合がある)
server-id = 1
# これが大事。レプリケーションで取得したログをバイナリログにも書き込む。(後述しますが、今回の作戦には必須です)
log_slave_updates
# 勝手にスレーブ起動はいや。
skip-slave-start
# セミシンクロ(準同期)レプリケーションを許可。(マスタはマスタだけ、スレーブはスレーブだけではなく、どっちにもどっちも書く。)
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_slave_enabled = 1

これでmysqlを再起動。

実装方法(KeepAlived編)

こちらも至って簡単です。

1. KeepalivedをサーバAとサーバBに入れる。

2. configを設定した後、keepalivedは止めておく。

keepalived.confは、ちょっとした細工をしてあります。MySQLが停止していたら、KeepAlivedを落とす処理が必要です。(KeepAlived側からMySQLを監視する)

例えば、次のような感じで。(Cronで監視しても問題ないですが、1分間隔はちょっと遅いので)

! Configuration File for keepalived

# フェイルオーバーした際のalertメールを送るための設定(無くても可)
global_defs {
    # メールの送り先
    notification_email {
        test@korehatest.jp
    }
    # メールの送信元アドレスと、メールサーバー
    notification_email_from KeepAlived@korehatest.jp
    smtp_server 127.0.0.1
    smtp_connect_timeout 30
    # サーバAでは、mysqlsvrA、サーバBではmysqlsvrBとかにしておくと、メールが来たときわかりやすい。
    router_id mysqlsvrA
}

#グローバル側
#[vrrp_instance ][好きな名前]
vrrp_instance VI_1000 {
    # マスタサーバー
    state BACKUP
    # VIPを割り当てるNIC
    interface eth0
    #今起動しているのがなければ、マスタになり、マスタがいれば、スレーブ。
    nopreempt
    # ネットワークが混雑していて、スイッチがARP信号を処理できない場合は、ARPを少し送らせて送信する設定。
    # 無くても動く。動作が変な場合(VIPを引き継いだのに、外部から認識できない現象が出たら、こいつを設定。)
    # 弊害として、VIPの引継完了が遅くなる。(デフォルトは引き継いだら即ARP信号の送信。)
    #    garp_master_delay 1
    # 仮想ルーターのID(VRRP信号を共有する他サーバーのインスタンス間で共通にする。)
    virtual_router_id 11
    # 優先度(MasterはBACKUPよりも高く設定。今回は一緒)
    priority 100
    # VRRP信号を送出する間隔
    advert_int 1
    #メール通知
    smtp_alert
    # VRRP信号を受け取る際のパスワード等(同じvirtual_router_idならば、同じにする。)
    authentication {
        auth_type AH
        auth_pass test@test
    }
    # このインスタンスで管理するVIP(同じvirtual_router_idなら、同じにする。※NICは同じ必要ない。)
    virtual_ipaddress {
        192.168.0.10/24 dev eth0
    }
}

#監視のためにつくってある。
#マスタ側
virtual_server 127.0.0.1 3307 {
    #監視の間隔
    delay_loop 5
    #rrラウンドロビン、
    lb_algo rr
    #DR:DSR構成、NAT:なっと
    lb_kind DR
    protocol TCP
    #マスタはこいつだけ。
    # サーバAでは192.168.0.1、サーバBでは192.168.0.2にする。
    real_server 192.168.0.1 3306 {
        weight 10
        TCP_CHECK {
            connect_port 3306
            connect_timeout 30
        }
        #もしマスタmysqlが止まったときに呼び出される処理。keepalivedを落とす。(後述)
        notify_down "/usr/local/keepscript/shutdown_keepalived.sh"
    }
}

スクリプト「shutdown_keepalived.sh」は次のような感じで。(生きているかどうかだけなので、何でチェックしてもかまいません。)

#!/bin/bash
####
#もしマスタが落ちたときの処理スクリプト
#とりあえず最終確認して、本当にダメだったらkeepalivedを落とす。
####
#ちょっと待つ
sleep 5
#本当にmysqlが死んでいるかどうかの確認
mysqlans=`mysql -u hoge -phogehoge -h 192.168.0.1 < /usr/local/keepscript/showmaster.sql | grep Bytes_received `
#もし、本当に死んでいるときは、keepalivedを落とす。
if [ "" = "$mysqlans" ]; then
    /etc/rc.d/init.d/mysql stop
    /etc/rc.d/init.d/keepalived stop
fi

「showmaster.sql」はこれ!(これは酷い・・)

show status;

スクリプト「shutdown_keepalived.sh」は単純に最終確認として、show status;を行って、結果が帰ってこなかったら、mysqlとkeepalivedを停止させるという処理です。

mysqlはとまっているはずですが、ハングアップとかの場合も考えて、こっちも落とす処理をしておきます。

実稼働させるには

まず、レプリケーションを開始させなければなりません。

新規データベースでいくなら、次の感じですね。

サーバAのmysqlにて

GRANT REPLICATION SLAVE ON *.* TO hoge@'192.168.0.%' IDENTIFIED BY 'hogehoge';

サーバBのmysqlにて、

change master to master_host='192.168.0.1',master_user='hoge',master_password='hogehoge',master_log_file='mysql-bin.000001',master_log_pos=107;
start slave;

これでOK。準同期も非同期も同じです。準同期が行われているかどうかを確認するには、マスタ側で

show status like 'Rpl%';

+--------------------------------------------+-------------+
| Variable_name                              | Value       |
+--------------------------------------------+-------------+
| Rpl_semi_sync_master_clients               | 1           |
| Rpl_semi_sync_master_net_avg_wait_time     | 0         |
| Rpl_semi_sync_master_net_wait_time         | 0      |
| Rpl_semi_sync_master_net_waits             | 0         |
| Rpl_semi_sync_master_no_times              | 0           |
| Rpl_semi_sync_master_no_tx                 | 0        |
| Rpl_semi_sync_master_status                | ON          |
| Rpl_semi_sync_master_timefunc_failures     | 0           |
| Rpl_semi_sync_master_tx_avg_wait_time      | 0         |
| Rpl_semi_sync_master_tx_wait_time          | 0       |
| Rpl_semi_sync_master_tx_waits              | 0         |
| Rpl_semi_sync_master_wait_pos_backtraverse | 0           |
| Rpl_semi_sync_master_wait_sessions         | 0           |
| Rpl_semi_sync_master_yes_tx                | 0         |
| Rpl_semi_sync_slave_status                 | OFF         |
| Rpl_status                                 | AUTH_MASTER |
+--------------------------------------------+-------------+

みたいに、「Rpl_semi_sync_master_clients」が1になっていればOKです。

スレーブ側でも、「show slave status\G;」とかで、レプリケーションの確認を行っておいてください。

レプリケーションが始まったら、KeepalivedをサーバAで起動、サーバBで起動の順に起動して、サーバAに仮想IPが振られていることを確認してください。

動作について

ここまでの設定で、フェイルオーバーは自動的に行われるようになっています。次のように動きます。

1. サーバAがクライアントからのクエリを受け付けている。

2. サーバAのMySQLがハングアップ

3. クライアントからのクエリが失敗しはじめる。

4. サーバAのKeepAlivedが監視失敗して、スクリプトを起動。(アラートメールが飛ぶ)→サーバAのmysqlとKeepalivedを停止。

5. サーバBのKeepAlivedがサーバAのKeepAlivedが停止したことを確認し、仮想IPを引き継ぐ。(アラートメールが飛ぶ)

6. サーバBのMySQLが自動的に仮想IPの3306ポートで待ちはじめる。(仮想IPを引き継いだら勝手に行う)

7. クライアントからのクエリがサーバBに行くようになり、クエリが成功するようになる。

だいたい、フェイルオーバー時のダウンタイムが数秒です。これでもかなりいい気がするのですが、このままでは、冗長構成に戻せません。

バックアップ作成について

このシステムの第二の肝となるのが、バックアップです。これをうまく利用することで、安心と信頼のサーバーになります(多分)

バックアップはスレーブ側で行います。(スレーブは、止めても問題ないためです。)

crontab -e
00 04 * * * /usr/local/mysqlbackup/backup_mysql.sh

みたいにスケジュールして、スクリプトは

#!/bin/bash
/etc/rc.d/init.d/keepalived stop
/usr/sbin/tmpwatch -m 50 /usr/local/mysqlbackup/backupdata
svr_name=`uname -n`
/etc/rc.d/init.d/mysql stop
sleep 3
daystr=`date -d '1 days ago' '+%Y%m%d'`
cd /var/lib/mysql
tar czvf /usr/local/mysqlbackup/backupdata/$svr_name$daystr.tgz ./* > /dev/null 2>&1
/etc/rc.d/init.d/mysql start
sleep 3
/etc/rc.d/init.d/keepalived start
exit

みたいな感じで。(自分で使っている物は、これにレプリケーションが止まっていないかや、stop slaveして、クエリが追い付くまで待つ等のチェックを加えています。)

ポイントは特に無いのですが、バイナリログも全部バックアップしているところでしょうか。このため、結構重くなる可能性があるので、別スクリプトか、同じスクリプトで、

バイナリログを最新のひとつだけ残して、後は消してしまうなんていうことも書いておくといいかもしれません。

また、スレーブのHDDがアクセス不能になったときに備えて、バックアップは他のサーバーにコピーしておいた方がいいかもしれないです。

サーバAが停止したときのフェイルバック動作について

今回の肝2となる点です。サーバAはサーバBのスレーブにしたいので、まず、サーバBにて

stop slave;
change master to master_host='192.168.2.1',master_user='dummy',master_password='dummy',master_log_file='mysql-bin.000001',master_log_pos=107;

みたいにして、スレーブ動作を行えないようにしておきます。(二行目は、ダミーアドレス・ダミーユーザーです。本来必要ないのですが、誤って双方向レプリケーションにならないように、ダミー設定しておくことをお薦めします。)

次に、サーバAで、mysqlを立ち上げる前に、

cd /var/lib/mysql
rm mysql-*
rm relay-*
rm master.info
rm ib*

として、すべてのデータを一旦削除します。(MyISAMを使っているときは、データベースフォルダも削除)

そして、毎日行っているバックアップファイルをコピーしてきて(以下はサーバAでの動作)

scp 192.168.0.2:/usr/local/mysqlbackup/backupdata/***20110101.tar.gz ~/
tar xzvf ***20110101.tar.gz

データベースファイルだけを/var/lib/mysql下にコピーします。(innoDBなら、ib*のように、ログファイルも。MyISAMなら、データベースフォルダをすべて。)

次に、どこまで進んでいる分かをmysqlbinlogにて確認します。

# 必ずバイナリログの最終ファイルを見ること(今回はmysql-bin.000005だったと仮定します)
mysqlbinlog mysql-bin.000005

ずらずらと流れるバイナリログのうち、最後の方で、

# at 468263
#110111 12:16:27 server id 60  end_log_pos 468290       Xid = 4267
COMMIT/*!*/;
# at 468290
#110111 14:58:40 server id 162  end_log_pos 468309      Stop
DELIMITER ;
# End of log file
ROLLBACK /* added by mysqlbinlog */;
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;

みたいなDELIMITER行があると思います。この直前の # at が最終ログポジションなので、上記の場合は、468290になります。

最後に、my.cnfのserver-idを変更してmysqlを起動します。

#サーバ毎に一意に振る。今回の初期値は1(サーバA)と2(サーバB)(今回の作戦では、変更かけなければならない場合がある)
#スレーブに移行したときは、サーバーIDを1→11に変更
server-id = 11

サーバーIDを変更するのは、理由があります。(後述。すごい大事。忘れると、レプリケーションエラーする可能性非常にたかい。)

ここまで準備ができたら、サーバAをサーバBのスレーブに設定するだけです。先ほどログを見てメモしたログファイルとポジションを使って、

change master to master_host='192.168.0.2',master_user='hoge',master_password='hogehoge',master_log_file='mysql-bin.000005',master_log_pos=468290;
start slave;

これで、元マスタだったサーバAがスレーブになりました。忘れずにKeepAlivedを起動しましょう。(KeepAlivedの起動は最後の最後です。また、コンフィグの書き換えは不要です。)

サーバBが停止したとき

大抵の場合は、再起動かけて、mysqlを起こして、start slaveするのとkeepalivedを起こしてしまえば復旧します。エラーするようなら、バックアップファイルから、スレーブを作りなおしてください。

サーバーIDを変更する理由

ここにつまづいて、二日ぐらい悩みました・・。

レプリケーションのログや、バイナリログは、サーバーIDというものが書き込まれています。これは、巡回レプリケーション(例えばマルチマスタとか)を組んだ場合に、「レプリケーションで取得してきたログのうち、自分のサーバーIDのものは実行しない」という規則のために存在します。これがない場合、マルチマスタとかで、log_slave_updatesをかけると、ログが永遠に巡回してしまい、一回のinsert命令が何度も実行される、なんていうことになります。

本来、上記の機能は有効な機能であり、問題ないのですが、今回のようなパターンのときには問題が発生します。

たとえば、サーバA(ID:1)がマスタ、サーバB(ID:2)がスレーブを初期状態として、次のような場合です。

1. サーバーB上で、バックアップを取る。(binlogポジション5000番までとする)

2. サーバAがしばらくクエリを受け付けていたが、突然落ちた。(このとき、サーバーB上のbinlogポジションを8000番とする)

3. サーバーBがフェイルオーバーして、クエリを受付始める。

4. サーバーAが復旧できたので、上記の手順で、サーバBのスレーブとして復旧させる。(このとき、サーバーBのbinlogポジションを15000番とする。)

おわかりの方も多いと思うのですが、4でサーバAが復旧したとき、サーバBから取得した、バックアップを使いますので、サーバAは、binlogポジション5000番の状態です。レプリケーションを開始すると、サーバBのbinlogポジションの15000番まで追い付こうとします。ところで、この間のbinlogに記録されているserver-IDは、次のようになります。

5000〜8000 : ID 1
8000〜15000 :ID 2

もし、サーバAのserver-IDを変更せずにスレーブとして復旧させた場合、5000番〜8000番のbinlogは、自分と同じserver-IDなので、mysqlは巡回してきたログであると判断し(つまり、既に実行済みのクエリだと判断し)、読み飛ばします。

このため、レプリケーションが追い付いた時点で、一部のクエリがそっくり抜けた状態になってしまいます。(そもそもレプリケーションが追い付く前にエラーするかもしれません。)

これを防ぐには、サーバーIDを変更する必要があります。

終わりに

いかがでしたでしょうか。単純な仕組みですが、非常に高効率でHA構成が組めると思います。実測での停止時間ですが、次のようなかんじでした。

・バックアップ時にスレーブを落としたとき、準同期レプリケーションが止まってしまうため、このときにクエリを発行していたら失敗する分(発行していなければダウンタイム無し。)
    →数秒
・マスタが停止したときのフェイルオーバ動作
    →最大約10秒(大抵5秒位)

サービス停止時間がたったこれだけとは、驚きですよね・・きっと。

問題点は、つぎの点ぐらいです。

「バックアップやってる最中にマスタが落ちる」ときがどうすることもできない点(バックアップ開始時からマスタ停止時までのクエリが飛ぶ)
二台同時、もしくはフェイルオーバーしてからフェイルバックする前に落ちたら終わり。(二台では限界ですね。)

疑問点や、間違いの御指摘等、ぜひお願いします!

Comments:0

Comment Form

Trackbacks:0

Trackback URL for this entry
http://dev.tapweb.co.jp/2011/01/325/trackback
Listed below are links to weblogs that reference
MySQL5.5-MySQL5.6+KeepAlivedを使った、2台構成HA型準同期レプリケーションの作り方 from tap dev blog

Home > まめ知識 | 開発裏話 > MySQL5.5-MySQL5.6+KeepAlivedを使った、2台構成HA型準同期レプリケーションの作り方

Search
Feeds

Return to page top