Rails複数DBシステムMySQLからPostgreSQL移行物語


こんにちは、三苫です。

この記事はTECHSCORE Advent Calendar 2014、5日目の記事です。

近年、Rails複数DB Casual Talksが開催されるなど、Railsでも複数・異種データベース混在したシステム構成は何ら特別でなものではなく通常の開発でカジュアルに選択される構成だぞという機運が高まっています。

togetterで参加者の反応を見ても、「establish_connectionは基本」「前にも見たぞこのスライド」など、おおむね知見が業界全体に広まりつつある事がわかります。

本記事はRails複数DBがまだカジュアルではない時代、マルチテナントシステムのデータベースをMySQLからPostgreSQLに、各サブシステムは縮退しつつも、システム全体としては無停止で移行を行った記録を共有するためのものです。

移行したシステムの前提

  • マルチテナントシステム
  • データベースはMySQL
  • テナント管理データベースが1つ
  • 各テナントがそれぞれデータベースを一つ持つ
  • 各テナントのテーブル群はほとんど同じだが、システムで動的にDDLを発行しているためテナント固有のテーブルが多数存在する

つまりこんな感じ。データベース数がN+1件。

移行内容

  • MySQLからPostgreSQLに全面移行
  • 各テナントの移行中に、移行対象のテナントが数時間停止することは許容するがシステム全体としての停止はNG
  • 各テナントの停止時期は個別調整なのでMySQLとPostgreSQLのDBが混在する時期がある(数か月程度)

つまりこんな感じ。

さすがに30代中年社員の私もこれはカジュアルと呼ばないですね~。

何を検討しなければいけないか?

さて、このような要件を出された場合、考えないといけないことというのはいろいろあります。
そもそも、これは本当にやらないといけない事なのかという話などももちろんありますが、今回はその先に議論を進めます。

ざっくりと、こんなことを検討しないといけないですね。

  • MySQLのデータをどうやってPostgreSQLに移行するか
    (テーブル定義、ビュー定義、インデックス、データ)
  • 移行したデータの正当性をどうやって検証・保証するか
  • MySQL特有の挙動をPostgreSQLにどうやってエミュレートするか
  • 異種データベース混在期間、システムはどのような構成にするか。その間も保守や開発は行う。
  • データ移行中に対象のテナントで実施できなかったジョブはどのようにリカバリするか

正直、こんなことオッサンは一度もやったことないっす。知見ゼロスタート!(^v^)

MySQLのデータをどうやってPostgreSQLに移行するか

MySQLとPostgreSQLのデータのバイナリファイルには互換性がありません。データを移行するには異種DB間をORマッパーで繋ぐというアプローチも有りそうですが、私たちのシステムはテーブル定義が各テナントで異なるのでテーブル定義の移行はデータベース毎に論理ダンプをとるしかなさそうです。
どうせダンプをとるならデータもダンプし、ヒューマンリーダブルなテキストのSQLファイルをPostgreSQLに投入できるようクエリを書き換えるのが良さそうです。既存のDB移行ツールも検討しなかったわけではないのですが、自信をもってこれを使おうと言えるものはなかった印象です。

ダンプファイル置換アプローチでまず問題になるのが型の互換性です。RailsはMySQLのtinyint(1)はbooleanとして扱います、blobはbyteaにしましょう。地道にDDLを文字列置換するスクリプトを書きます。データも同様に必要あらば置換です。バイナリ型はデータの表現が微妙に違うので置換ロジックを書くのは大変ですね。
auto_incrementも難しいですね。serial型に置き換えた上で、その項目の最大値+1でsetvalしておかなければ同じIDが重複発行!

構文解析せずに正規表現置換しただけだと、クエリとしてパーサーが解釈可能なのか不安ですね。PythonのPgSanity( https://github.com/markdrago/pgsanity )というツールを使ってPostgreSQLの構文として誤りがないか検証しましょう。

こんな感じ。

移行したデータの正当性をどうやって検証・保証するか

さあ、これでMySQLからPostgreSQLにデータ移行できる!
けどこのデータはRailsのアプリから見たら同一のものと呼べるのかな?目視で確認できるレベルでは同じに見えるけど全部のデータがそうなのかな?どうやって検証しよう、保証しよう?

Railsから全データを見てみるしかないよね。全テーブルに対応するモデルを動的生成して各レコードのattributeを足しあわせてmd5をとったリストをMySQLとPostgreSQLのそれぞれで作ろう。そのリストのdiffを取って差異がなければ、少なくともActiveRecordレイヤでは同一だよね。

こんな感じの事をする。

MySQL特有の挙動をPostgreSQLにどうやってエミュレートするか

暗黙的なキャストとか、大文字小文字無視の検索とか、ORDER BY時のnullの扱いとか曖昧なGROUP BYとかの事ですね。
ちょっとした挙動の違いだし仕様変更と言えばお客様に許してもらえないかな?クエリを全部検証して書き換える工数を認めてもらえないかな?

残念!それはなかなか通らない。最短の工数で同じ挙動を目指そう。

PostgreSQLは演算子の独自定義ができる。citext型を使えば大文字小文字無視の検索をデフォルトの挙動のように見せることかできる。
色々道を探ろう。それでも対応できない問題は仕方がない、アプリケーションを修正しクエリを書き換えよう。

異種データベース混在期間、システムはどのような構成にするか。その間も保守開発は行う。

この辺りになると、「理屈で考えろ!できるわけないだろうが!」とか言い出すメンバーが出てくる。(私の事です)
理屈で考えよう。現実的にMySQL対応のコードベースとPostgreSQL対応のコードベースやサーバーを独立して保持し、開発していくことは困難だ。
異種データベース混在期間は接続先のDBによってクエリを出しわける機構を組み込んだ、単一のコードベースで管理されているのが最良だ。ActiveRecordは良くできているので、このような要件に簡単に応えてくれる。

賛否が分かれるであろうがシステムはこのような挙動をしなければならない。

データ移行中に対象のテナントで実施できなかったジョブはどのようにリカバリするか

まあ後から実行するしかないよね。事前にお客様には停止時間中の各機能の挙動はどのようになるかを整理してお知らせしておこう。

検討のあとは?

検討し、出てくるであろう課題とその対応方針を洗い出せたら、各課題を解決するためにアプリの修正を進めながら基盤を構築し、移行スケジュールをたてよう。どんな順番で移行を進めたらいいだろう。

今回は管理DBから先に移行することにした。そして、徐々にテナントをPostgreSQLに移していって、最後はMySQLのテナントはゼロだ。図にするとこんな感じ。

スタートはこんな状態

↓最初のリリースでこの状態にして↓

↓移行の開始だ!↓

↓どんどん移行して最後はこうなる↓

どうだろう。物事は単純化すると単純に見える。けれど、問題を整理して課題を洗い出してそのそれぞれについてどう対処するか決定していかないとこんな風にはいかない。

この図を見て「恐ろしいな、えげつないな」と感じたうえで「やるぞ!」と言えるかどうか。僕は今回やるぞ簡単にいうことはできなかった。これはカジュアルではないし、シリアスにとらえたうえで準備を進めてもためらいが出てくるものだ。

まあしかし、仕事というのは一人の中年プログラマが「怖いよう」と震えているだけで中止されるようなものではない。実際にこの計画は実行されたし、細かなトラブルはあったけど全体としては驚くほど成功裏に移行は完了した。

記事の動機

移行が判断されてから検討や修正作業に追われるなか「Railsプロジェクトでこんな困りかたをしているのは弊社だけでは?」という不安が常にあった。検索しても「Rails複数DBシステム異種データベースへの無停止移行のベストプラクティス」なんて出てこないし、そもそも複数DBの時点でRailsのレールは銀河鉄道999のオープニングのように、とっくに飛び越えてる。

一人でやった仕事ではないが、なんとなく世間様はこんなことやらないだろうという孤独感があった。外部に助けを求めても誰も答えを知らないだろう感というか。

今回、複数DBカジュアルの実況やその後公開された資料を眺めてると「こんなことをしてるのはうちだけではなかったし、同じ問題に現在進行形で悩んでいる会社はたくさん有るだろう」と感じた。きっとこの記事があれば、直接の参考にならなかったとしても類似の事例として勇気づけられる人がいるんじゃないかと思う。細かいところが聞きたければコメントいただければ答えられる範囲で答えます。

改めて振り返って文章にまとめてみると技術的に楽しめるところの多い楽しい仕事だったと思う。こういう仕事をしてみたいと思うエンジニアはぜひTECHSCORE運営会社の採用に応募してほしいと思っています。(宣伝です。)

それではまた。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です