この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
いちいち本番で1からデータを作る必要なんてありません。既存seedに時間がかかるならまず使うことをやめましょう。DBのダンプと差分管理と並列処理で本番へのデータ投入を爆速にするのです。
はじめに
みなさま、お久しぶりです。新卒エンジニアのくろすです。
素手人間になって久しいですが、たまーにオレンジ折りたくなる衝動に襲われます。
今年こそは一番綺麗な空を飛べると信じて初日参加します。
推しが出るので2日目も参加します。
3日目はチケットがありません。チケットが、ありません。
データ投入用高速seed
localでダンプしたデータを差分管理しつつ直接本番に流し込める高速seedをyaml_dbとparallelを用いて作成したのでその紹介です。
差分を管理してデータ更新を高速化する、seed_fu:expressのご紹介
を参考に差分管理を実装し、
RailsのDBの初期データ(rake db:seed用)をyamlで美しく管理する方法
をヒントにデータをyaml_dbで管理することを思いつきました。
実際にはテーブル毎のダンプが出来さえすればいいのでmysqldump等でも問題ないと思いますが、sql管理に比べデータの視認性が良い、手修正がしやすいという2点からyaml_dbを使っています。
まだブロックスタイルでしかダンプできていないのであまり使い勝手はよくありませんが、フロースタイルでダンプできるようにできれば手修正はより容易になると思います。
背景
弊社ではExcelで作成していることが多いゲームのマスターデータですが、これはそのまま使うものもあればdbに投入する際にプログラム側で使いやすくするため加工するものもあります。
このデータを加工する作業というものは、ゲーム運営が長くなりデータが増えていくに従い時間を食う作業になっていきます。
また、このデータの作成に他のモデルを参照しながらデータを作るような書き方をしていると各masterに依存関係が生まれ並列処理できないため、既存seedプログラムにがっつり手を入れなければ高速化のしようがない…なんてこともあります。
新卒としてアピリッツに入社しrubyを書き慣れるにつれ、DRYに精神を乗っ取られ始めました……
スゥパァドゥルァァァァイ
今回の場合Don’t Repeat Ourselfでいきましょう。
なにもデータ作成までDBサーバーで行う必要なんてどこにもありません。
データ作成はlocalのみで行い本番はそのデータだけを流し込んではいけない決まりなんてありません。
すっぱりきっぱりと既存のseedを捨て去る覚悟をしましょう。
投入用データ作成
とりあえずdb:seedの後にダンプを行うrake taskを作ります。
namespace :gundum do
desc "gundum:seed"
task :seed => :environment do
Rake::Task["db:seed"].invoke
Rake::Task["db:dump_masters"].invoke
end
end
できました!!!ガンドゥムSEEDです!!!!
この高速seedを作ってる時にseedにinvokeを生やそうと[ seed invoke ]でググってしまったあの日からこれをやろうと心に決めていました。
さて、ここで呼んでいるdb:dump_mastersですが中身はyaml_dbの拡張である以下のプログラムをロードするようになっています。
module YamlDb
module SerializationHelper
class Base
# @Override
def dump_to_dir(dirname, *dump_table_names)
Dir.mkdir(dirname) if Dir[dirname].empty?
tables = if dump_table_names.present?
dump_table_names
else
@dumper.tables
end
tables.each do |table|
File.open("#{dirname}/#{table}.#{@extension}", "w") do |io|
@dumper.before_table(io, table)
@dumper.dump_table io, table
@dumper.after_table(io, table)
end
end
end
end
end
module RakeTasks
MASTER_TABLES = ["fizz_master", "buzz_master"].freeze
def self.dump_masters
SerializationHelper::Base.new(helper).dump_to_dir("#{Rails.root}/db/masters/yml", *MASTER_TABLES)
end
end
end
#--------------------------
YamlDb::RakeTasks.dump_masters
テーブルを指定したダンプが行えるよう拡張して、マスターのみダンプする新しいRakeTaskを追加しています。
データ投入と差分管理
一番最初に紹介した
差分を管理してデータ更新を高速化する、seed_fu:expressのご紹介
を参考に差分管理しつつデータ投入を行えるようyaml_dbを拡張していきます。
require 'parallel'
module YamlDb
module SerializationHelper
class Base
def load_masters_from_dir(dirname, truncate = true)
updated_masters = []
Dir.entries(dirname).each do |filename|
next if filename =~ /^[.]/
master = MasterVersion.find_or_create_by(master: File.basename(filename, ".yml"))
checksum = Digest::MD5.file("#{dirname}/#{filename}").to_s
if master.chesksum == checksum
p "✔ "
else
p "✖"
updated_masters << master
end
end
if updated_masters.present?
Parallel.each(updated_masters) do |master|
filename = "#{master.name}.yml"
begin
ActiveRecord::Base.transaction do
@loader.load(File.new("#{dirname}"/#{filename}", "r"), truncate)
master.save!
end
rescue => e
p "✖"
raise e
end
p "✔ "
end
end
end
end
end
module RakeTasks
def self.load_masters
SerializationHelper::Base.new(helper).load_masters_from_dir(dump_dir("/masters/yml"))
end
end
end
#--------------------------
YamlDb::RakeTasks.load_masters
テーブル情報の管理にMD5を使っています。
MD5の衝突耐性は容易に突破され、ハッシュ値から入力値を求めることができるようになっていますが、テーブル管理に使うくらいなら問題ないでしょう。
手元のファイルからハッシュ値を計算しているため、管理対象のテーブルがタイムスタンプを持っている場合は削除しておきましょう。
seedで更新されるデータのの更新時間なんてそんな使うもんじゃありません。
後はこれを標準のrakeから使えるようにrake taskを作ります。
namespace :gundum do
desc "gundum:seed_destiny"
task :seed_destiny => :environment do
load "gundum/seed_destiny.rb"
end
end
U・N・M・E・I
感じちゃいますね
まとめ
今までローカル反映にすら15分ほどかかっていましたが、一度誰かが取り込み済みのデータなら全データ入れ替えても1分30秒で済むようになりました。
既存seedが遅い原因は富豪プログラミングによるString.newが走りまくることだったので、それも解決。
結果としてデータ入れ替えながらの作業が捗るようになりました。
gundum:seed_destiny最高ですね。