spacelyのブログ

Spacely Engineer's Blog

150万レコードを持つ画像テーブルの移行

株式会社スペースリー Railsエンジニアの大津です。

弊社サービスでは物件画像データの管理機能を提供しており、サーバーサイドのフレームワークにはRuby on Railsを、データベース管理にはMySQLを採用しております。これまで物件画像は用途ごとにテーブルを分けて管理していましたが、 アップロードした後に用途を変更できないため画像テーブルを1つに統合しました。 今回は統合する際に実施した旧テーブルから新テーブルへのレコード移行の手順をご紹介します。

統合前後の画像テーブルの構成

まず、統合前の物件画像の管理方法について説明します。 統合前は物件画像テーブルは部屋の写真を管理する内観画像テーブルと建物の写真を管理する外観画像テーブルの2つに分かれていました。 画像ファイルは各テーブルのimageカラムに格納され、Rails GemのCarrierWaveによりAWS S3バケットにアップロードされていました。 また、この過程でサムネイル画像も自動的に生成され、同じくS3バケットに保管されるようになっています。

一方、統合後の画像テーブルの構成は以下の通りです。

画像ファイルは以前と同様にimageに格納されますが、新たにcategoryカラムを追加しました。これにより、各画像が部屋の写真なのか、建物の写真なのかといった用途を柔軟に指定できるようになりました。

レコード移行の課題

画像レコードの移行に当たり、以下の点が課題となりました。

  • 画像レコードは合計150万件あり、レコードの移行に時間がかかる。
  • 画像テーブルの切り替えに際して、サーバーメンテナンス期間は設けない。
  • 統合されたテーブルを利用する新機能のリリース時期が決まっている。
  • 新テーブルでは旧テーブルと異なるサイズのサムネイル用の画像を用意する必要がある。

対応方針

レコード移行する方法として以下の二つを検討しました。

  • Rakeタスクで旧テーブルから新テーブルにレコードをコピーする。
  • AWS BatchでS3の画像ファイルをコピーし、MySQLで直接レコードをコピーする。

その結果、以下の理由からRakeタスクを採用することを決めました。

  • CarrierWaveを使用することで、レコードコピーと同時に新たなサムネイル画像を準備できる。
  • Railsの機能を活用してレコードを作成するため、モデルバリデーションを適用できる。
  • レコードのコピー速度が遅い場合でも、並行処理によってそれをカバーすることが可能。
  • 検証環境での動作確認が容易。

レコード移行のための準備

レコード移行を実施するため以下の準備を行いました。

  • 画像移行Rakeタスクの作成
  • コピー用サーバーの用意

画像移行Rakeタスクの作成

Rakeタスクで旧テーブルのレコードを新テーブルにコピーします。 以下にRakeタスクのサンプルコードを記載します。

namespace :oneshot do
  task :copy_real_estate_inside_images, %i[start_id end_id] => [:environment] do |_task, args|
    puts "### Start copy_real_estate_inside_images"

    raise "引数を入力してください。" if args[:start_id].blank? || args[:end_id].blank?

    real_estate_inside_images =
      RealEstateInsideImage.where(id: args[:start_id]...args[:end_id], real_estate_image_id: nil)

    real_estate_inside_images.find_in_batches do |inside_images|
      attributes = inside_images.map do |inside_image|
        {
          real_estate: inside_image.real_estate,
          title: inside_image.title || "",
          image: inside_image.image,
          category: :interior_and_views
        }
      end

      real_estate_images = RealEstateImage.create(attributes)

      inside_image_ids = inside_images.map(&:id)
      real_estate_image_ids = real_estate_images.map(&:id)
      records = inside_image_ids.zip(real_estate_image_ids).select(&:second)
      update = [
        "real_estate_image_id = ELT( FIELD(real_estate_inside_images.id, ? ), ? )",
        records.map(&:first), records.map(&:second)
      ]

      RealEstateInsideImage.where(id: records.map(&:first)).update_all(update)
    rescue StandardError => e
      puts "エラーが発生しました。inside_image_id: #{inside_images.first&.id}-#{inside_images.last&.id} error: #{e.message}"
    end

    puts "### Finish copy_real_estate_inside_images"
  end
end

大まかな流れと解説です。

  1. Rakeタスクで引数によりIDを指定し、その範囲の旧テーブルのレコードを取得します。
    • 引数でコピー対象の範囲を指定することで、タスクを並列稼働させることが可能になります。
    • 範囲を指定することでRakeタスクの稼働時間を短縮し、万が一AWSなどで障害が発生した場合でも影響範囲を特定しやすくなります。
  2. 旧テーブルのレコード情報をコピーし、その情報を使用して新テーブルのレコードを作成します。
    • CarrierWaveにより画像ファイルがS3にアップロードされます
    • この時点で新しいサムネイル用の画像も同時に生成されます。
  3. コピーが完了した旧テーブルのレコードに新テーブルのレコードIDを保存します。
    • 事前に旧テーブルに新しいIDを保存するカラムを追加しておきます。
    • 旧テーブルのレコードを取得する際にコピー済みのレコードを除外することができます。
    • コピー前後のレコードが紐づくため、正しくコピーされたかを検証できます。

コピー用サーバーの用意

このタスクではRakeタスクを複数並列で実行するため、CPUやメモリなどのサーバーリソースに負荷がかかることが予想されました。そこで本番環境に影響を与えないように、他のサービスが稼働していない独立したサーバーを用意し、そこでRakeタスクを実行しました。

レコード移行の実施

移行のために用意したサーバーで以下のコマンドを実行し、サーバーリソースを監視しながら並列での実行数を調整していきました。

nohup bundle exec rails oneshot:copy_real_estate_inside_images[start_id,end_id] RAILS_ENV=production 2>&1 | tee -a log/copy_real_estate_inside_images.log &

最大で4つのプロセスを並行して実行し、約2週間かけて150万件のレコードを新しいテーブルにコピーしました。 アプリケーションの改修がリリースされるまでは画像ファイルは引き続き旧テーブルにアップロードされるため、リリース前後で再度コピーを行い差分のレコードも新しいテーブルに移行しました。

レコード移行後の検証

移行作業が完了した後は、適切にレコードが新しいテーブルに移行されたか、問題なく動作するか検証しました。

  • 移行後のレコード数が移行前と一致するか
  • 新しいテーブルを参照するように修正したロジックが問題なく動作するか

まとめ

今回は最も確実な方法としてRakeタスクによる移行を採用し、150万件の画像レコードを新テーブルに移行することができました。 今後より件数の多い画像レコードの移行が必要になった際は、AWS Batchを用いた方法も試してみたいと思います。

最後に

spacelyでは一緒に働いてくださる方を募集中です。 詳しくは採用サイトをご覧ください。