Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

差分を管理してデータ更新を高速化する、seed_fu:expressのご紹介

背景

ゲームサーバーで扱うデータはとにかく多いです。 特にイベントなどをほぼ毎日運用する場合はサービス開始から1年でレコード数が無視できない量になり、それに伴って更新に要する時間が激増してしまいます。

以前執筆した9分43秒のデプロイを19秒にした話でもデータ更新を速くする為に変更テーブルをgit差分から取得する方法を紹介しました。 アカツキではデータの追加や管理の為に、seed_fuというデータ更新用Gemを使っています。差分データファイルを検出し、seed_fuに一致するテーブルだけを指定する方式で毎回データベースを更新していました。 しかしseed_fuが何らかの方法で失敗した場合やgitの操作ミスで差分が無くなってしまった場合のリカバリに欠けていたため、結局seed_fuを全てのテーブルで実行し直すという自体が少なからずありました。

外部ツールを追加してこのへんを管理できるかなと思ってしばらく探してみたのですが、gitの中に別のバージョン管理を入れるのはいかがなものかなというのと、運営チームのオペレーションが複雑化するのが懸念されたので一旦考え直すことに。 そこで結局git等の外部ツールに頼らずにseed_fuの実行毎に更新差分だけ自動でテーブルを指定する方法を設計したのでここでご紹介します。

要件定義

  1. 外部ツールに依存しない方法で作成する。
  2. 各テーブルの最新状況を保存できる方法を用意する。
  3. 各テーブルが更新が必要かどうかを判別できる方法を用意する。

実際にやったこと

データベースに新しいテーブルを追加する

まず管理用データの保存先として、データベースに新しいテーブルを追加します。

class CreateSeedCaches < ActiveRecord::Migration
  def change
    create_table :seed_caches do |t|
      t.string :table_name, null: false
      t.string :cache_value, default: "0"

      t.timestamps
    end
  end
end

ここではテーブル名と保存値(cache)を登録できるようにします。timestampsカラムも追加しましたがこちらはデバッグやエラー発生の時に便利だと思っただけなので必須ではありません。実行速度改善のためにindexをtable_nameにつけてもいいですが、テーブルの数はそこまで増えないのでここでは省略しています。

各テーブルの最新情報を保存する

次に更新元となるymlファイルのハッシュ値(md5)を毎回格納するようにします。 各テーブルをフラット化した際のハッシュデータを格納する方法も考えたのですが、オーバーヘッドに時間がかかりすぎたので断念しました。

# ymlファイルからmd5を計算
def yml_to_md5(table)
  Digest::MD5.file("フォルダパス/#{table}.yml").hexdigest
end

# ハッシュ値を更新
def update_cache(tables=[])
  tables.each do |table|
    cache_info = SeedCache.find_or_create_by(table_name: table)
    cache_info.cache_value = yml_to_md5(table)
    cache_info.save!
  end
end

各テーブルが更新が必要かどうかを判別する

ハッシュ値の差分から更新するテーブルを判別して、seed_fuを実行します。

def hoge
  #seed可能なテーブルを全抽出
end

desc "seed_fu with cache mechanism"
  task express: :environment do
    tables = hoge
    targets = [] # 更新対象を登録場所

    tables.each do |table|
      digest = yml_to_md5(table)
      cache_info = SeedCache.find_or_create_by(table_name: table)

      # ハッシュ値が違っていたら更新対象に登録
      if digest != cache_info.cache_value
        targets.append(table)
      end
    end

    # 更新されたテーブルが無い!
    next if targets.size < 1

    # テーブル指定
    ENV['TABLES'] = targets.join(",")
    pp "Executing seed_fu TABLES=#{ENV['TABLES']}"

    # seed_fu呼び出し
    Rake::Task['db:seed_fu'].invoke
  end
end

これでseed_fu高速版、seed_fu:expressの完成です。 が、同僚から「何もないseed_fu実行時もハッシュ値を保存すれば、seed_fu:express移管して直ぐ使えますよね」と提案されたのでseed_fuにenhanceを導入して拡張しました。

Rake::Task['db:seed_fu'].enhance do
  update_cache( ENV["TABLES"].try(:split, ",") || [] )
end

これをするとseed_fuを打てば必ずテーブルキャッシュが更新されるようになります。二回更新が走らないようにseed_fu:expressの最後のキャッシュ更新を削除しましょう。

まとめ

いかがでしたでしょうか? いままで入力ミスで実行に失敗したする度にseed_fuかける必要がなくなりました。 副産物では有りますが、ローカル環境のサーバーブランチを切り替えた後のデータ更新も切り替えが発生したファイルのみが対象になるようになったのでかなり短縮できるようになりました。

seed_fuはRailsエンジニアなら一度は見たことがあるほど有名なGemかと思います。 しかしdb依存する内容であるからなのかキャッシュ化や高速化についてはあまり言及されていないようです。この手の高速化は意外と見つからないのですが、やってみるとそこまで難しくないのだなと思いました。