MySQLレプリケーション+keepalived

linux, サーバー関連 2010年6月19日,

地味にみなさんがどういう設定をしてるかが気になるところですが
正直何の知識もない僕が仕事で
「DB2台でレプリケーションよろしく」といわれて試行錯誤で構築した軌跡です。

他の人たちがどのように構築しているかがすごく気になるところですねぇ・・・

目的

すでに構築されているWEBとDBの構成のもの。
DBを1台増やすから「1台が落ちても2台目がホットスタンバイしてて即座に切り替わる構成」
としてほしいというのが依頼。



構成的にはこんな感じにDBが1台増える感じ。



こんな風に普段はWEBはDB1を参照するけど、DB1に障害が起きた場合は!



こんな形でDB2を参照するようにしちゃう。
こういった風に代替サーバーが役割を引き継ぐ事をフェイルオーバーって言うらしいよ!

これを可能にするにはDB1とDB2の間で以下の事がないと成立できない
例えばDB1に障害が起きた場合は

・DB1に起きた障害を検知できる事
・検知後、DB2に切り替わる事
・DB1とDB2のデータベースの内容が常に同じである事

の3つが成立しないといけないわけですね。
これを実現させるためにkeepalivedやMySQLレプリケーションといった技術を組み合わせて使います。

※今回の依頼は負荷分散に関してはまったく言及されていなかったのでこんな構成です。
負荷分散も視野にいれるとWEBとDBの間にロードバランサーとかをいれてスレーブ間だけを
フェイルオーバーしていくような構成になると思いますがやったことないのでほっときます。


keepaliveで障害感知&バーチャルIP

んでタイトルにもあるkeepalivedを使います。

普段はロードバランサ側に入れてバックエンドのヘルスチェックとVRRPで自身の冗長化とかに使うみたい。
今回使える!と思ったのは「VRRPで自身の冗長化」という部分ですね。
VRRPとか言われても専門用語うぜー。意味不なんだけど
2台間でバーチャルIPを持つ事ができるみたいなんですよね。

つまりこんな感で使うようにしてみた



ここではDB1とDB2にkeepalivedをインストールします。
DB1とDB2はkeepalivedの機能でお互いを監視(冗長化)させます。

10.10.10.100がバーチャルIPで10.10.10.100は現在DB1が持っているIPです。
つまり10.10.10.100に接続がくるとDB1(10.10.10.101)へ向きます。
ここで上記のヘルスチェックの際、DB1に障害が発生した場合に、
10.10.10.100の向き先がDB2(10.10.10.102)へ向かうようになれば今回の案件に適います。

今回の件では設定のvirtual_serverのディレクティブはいらなかったかも・・・?

DB1にkeepalivedのインストールと設定

OSはDebian lennyです。
まずはDB1側。
今回はこのDB1をマスターとして使用します。
DB1にログインしてインストールします。

インストール

apt-get install keepalived

設定

vi /etc/keepalived/keepalived.conf


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
! Configuration File for keepalived

global_defs {
  notification_email {
    sonar@sonarsrv.com
  }
  notification_email_from sonar@sonarsrv.com
  smtp_server localhost
  smtp_connect_timeout 30
}


vrrp_instance vip_mysqld {
 state MASTER
 interface eth0
 grap_master_delay 5
 virtual_router_id 1
 priority 150
 nopreempt
 advert_int 1
 authentication {
   auth_type PASS
   auth_pass secret
 }
 virtual_ipaddress {
   10.10.10.100/24 dev eth0
 }
}
virtual_server 10.10.10.100 3306 {
 delay_loop  3
 lvs_method  DR
 protocol    TCP
 real_server  10.10.10.101 3306 {
   TCP_CHECK {
     connect_port 3306
     connect_timeout 30
   }
 }
 real_server  10.10.10.102 3306 {
   TCP_CHECK {
     connect_port 3306
     connect_timeout 30
   }
 }
}

起動

/etc/init.d/keepalived start


DB2にkeepalivedのインストールと設定

次にDB2側。
DB2はスレーブとして使用します。
DB2にログインしてインストールします。

インストール

apt-get install keepalived

設定

vi /etc/keepalived/keepalived.conf


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
! Configuration File for keepalived

global_defs {
  notification_email {
    sonar@sonarsrv.com
  }
  notification_email_from sonar@sonarsrv.com
  smtp_server localhost
  smtp_connect_timeout 30
}


vrrp_instance vip_mysqld {
 state BACKUP
 interface eth0
 grap_master_delay 5
 virtual_router_id 1
 priority 100
 nopreempt
 advert_int 1
 authentication {
   auth_type PASS
   auth_pass secret
 }
 virtual_ipaddress {
   10.10.20.100/24 dev eth0
 }
}
virtual_server 10.10.10.100 3306 {
 delay_loop  3
 lvs_method  DR
 protocol    TCP
 real_server  10.10.10.101 3306 {
   TCP_CHECK {
     connect_port 3306
     connect_timeout 30
   }
 }
 real_server  10.10.10.100 3306 {
   TCP_CHECK {
     connect_port 3306
     connect_timeout 30
   }
 }
}

起動

/etc/init.d/keepalived start


keepalivedの確認

これでもうkeepalivedはちゃんと動いているはずです。
WEB側から10.10.10.100へpingを飛ばしてみると繋がるはずです。

ここでDB1(マスター)にて

ip addr show eth0

と入力すると、VIPが設定されているかが見れます

inet 10.10.10.101/24 brd 10.10.20.255 scope global eth0
inet 10.10.10.100/24 scope global secondary eth0


ここでマスター側をシャットダウンなりkeepalivedを止めるなりしてみると
10.10.10.100のIPがバックアップ側が持ちます。
WEB側から常にpingを飛ばしながら実験してみると確認できます。

マスターをrebootした時の流れは
①バックアップ側がマスターへ昇格。vipの10.10.10.100を持つ
②マスター側が復活するとマスターがマスターへ昇格。バックアップはバックアップへ戻り
 vipの10,10.10.100はマスターへ戻る
という動きとなります。

このマスターとスレーブの動きはよく検証しておく必要があります。
MySQLのレプリケーションとからませる場合は、どちらかのDBがマスターとして常に最新でなくてはならず
vipがふらふら動いてどっちにも更新が行われた場合はお互いのDBが「最新」ではなくなり
管理者としては「カス!ゴミ!クズ!」のレッテルを貼られることとなります。
当然レプリケーションも停止しますし。

フェイルオーバーした後のvipの動きを理解してどのような方針で復旧していくのか。
管理ポリシーをしっかりと想定しておきましょー。

ちなみに僕はDB1マスターが落ちた後、復活した場合でもkeepalivedは自動起動させず
万一起動してしまってもマスターではなくスレーブとしています。
一度マスターに障害がおきたらMySQL、keepalivedと共にDB2がマスターとなるようにし、
自動的にvipが元に戻るような事はないようにしています。

当初、この方針でいくと報告した際に心の中では
「ちょっとした負荷でkeepalivedで”落ちた判定”になりvipが移動しまくったらどうしよ」
とか思ってましたが今のところ問題はないです。


MySQLレプリケーション

ここまでで、WEB側からは10.10.10.100としてDB接続すればOKな構成になっています。
あとはDB1とDB2が常に同じ構成になればOKです。
ここでDB1とDB2を同期する方法はいろいろ考えました。
lrsyncとかネットワークごしのRAID1みたいな技術とか(名前忘れた)

でも試した結果、負荷が圧倒的に少ないのがレプリケーションでした。
概要としてはDB1をマスター、DB2をスレーブとしてDB1にきたクエリとかをバイナリログとして
そのままDB2に流し込み~ みたいな感じだと思うのですが
なぜか圧倒的に早い。そして圧倒的にDB2の負荷が軽い。

なんだろね~すごいね~
というわけでMySQLレプリケーションの構築。


マスターの設定
マスター側から。
僕の環境ではmy.cnfがない状態だったんで
適当なサンプルもんをつこうた。
/usr/local/mysqlにインストールしてたので

cp /usr/local/mysql/support-files/my-medium.cnf /etc/mysql/my.cnf
vi /etc/mysql/my.cnf


レプリケーションの要となるのは
server-id と log-binの項目になってきます。
server-idはレプリケーションをおこなるサーバー群で一意なものとなる必要があります。
重複してはいけない。

server-id = 1
log-bin = mysql-bin


としときました。
次にアカウントの作成
mysqlにログインして

GRANT REPLICATION SLAVE ON *.* TO repl@10.10.10.102 IDENTIFIED BY ‘password’;


とし、replというレプリケーション用のユーザーを作成しました。
10.10.10.102(スレーブ)からの接続のみを許可するようなユーザーとなります。
passwordの部分はちゃんと適当なパスワード作ってね。

この設定を反映させるためにmysqlを再起動します。

次にDB1とDB2を同期させる設定です。
まだお互いが空の状態ならいいんですがすでに動いてる環境からレプリケーションをする場合、
まずはDB1とDB2を同じ状態とする必要があります。

その為にDBの、バイナリログの値とオフセット値を控えて完全バックアップを取ります。
スレーブ側にはこの完全バックアップデータとバイナリログの値とオフセット値を与える事で
自動的にそれ以降のバイナリログを読み取って同期してくれる感じです。



DB1の完全バックアップを行うにはDBの更新処理を一時的にできなくする必要があります(ロック)
なぜならバックアップ中に更新処理が行われても
バイナリログの値とオフセット値は刻々と変化し続けるからです。

ここはどうしても停止処理が必要なので先方さんと話をあわせます。(まぁ再起動の必要もありますしね)
事前にバックアップにどれくらいの時間がかかるかを測定しておくのがいいです。

さて実際にDBをロックしてバックアップを取るには。
MySQLにログインした後

FLUSH TABLES WITH READ LOCK;


これで更新処理は完全に停止します。
この間に

SHOW MASTER STATUS;


と入力することでバイナリログ名とオフセット値がでてきます。
これは確実に!メモしてください。ロックまでしている意味がなくなりますのでw

1
2
3
4
5
6
+------------------+----------+--------------+------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000004 |      189 |              |                  |
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)

今回のような例なら mysql-bin.000004と189 をメモしておく。
次に完全バックアップ。

MySQLをログアウトして

/usr/local/mysql/bin/mysqldump -pパスワード-u root -x -A > mysql_dumpall.db


って感じでmysqlのフルバックアップを取得。
mysql_dumpall.dbにバックアップデータが出来たことを確認したら
再びmysqlにログインし、ロックを解除する。

UNLOCK TABLES;


とりあえず停止時間は終わり。
この後スレーブ側の設定へと移るわけですが
あんまりのんびりしてると完全バックアップ以降の更新がたまっていって同期に時間かかるので注意w

ちなみに、SHOW MASTER STATUS;をした時に
Empty set (0.00 sec)
と出た場合は、MySQLが空っぽで生まれたての姿であるという事です。
この場合はFileは””(空文字)、Positionは4となります。



スレーブの設定
マスター側と同じような設定をしていきます。

cp /usr/local/mysql/support-files/my-medium.cnf /etc/mysql/my.cnf
vi /etc/mysql/my.cnf


レプリケーションの要となるserver-id と log-binの項目を編集します。
server-idはレプリケーションをおこなるサーバー群で一意なものとなる必要があります。
重複してはいけない。
さっきは1を設定したのでスレーブは適当に2をw

server-id = 2
log-bin = mysql-bin


次にアカウントの作成
mysqlにログインして

GRANT REPLICATION SLAVE ON *.* TO repl@10.10.10.101 IDENTIFIED BY ‘password’;


とし、replというレプリケーション用のユーザーを作成しました。
10.10.10.101(マスター)からの接続のみを許可するようなユーザーとなります。
これは必ず必要ってわけじゃーないんですがkeepalivedでこちらがマスターとなった場合に
障害復旧する場合に必要になってくるので今のうちにつくっとこーって感じです。
passwordの部分はマスターのreplユーザーと同じでいいと思います。

ここまででmysqlを再起動。

で、ついにレプリケーションの開始です。
まずはマスター側からとってきた完全バックアップデータを
mysqlに流し込みます

mysql -u root -pパスワード < mysql_dumpall.db


mysqlにログインしデータベースの復元を確認します。
データベースの確認ができたらスレーブを開始!

1
2
3
4
5
6
CHANGE MASTER TO
MASTER_HOST='10.10.20.101',
MASTER_USER='repl',
MASTER_PASSWORD='パスワード',
MASTER_LOG_FILE='mysql-bin.****',
MASTER_LOG_POS=****;

****の部分にはメモしたバイナリファイル名とオフセット値を入力します。

スレーブを開始します。

START SLAVE;


以上でレプリケーションは完了です。

スレーブが開始されているかを確認

SHOW SLAVE STATUS\G


レプリケーションが開始された事を確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
*************************** 1. row ***************************
              Slave_IO_State: Waiting for master to send event
                 Master_Host: 10.10.10.101
                 Master_User: test
                 Master_Port: 3306
               Connect_Retry: 60
             Master_Log_File: mysql-bin.000004
         Read_Master_Log_Pos: 189
              Relay_Log_File: testcam-relay-bin.000022
               Relay_Log_Pos: 251
       Relay_Master_Log_File: mysql-bin.000004
            Slave_IO_Running: Yes
           Slave_SQL_Running: Yes

ここで新たに/usr/local/mysql/data内にmaster.info、relay-log.infoが作成される
(余談。master.infoはスレーブとして同期するマスターの情報がかかれていてmy.cnfよりも優先に読まれます。
ですのでmysqlを再起動しても何してもスレーブとして自動で動いてくれます。
障害時にマスターとスレーブを入れ替える必要がある時などは必ず削除して下さいな。
これで大変な事になったんだよねww旧マスターのDB全削除したらレプリケーション生きてて全DB消えたりとかw)

状態が両方yesになるまでしばらく待つ必要あり

Slave_IO_StateがWaiting for master to send event
Slave_IO_RunningがYes
Slave_SQL_RunningがYes
こうなっているのが確認できれば正常に動いています。

マスター側でDBを作ってみたりして即座にスレーブ側も反映されてればOK


レプリケーションがうまくいかない場合!

さて、僕が構築する上でよく陥ってたエラーです。

①ポートが開いてない
mysqlは3306ポートを使うのですがマスターとかスレーブ側のmysqlポート、
3306をちゃんとiptablesであけてますか?

②設定変更後mysqlを再起動していない
以外とやっちゃいます。my.cnfを作ったりちょっとでも変更したなら
反映させるにはmysqlの再起動が必要ですよ


mysqlレプリケーションはエラーでときどき止まる

長いことやってるとどうしてもエラーで止まる事がしばしば・・・
まず、エラーが起きるというタイミングは
スレーブ側にしかないレコードやらDBやらがあった場合。

例えばマスター側でtest というDBをcreateしたとしよう。
すると当然スレーブ側でもtestというDBを作られる事になるわけだけど
ここでスレーブ側にtestというDBがすでに存在すればこれだけでレプリケーションがストップします。

また、そのほかにもランダム系のクエリはエラーの発生が起こりやすくなります。
例えば1~4をランダムで取得してDBに入れるようなSQL文を実行すると
マスター側では1が格納されたのにスレーブ側では2が格納されてた、とか。
(ここらへんは結構改善されていってるっぽいですが)

まぁ多くは上2個が原因だったんですがとにかく結構とまります。
大手の構築でも、「実はレプリケーションは行われていなかった」なんて事よくあるみたいですしね。

と、いうわけで、レプリケーションが正常に行われているかの監視は必須です。
有名な監視ソフトでも死活監視はできてもレプリケーションの監視はできないので自前で作ります。
要はSHOW SLAVE STATUS\Gで
Slave_IO_RunningがYes
Slave_SQL_RunningがYes
になっていればよいので

1
2
3
4
5
6
7
8
9
#!/bin/sh

PASSWORD="パスワード"

eval  "`/usr/local/mysql/bin/mysql -u root -p${PASSWORD} -e 'show slave status \G' | sed -ne 's/: \(.*\)/="\1"/p' `"
if [ "$?${Slave_IO_Running}${Slave_SQL_Running}" != "0YesYes" ]; then
/usr/local/mysql/bin/mysql -u root -p"${PASSWORD}" -e 'show slave status \G' |/usr/bin/mail sonar@sonarsrv.com -s "REPLICATION DOWN "`hostname`
exit 1
fi

て感じで
Slave_IO_RunningとSlave_SQL_Runningの値がyesでなければ
show slave statusで取得できるステータス一覧をメールで送信するスクリプトを作成。
適当にreplchk.shとでもつけて実行権限を与えてcronで5分ごととかにまわしておけば
レプリケーションのチェックができます。(ついでにmysqlの死活監視もw)

基本的にはレプリケーションは止まってもサービスの停止にはならないので気長に復旧すればいいです。

多くは

SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1;

というコマンド一発で解決します。
(これは、エラーの出たクエリを1つ飛ばす(無視する)という意味だと思われ)

仕上げ


ここまでで、keepalivedとmysqlレプリケーションは完成したわけですが肝心の
マスター側のMySQLの監視ができていません。
例えばWEB側から変なSQL文きてループに陥ったりしてMySQLに接続できなくなっても
それは立派な障害です。(ポートはあいてるから監視ではOKとかでちゃうんだよね)
ってわけで適当にmysqlにつなぎにいって
もしmysqlに繋がらなければ自信のkeepalivedを落とす
というスクリプトを作成してcronでまわしときました。
今回はphpでww

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
/***********************************
* MYSQLの死活チェック
* 接続できるかどうかをチェックする
************************************/

// 接続先ホスト(必須)
$HOST = '127.0.0.1';
// 接続ユーザ(必須)
$USER = 'mysql';
// パスワード(必須)
$PASS = 'パスワード';
// 直接DBまでつなげたかったら書く
$DB_NAME = '';

// 失敗したときに実行したいコマンド
$FAIL_BIN = '/etc/init.d/keepalived stop |/usr/bin/mail sonar@sonarsrv.com -s "mysql down! "`hostname`';

// 成功したときに実行したいコマンド
$SUCCESS_BIN = '';

$result = db_connect_check($HOST, $USER, $PASS, $DB_NAME);

if (!$result) {
        // 失敗
        print "error\n";
        if ($FAIL_BIN) {
                system($FAIL_BIN);
        }
} else {
        // OK
        if ($SUCCESS_BIN) {
                system($SUCCESS_BIN);
        }
}
exit;

function db_connect_check($host, $user, $pass, $db_name) {
        $con = mysql_connect($host, $user, $pass, true);
        if (!$con) {
                // エラー表示
                print mysql_error();
                return false;
        }

        if ($db_name) {
                if (!mysql_select_db($db_name, $con)) {
                        // エラー表示
                        print mysql_error();
                        return false;
                }
        }
        return true;
}
?>

終わり

結構複雑になっちゃったんだよね・・・
他の人がどうしてるかかなり気になるのでメールなりなんなりで教えてくれると嬉しいです。

ちなみに最近ではAmazon EC2で14台構成の構築を行う案件があるんですが
DBのレプリケーションで悩んでいます。
今回のようにkeepalivedでバーチャルIP持てないし
そもそもローカルIPなんて概念があるのか・・・とか。
インスタンス全部がグローバルだしグローバル越のレプリケーションってできるの??違和感!!

Amazon EC2でMySQLレプリケーション行ってる事例があったら教えて欲しい!詳細を!

2 Responses to “MySQLレプリケーション+keepalived”

  1. CentOS5.5 keepalivedで負荷分散と冗長化 – igreks開発日記 Says:

    […] mysqlが落ちて、かつマシン(vrrp)が動いている場合、フェイルオーバが効かないので、mysqlが落ちた時に自身のkeepalivedを落とすスクリプト(PHP)を作成。 こちらからのまるパクリです。 […]

  2. funny videos Says:

    The following time I learn a weblog, I hope that it doesnt disappoint me as a lot as this one. I imply, I know it was my choice to learn, but I really thought youd have something fascinating to say. All I hear is a bunch of whining about one thing that you can repair in case you werent too busy searching for attention.

コメントどうぞ