レプリケーションしてるMySQLで、マスタやスレーブが障害停止した場合のリカバリプラン

MySQLで、レプリケーションベースのHAな構成について考えたメモです。

3台(というか2台+1台)がいいかなぁと思っていて、前半はその理由を、後半では{マスタ,スレーブ}が{再起不能になった,ちょっとダウンしてすぐ復帰した}場合のリカバリプランについて書きます。

今のところはこれがベストかなと思っているのですが、「こうしたほうがいいと思う!」「ここがおかしい!」などなどのご意見はコメント、TBなどでいただけるとうれしいです。

ゴール

  • マスタが落ちてもぐーすか寝ていられるようにしたい
  • リカバリの作業はできるだけ単純に、かつ、短時間で完了するようにしたい
  • めんどくさいのはいや

基本構成、方針


  • 2台+1台
    • サービスで使うのは2台 (db1, db2)
    • もう1台は管理用 (db3)
  • スレーブを多数並べる構成にはしない
    • 台数増えると管理コストが上がる
    • マスタダウン時のフェイルオーバとそのフェイルバックの作業が煩雑になる
      • DBサーバは、並べるだけで済むWebサーバと違う
  • アクセスが捌けなくなりそうなときは:
    • MySQLやアプリのチューニングで乗り越える
    • どーしても突破できないときは、スケールアップを考える
    • それでもどーしても突破できないときは、水平パーティショニングを考える
    • それでもそれでもどーしても突破できないときは、垂直パーティショニングを考える

レプリケーションの世界の話

  • 双方向レプリケーションしている理由
    • 例えば「プライマリがフリーズしたのでリセットして復帰した場合」など、プライマリだったサーバが短い時間の後に復帰した場合の復旧作業を楽にするため。詳しくは後述。
    • 経験上、「短時間の停止」は割とよくあることなので、復帰手順を簡単にしたい。

用語の整理: プライマリとセカンダリとバックアップ

  • プライマリとは、更新系クエリを受け付ける唯一のサーバのこと
    • プライマリの実体というか正体というかは浮動IPアドレス
  • セカンダリとは、参照系のクエリを受け付けるサーバのこと
  • バックアップとは、実サービスには使わないスレーブのこと
  • クライアントは、プライマリのVIPに向けて更新系クエリを発行する
    • プライマリのVIPはひとつのサーバにしか付与されないので、更新系クエリを受け付け処理するのは、ただ一つのサーバということになる
  • クライアントはlvsを介してセカンダリに参照系クエリを発行する
    • セカンダリが停止しても参照系クエリを処理できるように、ロードバランサを介してセカンダリとプライマリでアクティブ/バックアップ構成にするか、重み付けのロードバランスにする。
    • セカンダリは set global read_only = 1 して、更新系クエリが来てしまっても受け入れないようにする(事故防止)
  • バックアップの用途:
    • 日次のスナップショットの採取に (7世代ぐらい保存する)
      • for db in ALL_DB; do mysqldump --opt --no-autocommit --hex-blob -master-data=2 --database $db | gzip -c > YYYYMMDD/$db.gz; done
    • 集計などに
      • 実サービスには投入していないので、重いクエリを発行してもOK。

障害時の対応手順

プライマリが再起不能
  • プライマリにデータの消失を伴う故障が発生した場合
    • RAID配下のディスクが複数台同時に故障した
    • RAIDコントローラが故障した
  • 作業概要
    • プライマリがダウンした時点で、(自動的に)セカンダリがプライマリに昇格する
    • 旧プライマリは故障解消後、バックアップからのまるごとコピーを使って復旧する。
  • プライマリがダウン
  • ★サービス停止★ (更新系のみ停止)
  • <セカンダリをプライマリに昇格>
    • →更新、参照共に新プライマリに行く
  • ★新プライマリ復旧★
  • ★サービス仮復旧★
  • セカンダリとバックアップで、SHOW SLAVE STATUSのExec_Master_Log_Posを比較する
    • ▼(A):旧セカンダリExec_Pos = バックアップExec_Pos:
      • セカンダリとバックアップのデータの同期点がとれたので次のステップ(C)へ移る→
    • ▼旧セカンダリExec_Pos > バックアップExec_Pos:
      • セカンダリとバックアップで、SHOW SLAVE STATUSのRead_Master_Log_Posを比較する
        • ▼旧セカンダリRead_Pos = バックアップRead_Pos:
          • Exec_Posが同じになるまで待って、ステップ(A)へ移る→
        • ▼旧セカンダリRead_Pos > バックアップRead_Pos:
          • セカンダリとバックアップのrelayログをmysqlbinlogで見て比較し、差分のクエリをバックアップに対して手動で発行する。
          • 後、Exec_Posが同じになるまで待って、ステップ(A)へ移る→
        • ▼旧セカンダリRead_Pos < バックアップRead_Pos:
          • (B):あんまり起こらないはず、起こるとちょっとまずいシチュエーション。。。
          • セカンダリとバックアップのrelayログをmysqlbinlogで見て比較し、差分のクエリを旧セカンダリに対して手動で発行する。
          • セカンダリは既に新プライマリとして更新系クエリを受け付けているので、差分の確認とその適用は、新プライマリになった後で受け付けたクエリも加味して慎重に検討する必要がある。
          • 後、Exec_Posが同じになるまで待って、ステップ(A)へ移る→
    • ▼旧セカンダリExec_Pos < バックアップExec_Pos:
      • 起こるとちょっとまずいシチュエーション
      • セカンダリとバックアップで、SHOW SLAVE STATUSのRead_Master_Log_Posを比較する
        • ▼旧セカンダリRead_Pos = バックアップRead_Pos:
          • 起こり得ない。
            • プライマリに昇格する際にReadとExecが同じになるまで待っているので、Execが旧セカンダリ < バックアップで、Readが旧セカンダリ = バックアップになるのはあり得ない。待てばバックアップのExec_Posが進むはずなので。
        • ▼旧セカンダリRead_Pos > バックアップRead_Pos:
          • Exec_Pos < Read_Pos なのであり得ない
        • ▼旧セカンダリRead_Pos < バックアップRead_Pos:
          • 起こるとちょっとまずいシチュエーション
          • ステップ(B)へ移る→
  • バックアップの復旧
    • (C):バックアップのmysqldを停止して、<丸ごとバックアップを採取>する
    • master.infoを編集して、新プライマリのスレーブになるようにする
      • Master_Host を新プライマリの個有名(db0ではなくdb2など)に変更
      • 新プライマリは昇格時にバイナリログをローテートしているので、以下の通りに変更する
        • Master_Log_Fileは新しいログファイル名
        • Read_Master_Log_Posは4
    • バックアップのmysqldを起動する
  • ★バックアップ復旧★
  • セカンダリの復旧
    • セカンダリのサーバのセットアップができたら
    • スレーブのロードバランスグループに入らないようにする
    • (C)で採取した丸ごとバックアップを展開する
    • master.infoを編集して、新プライマリのスレーブになるようにする
      • Master_Host を新プライマリの個有名(db0ではなくdb2など)に変更
      • 新プライマリは昇格時にバイナリログをローテートしているので、以下の通りに変更する
        • Master_Log_Fileは新しいログファイル名
        • Read_Master_Log_Posは4
    • セカンダリのmysqldを起動する
    • 新プライマリと同期するまで待つ
    • セカンダリでSHOW MASTER STATUSし、新プライマリで、そのPosにCHANGE MASTER TOする
      • CHANGE MASTER TO MASTER_LOG_FILE = 'mysql-bin.000XXX', MASTER_LOG_POS = XXX;
    • 新プライマリで START SLAVE
    • レプリケーションが追いついたのを確認してスレーブのロードバランスグループに入れる
  • ★新セカンダリ復旧★
  • ★サービス完全復旧★
プライマリが短時間停止
  • 比較的短時間の停止
    • OS rebootした
    • ハングアップしたのでリセットした
  • 作業概要
    • プライマリがダウンした時点で、(自動的に)セカンダリがプライマリに昇格する
    • 旧プライマリは復帰後、レプリケーションを再開し、追いついたらセカンダリとして参照系クエリを処理するようにする
  • プライマリがダウン
  • ★サービス停止★ (更新系のみ停止)
  • <セカンダリをプライマリに昇格>
    • →更新、参照共に新プライマリに行く
  • ★新プライマリ復旧★
  • ★サービス仮復旧★
  • セカンダリとバックアップで、SHOW SLAVE STATUSのExec_Master_Log_Posを比較する
    • (以下↑の手順と同じ)
  • 旧プライマリのmysqldは起動しない
  • 新プライマリのSHOW SLAVE STATUSのExec_Master_Log_Posと旧プライマリの最新のバイナリログの最後のend_log_posを比較
    • mysqlbinlog $(ls -1t mysql-bin.[0-9]*|head -n 1) | grep end_log_pos | tail -n 1
    • ▼(A):新プライマリExec_Pos = 旧プライマリend_log_pos:
      • ステップ(B)へ移る→
    • ▼新プライマリExec_Pos > 旧プライマリend_log_pos:
      • sync_binlog = 0 になっていると、起こりうる。
        • 不意の電源断とかリセットがかかった場合、ディスクにsyncされていないbinlogは失われてしまうので。
      • この場合、旧プライマリのデータは使えないので、「プライマリが再起不能」の手順に従い、データを捨てて復旧する。
    • ▼新プライマリExec_Pos < 旧プライマリend_log_pos:
      • 新プライマリのSHOW SLAVE STATUSのRead_Master_Log_Posと旧プライマリの1つ前のバイナリログの最後のend_log_posを比較
        • ▼新プライマリRead_Pos = 旧プライマリend_log_pos:
          • Exec_Posが同じになるまで待って、ステップ(A)へ移る→
        • ▼新プライマリRead_Pos > 旧プライマリend_log_pos:
          • ありえない。
        • ▼新プライマリRead_Pos < 旧プライマリend_log_pos:
          • 起こるとちょっとまずいシチュエーション。
          • 新プライマリリレーログと旧プライマリのバイナリログをmysqlbinlogで見て比較し、差分のクエリを新プライマリに対して手動で発行する。
          • 新プライマリは既に更新系クエリを受け付けているので、差分の確認とその適用は、新プライマリになった後で受け付けたクエリも加味して慎重に検討する必要がある。
          • 後、Exec_Posが同じになるまで待って、ステップ(A)へ移る→
  • (B):旧プライマリを新プライマリのスレーブにする
    • 旧プライマリのmysqldは停止しているはず
    • スレーブのロードバランスグループに入らないようにする
      • iptables -I INPUT -j REJECT -p tcp -s 192.0.2.60/30 --dport 3306
      • ヘルスチェックが失敗するように -s には内部lvsのアドレスを指定する
    • 旧プライマリのmysqld開始
      • my.cnfにread_onlyと書いておく
      • mysqldを起動する
      • 新プライマリで START SLAVE する
      • 旧プライマリで START SLAVE する (自動的に開始してるかも)
    • レプリケーションが追いついたのを確認してスレーブのロードバランスグループに入れる
  • ★新セカンダリ復旧★
  • ★サービス完全復旧★
セカンダリが再起不能

セカンダリがダウンすると、参照系のクエリはプライマリに流れるようにしているので、セカンダリがダウンしてもサービスには影響なし。

  • 作業概要
    • バックアップで<まるごとバックアップを採取する>
      • バックアップはサービスに使ってないので止めても影響なし
    • それをセカンダリに展開して、mysqldを起動
セカンダリが短時間停止
  • 作業概要
    • mysqldを立上げるだけでOK

共通手順

<セカンダリをプライマリに昇格>
  • プライマリの浮動IPアドレスを付与する
    • ip addr add PRIMARY_IPADDR/16 broadcast 192.0.2.255 dev bond0:0
    • send_arp PRIMARY_IPADDR $(ip link show bond0 | grep 'ether' | awk '{print $2}') 255.255.255.255 ff:ff:ff:ff:ff:ff
    • ちなみに剥奪するのは ip addr del PRIMARY_IPADDR/16 dev bond0:0
  • Read_Master_Log_Pos が Exec_Master_Log_Pos と同じになるまで待つ
  • SHOW SLAVE STATUS の結果をメモる
  • STOP SLAVE
  • FLUSH LOGS
    • binlogをローテートするため
  • SET GLOBAL READ_ONLY = 0
<まるごとバックアップを採取する>
cd /db
tar --exclude 'mysql-bin.*' -cvpf mysql.tar mysql