ユユユユユ

webエンジニアです

レプリケーションログの実装

 リーダーノードからリードレプリカに変更差分を通知するのはレプリケーションログの転送によってであるとこれまで述べてきた。ではレプリケーションログとはなにか。どのような詳細を持っているのか。これにもいくぶんばらつきがあり、ここで整理することにする。次の4本立てになる。

クエリ単位でのレプリケーション

 もっとも単純な連携手法として、リーダーは単に実行したクエリをログに落とし、リードレプリカにログを転送することが考えられる。リードレプリカ側では受け取ったログをパースして、厳密に同じ順序でひとつずつクエリを実行していけば、リーダーと同じデータを複製できるはずである。

 うまくいくように見えるかもしれないが、これには本質的な欠陥があって、一般には推奨できない。例えば NOW()RAND() といった関数がクエリに含まれるとき、同じ結果を再現することができなくなってしまう。また厳密に同じ順序で実行することを要請するにも限界があり、例えば並行トランザクションの扱いをどうするか? といった振る舞いは定義不能に陥ってしまう。

 MySQL はバージョン 5.1 以前でこの方式を採用していたが、いまでは使っていない。

ログ先行書き込み (WAL) を利用したレプリケーション

 データベースへの書き込みに先立って、変更内容をあらかじめログに書き出すことで処理の永続性を保証するログ先行書き込み (Write-ahead log, or WAL) という仕組みがデータベースエンジンに備わっていることがある。本来的にはこのログは、書き込み中にダウンしたノードの再起動時に、どこまで処理が成功しているかを参照するためのものであるが、このログをレプリケーションログに転用して、書き込み結果を複製することが可能である。 PostgreSQLOracle はこの方式によりレプリケーションを備えている。

 この方式にもデメリットがある。それは低レベルな実装の詳細である WAL にレプリケーションが依存してしまうことで、データベースのアップグレードの障害となりかねない点である。要するに、 WAL に互換性のない変更が導入されたときに、クラスタ内で異なるバージョンのデータベースを動かすことが不可能になってしまうのである。

 ちなみにこの制約がなければ、データベースのバージョンアップは一般に次のような手順でダウンタイムなく実施することができる。

  1. リードレプリカを先にアップグレードする
  2. フェイルオーバーを実施しアップグレード済みのノードをリーダーに昇格させる
  3. 降格した元リーダーをアップグレードする

 WAL の実装にレプリケーションを依存させてしまうと、クラスタに属するすべてのノードを一斉にアップグレードする必要が生じる。サービスを稼働させたままこのオペレーションを行うのは現実的に不可能で、サービスを停止させてアップグレード作業を実施する必要が生じてしまう。

列単位での論理的レプリケーション

 前項の議論を踏まえると、実装の詳細である WAL をそのままレプリケーションログに転用するのではなく、なんらかのアルゴリズムによって WAL を抽象化したものをレプリケーションログとして利用すればよさそうである。例えば次のようなやり方が採用できそうである。

  • INSERT についてはすべての行の値をログする
  • DELETE については主キーをログする。主キーがないときはすべての行の値をログして完全一致する列を消す。
  • UPDATE については行を特定するのに最低限必要な数の行のデータと新しい値をログする。

 これらの道具があれば事足りるはずだ。トランザクション処理については、これらの道具の組み合わせと、コミットを表すログでひとまとまりとみなしてログに落とせばよい。

 この方式で作成されるレプリケーションログは WAL よりもパースしやすい構造となっているはずで、おのずと外部システム連携への道も開ける。例えばレプリケーションログをデータウェアハウスに送信して、異なるクラスタにデータを複製するようなことも可能になる。

トリガーを利用したレプリケーション

 データベース層でなくアプリケーション層でレプリケーションを行いたいというケースもまれにあるだろう。ストアドプロシージャなどの仕組みを使って、データベースの変更をフックにアプリケーションコードを実行することも不可能ではない。不可能ではないが、データベース層で実行するレプリケーションよりも当然オーバーヘッドは大きくなるから、計算量その他に対する繊細な配慮は必要となるだろう。