『High Performance MySQL 第4版』第9章まとめ:レプリケーション

技術系ノウハウ

『High Performance MySQL 第4版』を読んだ内容を章ごとにまとめています。


第9章 レプリケーション

  1. 概要
  2. レプリケーション概要
    1. 目的
    2. 非同期であることの性質
    3. 互換性
    4. 読み取りは伸ばせるが、書き込みは別の工夫が要る
    5. よくある用途
    6. レプリケーションの仕組み
  3. 内部の仕組み
    1. レプリケーション形式の選択
    2. GTID(グローバルトランザクション識別子)
    3. レプリケーションをクラッシュセーフにする
    4. 遅延レプリケーション
    5. マルチスレッドレプリケーション
      1. バイナリログのグループコミットとは
    6. 準同期レプリケーション
    7. レプリケーションフィルタ
  4. レプリケーションのフェイルオーバー
    1. 計画された昇格
    2. 計画外の昇格
    3. 昇格のトレードオフ
  5. レプリケーション・トポロジ
    1. アクティブ/パッシブ
      1. 構成
      2. 冗長性
      3. 注意点
    2. アクティブ/リードプール
      1. 構成
      2. 冗長性
      3. 注意点
    3. 推奨しないトポロジ
      1. デュアルソースのアクティブ/アクティブ
      2. デュアルソースのアクティブ/パッシブ
      3. デュアルソース+それぞれにレプリカ
      4. リング(循環)
      5. マルチソース
  6. レプリケーション運用とメンテナンス
    1. レプリケーション監視
    2. レプリケーションラグの測定
    3. レプリカがソースと一致しているかを判断する
  7. レプリケーションの問題と解決策
    1. ソース側のバイナリログ破損
    2. server_id の重複
    3. server_id の未設定
    4. 一時テーブルの欠落
    5. 更新がすべて複製されない
    6. レプリケーションラグが大きすぎる
    7. ソースからのパケットが大きすぎる
    8. ディスク容量不足
    9. レプリケーションの制約
  8. まとめ
  9. 業務での活かし方

概要

この章は、レプリケーションの運用方法についてまとめている。
レプリケーションは、ソースの変更をレプリカへ伝播させ、データを同期させる仕組み。スケールアウト構成の土台であり、高可用性、災害復旧、バックアップ、分析用途の負荷分離など、さまざまな目的で使われる。

ソース:同期元
レプリカ:同期先

レプリケーション概要

目的

同一トポロジ内の複数インスタンス間でデータを同期させることが目的である。ソースが「データやデータ構造を変更するイベント」をログに書き、レプリカはそのイベントを読んで再生する。

非同期であることの性質

レプリケーションは基本的に非同期であり、レプリカが常に最新である保証はない。レプリケーションラグ(現実の時間とレプリカの状態のズレ)には上限がなく、大きなクエリや負荷で秒〜時間単位まで遅れることもある。

互換性

新しいサーバを古いサーバのレプリカにするのは比較的うまくいく一方、古いサーバを新しいサーバのレプリカにするのは難しくなりがちである。新機能やSQL構文、レプリケーションが使うファイル形式の差が理由になる。メジャー/マイナーアップグレード前の検証が重要になる。

読み取りは伸ばせるが、書き込みは別の工夫が要る

レプリケーションは読み取りトラフィックの分散に効く。一方で、単にレプリカを増やすだけでは書き込み性能はスケールしない。ソースの書き込みが各レプリカで再生されるため、全体は最も弱い箇所に引っ張られる。

よくある用途

データ配布
遠隔地(別データセンター/別リージョン)にデータのコピーを維持する目的で使える。回線が断続的でも追随は可能だが、低ラグを求めるなら安定した低レイテンシのネットワークが必要になる。

読み取りトラフィックの分散
読み取りの多いアプリでは、レプリカに読み取りを逃がして分散できる。簡単なやり方から、ロードバランサなどの仕組みまで選択肢がある。

バックアップ
バックアップの補助にはなるが、レプリカはバックアップではなく、バックアップの代替にもならない。

分析・レポーティング
分析クエリ用の専用レプリカを用意し、業務トラフィックと分析負荷を分離する戦略が取れる。

高可用性とフェイルオーバー
MySQL を単一障害点にしないための構成要素として使える。適切なフェイルオーバー設計と組み合わせると、ダウンタイムを大きく減らせる。

アップグレード検証
新バージョンでの互換性確認として、先にアップグレードしたレプリカでクエリの動作を確かめる運用が一般的である。

レプリケーションの仕組み

最も基本的な構成(ソース1台+レプリカ1台)の概略は次のとおりである。

  1. ソースが変更をバイナリログへイベントとして記録する
  2. レプリカがソースのイベントを取り込み、リレーログへ保存する
  3. レプリカがリレーログを再生して、自分のデータへ反映する

取得処理と適用処理が分離されているため、両者は非同期に動作できる(I/O スレッドと SQL スレッドが独立に進む)。

内部の仕組み

レプリケーション形式の選択

バイナリログの書き方は binlog_format で選べる。

  • ステートメント形式: 実行したSQLを記録する。ログはコンパクトになりやすいが、非決定的なSQLで不整合が起きやすい
  • row形式: 行の変更内容を記録する。決定的で安全だが、ログが大きくなりやすい
  • ミックス形式: 状況に応じて切り替える。条件が多く、ログの内容が予測しづらい

本章の推奨は、特別な理由がない限りrow形式を選ぶこと。

GTID(グローバルトランザクション識別子)

従来は、レプリカが「どのバイナリログのどの位置まで読んだか」をファイル名+位置で管理していた。ソース障害や復旧でバイナリログが変わると、付け直し位置の判断を誤りやすく、重複適用や欠落につながる。

GTID は、コミットされた各トランザクションに一意な識別子を割り当て、レプリカは「どのGTIDまで適用済みか」を覚える。これにより、付け替えや昇格後の再構成がやりやすくなる。本章は GTID の常時有効化を強く推している。

レプリケーションをクラッシュセーフにする

GTID だけでは破綻要因が残るため、次の設定で壊れにくくする方針が示される。

  • innodb_flush_log_at_trx_commit = 1: 毎トランザクションでログを同期し、耐久性を上げる(入出力負荷は増える)
  • sync_binlog = 1: バイナリログも毎トランザクションで同期し、クラッシュ時の欠落を減らす(入出力負荷は増える)
  • relay_log_info_repository = TABLE: 位置情報をInnoDBテーブルで管理し、更新を原子的に扱えるようにする
  • relay_log_recovery = ON: クラッシュ時にリレーログを破棄し、ソースから再取得して整合を取り直す

遅延レプリケーション

意図的にレプリカを「常に数時間〜数日遅らせる」構成がある。誤操作でテーブルを落とした、などの事故時に、問題のGTIDの直前まで追いつかせて復旧を早める狙いで使う。設定は CHANGE REPLICATION SOURCE TO ... SOURCE_DELAY で行う。

遅延レプリカは有用だが、フェイルオーバー候補からの除外、監視、運用手順の複雑化などのトレードオフも大きい。

マルチスレッドレプリケーション

レプリカ側の適用が単一スレッドだと追随が追いつかない、という課題に対して、複数の適用スレッドで並列に反映する仕組みがある。

並列化のモードは大きく2つである。

  • DATABASE: DB単位で並列適用する(同一DBは同時更新しない)。複数DBにデータを分散している構成向き
  • LOGICAL_CLOCK: グループコミット単位で並列適用でき、単一DB中心でも並列性を得やすい

バイナリログのグループコミットとは

グループコミットは、コミットをまとめてディスク同期できるようにする考え方である。これを増やすための調整として、binlog_group_commit_sync_delaybinlog_group_commit_sync_no_delay_count が紹介される。コミットをまとめて fsync() できると効率が上がる、という位置づけになる。

実務では、replica_parallel_workers を 0 以外にし、単一DB中心なら replica_parallel_type = LOGICAL_CLOCK を検討する。並列適用にはコーディネータのオーバーヘッドがあるため、replica_preserve_commit_order を有効にして順序の問題を避けつつ、ワーカー数を詰める。

ワーカー数の決め方として、性能スキーマで各ワーカーの処理件数を観察し、並列性が実際に得られているかで判断する方法が示される。

準同期レプリケーション

準同期を有効にすると、ソースはコミットのたびに、少なくとも1台のレプリカが「受け取ってリレーログへ書いた」ことを確認してから完了する(適用完了まで待つわけではない)。その分レイテンシが増える。

重要な点として、一定時間内に応答がなければ非同期へフォールバックし、トランザクション自体は失敗しない。したがって、データ整合性の担保としてこれに依存すべきではない、というスタンスが示される。

レプリケーションフィルタ

「一部だけ複製する」ためのフィルタ。便利そうに見えて事故を呼びやすい。

フィルタは大きく2種類である。

  • ソース側でバイナリログへ書くイベントを絞る: binlog_do_db / binlog_ignore_db
  • レプリカ側でリレーログから読むイベントを絞る: replicate_*

特に *_do_db / *_ignore_db は「対象オブジェクトのDB名」ではなく「現在のデフォルトDB」に対して効く、という点に注意。

USE test;
DELETE FROM sakila.film;

この場合、フィルタは sakila ではなく test を基準に作用し得る。意図しない欠落や不整合を作りやすい。

さらに binlog_do_db / binlog_ignore_db は、ポイントインタイムリカバリも成立しなくなり得るため、ほとんどの場面で避けるべきとされる。例外的に、特定DBだけを別サーバへ移すための一時的な用途はあり得るが、基本方針は「必要なときだけ、細心の注意で」である。

また「特定の操作だけ複製させたくない」という目的で SET SQL_LOG_BIN=0 を使う手もあるが、別種の事故を招きやすく、使う場合は副作用込みで慎重な運用が必要になる。

レプリケーションのフェイルオーバー

レプリカをソースへ昇格させる行為(フェイルオーバー/昇格)は、誤るとデータ欠落や長時間停止につながる。本章では、昇格とフェイルオーバーを同義として扱い、手順を明確にしておく重要性を強調している。

計画された昇格

メンテナンス(セキュリティパッチ、カーネル更新、MySQL 再起動など)が典型理由になる。

  • 昇格対象のレプリカを決める(最もデータが揃っている可能性が高いもの)
  • ラグを確認する
  • 既存ソースを super_read_only にして書き込みを止める
  • GTID を比較し、昇格対象が追いつくまで待つ
  • 昇格対象の read_only を解除する
  • アプリの書き込み先を昇格先へ切り替える
  • 他レプリカを新ソースへ付け替える(GTID と AUTO_POSITION=1 があると作業しやすい)

計画外の昇格

ソース障害が原因になる。生きているソースがないため、レプリカに届いている範囲で候補を選ぶことになる。

  • 昇格対象のレプリカを決める
  • 昇格対象の read_only を解除する
  • アプリの書き込み先を昇格先へ切り替える
  • 他レプリカを新ソースへ付け替える(旧ソース復帰後の扱いも含める)

旧ソースが復帰したときに誤って書き込みを受けないよう、デフォルトで super_read_only を有効にしておくべき、という注意もある。

昇格のトレードオフ

停止時に反射的にフェイルオーバーしたくなるが、必ずしも最速とは限らない。計画外の昇格は頻繁に練習するものでもなく、候補選定や手順確認に時間がかかる。耐久性の設定が適切なら、復旧を待つほうが早く、欠落も避けやすいケースがある。

レプリケーション・トポロジ

どのMySQLサーバが書き込み元(ソース)で、どのサーバが複製先(レプリカ)で、それらをどうつなぐかという「複製の構成図」
トポロジの自由度は高いが、複雑にすると運用不能になりやすい。まずはシンプルな構成を優先すべき。

アクティブ/パッシブ

読み取り・書き込みをソース1台へ集約し、レプリカは通常トラフィックに使わない構成。ラグを気にせず、書き込み直後に古いデータを読んでしまう問題を避けたい場合に向く。

構成

フェイルオーバー先になれるよう、ソースとレプリカのCPU/メモリなどの構成を同等にしておく。

冗長性

物理環境では n+2(最低3台)を勧める。クラウドでは条件により n+1(2台)も成立し得るが、データサイズやコピーの容易さ次第になる。クラウドの動的プロビジョニングを活かし、メンテ時に一時的に追加レプリカを立てて更新し、常に「すぐ昇格できるレプリカ」を確保する。

注意点

読み取りの上限が単一サーバに縛られる。読み取り限界に達したら、次の構成へ進む必要が出る。

アクティブ/リードプール

書き込みはソースに集約し、読み取りをソースまたはレプリカ群にも流す構成。水平に読み取りを伸ばしやすい一方、プール拡大に伴いソースのレプリケーション負荷も増え、どこかで伸びが鈍る。

構成

フェイルオーバー候補として、プール内に同等構成のレプリカを最低1台(できれば2台)確保する方針が示される。コスト最適化で異なるスペックを混ぜる場合は、トラフィックの重み付けで調整する。

冗長性

フェイルオーバー候補を確保しつつ、読み取りトラフィックとノード故障を吸収できる台数を用意する。CPU 利用率は上げすぎるとコンテキストスイッチが増えレイテンシが悪化するため、余裕を持つ設計が重要になる。

注意点

古い読み取りを許容する設計が必須である。ラグが大きいノードをプールから外す仕組みが要る。ノード数が増えるほどパッチ適用などの運用作業が増えるため、デプール→作業→リプールの自動化が効く。

推奨しないトポロジ

デュアルソースのアクティブ/アクティブ

両側へ書き込みを送る構成は、ルーティングの難しさとラグによる不整合、容量設計(片側障害時に片側へ全負荷が寄る)、作業セットの変化によるバッファプールの入れ替わりなど、問題が多い。

デュアルソースのアクティブ/パッシブ

表面的には悪くないが、2台構成に縛られ、障害時の柔軟性を落とす。レプリケーション設定自体は自動化可能であり、事前に固定しておく必然性が薄い、という立場である。

デュアルソース+それぞれにレプリカ

ルーティング問題が残り、フェイルオーバー手順も増える。恒久構成として避けるべき、という評価になる。

リング(循環)

どこか1台落ちるだけで更新が止まる。利点がなく、複雑さだけが増える。

マルチソース

特定用途の一時的利用(例: 別クラスタのデータを集約して統合する)としては成立するが、恒久的なトポロジとしては避けるべき、という整理である。1つのレプリカが同一ソースに対してマルチソースを多重に設定できない、といった制約もある。

レプリケーション運用とメンテナンス

レプリケーション監視

監視はソースとレプリカの双方に関係するが、問題の多くはレプリカ側に出る。本章は次を重要項目として挙げる。

ディスク容量
ソースはバイナリログ、レプリカはリレーログを使う。ソースが容量不足になるとコミットできずタイムアウトし始める。レプリカ側でも容量不足でレプリケーション停止が起き得る。

状態とエラー
スレッドが動いているか、止まっているなら直近エラーが何か、を見て次の手当てにつなげる必要がある。

遅延レプリカの遅延
意図した遅延になっているかを監視する。遅すぎても使いづらく、遅れなさすぎると事故時に役に立たない。

レプリケーションラグの測定

SHOW REPLICA STATUSSeconds_behind_source は一見便利だが、そのまま信用できない理由がある(処理中でないと計算できない、停止時は NULL、エラーでも 0 を返し得る、長時間トランザクションで値が跳ねる、など)。

より確実な方法として、ソース側で毎秒更新するハートビート(タイムスタンプ)を作り、レプリカ側で差分を取る方法が推奨される。代表例として Percona Toolkit の pt-heartbeat が挙げられる。

ハートビートは、単にラグを測るだけでなく「レプリカのデータがどの時点まで反映されているか」を示す目印にもなり、障害対応でも役に立つ。

また、ラグ指標だけでは「追いつくまで何分かかるか」は決まらない。レプリカの性能や、ソースが継続して処理する書き込み量など、多くの要因に左右される。

レプリカがソースと一致しているかを判断する

理想は「ラグ以外は一致」だが、現実には差分が入り得る。要因として、レプリカへの誤書き込み、両側書き込み構成、非決定的なSQLとステートメント形式、耐久性を落とした運用中のクラッシュ、MySQLのバグなどが挙げられる。

本章が推す実務ルールは次のとおりである。

  • レプリカは常に super_read_only を有効にして運用する
  • row形式を使う、またはSQLを決定的に書く
  • 同一トポロジで複数サーバへ同時に書き込まない

決定的に書く例として、LIMIT だけの削除は行順に依存し得るため、ORDER BY を付けて対象を固定する、という考え方が示される。

DELETE FROM users
WHERE last_login_date <= NOW()
ORDER BY user_id
LIMIT 10;

レプリケーションエラーに遭遇したら、公式ドキュメントに沿ってレプリカを再構築する方針が推奨される。

レプリケーションの問題と解決策

レプリケーションはセットアップしやすい一方で、止まる/壊れる要因も多い。本節では代表的な問題を列挙する。

ソース側のバイナリログ破損

破損箇所を飛ばすとトランザクション欠落につながるため、基本はレプリカを再構築するしかない。

server_id の重複

複数レプリカで同じ server_id を使うと、切断・再接続を繰り返したり、イベント欠落/重複で不整合が起きたりする。対策は重複させないことに尽き、割り当て表の管理などが挙げられる。

server_id の未設定

CHANGE REPLICATION SOURCE TO は通っても START REPLICA でエラーになる。@@server_id に値が見えてもデフォルトの場合があるため、明示設定が必要である。

一時テーブルの欠落

ステートメント形式では、一時テーブルが停止/クラッシュで消えると後続が失敗する。根本対策はrow形式である。次善策としては、一時テーブルの命名規約を決め、複製対象から除外する運用もあり得る。

更新がすべて複製されない

SET SQL_LOG_BIN=0 の誤用やフィルタ理解不足により、ソースの更新が伝わらないことがある。たとえば、DBだけを対象にするフィルタは「デフォルトDB」に作用するため、意図せず重要な更新が落ちるケースがある。

USE test;
UPDATE sakila.actor ...;

レプリケーションラグが大きすぎる

「多少のラグを許容する」アプリ設計が前提になる。対処として、マルチスレッドレプリケーション、シャーディング、耐久性の一時的引き下げ(最終手段)が挙げられる。

耐久性の一時的引き下げは、レプリカ側の sync_binloginnodb_flush_log_at_trx_commit を下げ、書き込み適用を加速する狙いになる。ただし、バックアップ方式次第で復旧不能になる、クラッシュしたら再構築が必要になる、戻し忘れやすい、などのリスクが大きい。監視や自動復帰まで含めた設計が必要になる。

ソースからのパケットが大きすぎる

ソースとレプリカで max_allowed_packet が不一致だと、ソースがログに残したイベントをレプリカが「大きすぎる」と判定し、エラーのループやリレーログ破損を起こし得る。

ディスク容量不足

バイナリログ、リレーログ、一時ファイルなどでディスクが埋まる。特にレプリカが遅れるほど、未適用のリレーログが溜まりやすい。監視に加えて relay_log_space の設定が挙げられる。

レプリケーションの制約

レプリケーションは、エラーが出なくても不整合に陥り得る。再現性のない関数や書き方、アプリ側の複雑さ、そしてMySQL自体のバグが要因になり得る。新機能やアップグレード時はテストが重要であり、監視で早期検知できる体制が必要になる。

まとめ

レプリケーションは強力だが、複雑にすると脆くなる。リングや両側書き込み、安易なフィルタリングなど、凝った構成は避け、まずは「全体を丸ごと複製し、ソースとレプリカをできるだけ同一に保つ」という方針を優先する。これが、運用で破綻しにくい最短距離になる。

業務での活かし方

  • レプリケーションの仕組みを理解して設計する
    • 非同期であり、レプリカが常に最新とは限らない(ラグはゼロ保証ではない)
    • 「書き込み直後に読み取りが古い」問題が起きる前提で、アプリ側の読み取り先を決める
  • レプリカを「安全に運用するための設定」を揃える
    • GTID を有効化し、付け替えや昇格後の再構成を簡単にする
    • 可能ならrow形式で運用し、非決定的なSQL(LIMIT だけなど)による不整合リスクを下げる
    • レプリカは super_read_only を基本にして、誤書き込みを防ぐ

コメント

タイトルとURLをコピーしました