本番DBと開発DBが乖離した無法地帯の整備

f:id:vasilyjp:20171212135214j:plain

こんにちは。バックエンドエンジニアインターンの田島です。弊社ではIQONの運用を7年間続けています。長年の運用から技術的負債が溜まってきていました。その中の1つに、IQONの本番DBと開発DBの状態が乖離しているという問題があります。この問題をどのように解決したかについて紹介します。

IQONについて

IQONはRuby on Railsで運用されており、以下のような環境で動作しています。

  • Ruby 2.2
  • Rails 4
  • MySQL 5.6

IQONのデータベースについて

IQONではRDBとしてMySQLを利用しています。DBは本番DB、開発DB、テストDBの3種類に分かれています。スキーマ変更の作業はRailsのマイグレーション機能を利用せず、SQLを直接利用して行っています。これは、サービスの大規模化に伴い、マイグレーション機能だけでは要件を担保できなくなったためです。手順は以下のようになっています。

  1. SQLファイルを作成
  2. 1で作成したSQLをdevelopmentDBに反映
  3. db:schema:dump コマンドを利用しschema.rbに変更を反映
  4. 一通り確認
  5. 本番DBに1で作成したSQLをproductionDBに反映

3の手順を含める理由は、CIでのテストDBを作成時にschema.rbからDBを生成しているためです。

抱えていた問題

IQONのデータベースが抱えていた問題として以下が挙げられます。

  1. 本番DBと開発DBのVARIABLESに差異がある
  2. 本番DBと開発DBのストレージエンジンに差異がある
  3. 本番DBと開発DBのCollationに差異のあるテーブルが存在する
  4. 本番DBと開発DBのCollationに差異のあるカラムが存在する
  5. 本番DBと開発DBの型に差異のあるカラムが存在する
  6. 本番DBにしか存在しないテーブルが存在する
  7. 開発DBにしか存在しないテーブルが存在する
  8. 開発DBにしか存在しないカラムが存在する

本番DBと開発DBで多くの差異が発覚しました。この状態では、開発環境では見つからないバグが本番環境に入り込んでしまう恐れがあります。

ゴール

開発DBを以下ような状態にすることを課題解決のゴールに設定しました。

  • 本番DBと同様のテーブル構造になっている
  • 現在の開発DBと同様のデータを持つ

f:id:vasilyjp:20171208150246p:plain

解決手順

解決の大雑把な流れを説明します。最初に開発DBからdumpデータを作成します。本番DBと同じ設定の新開発DBを用意します。最後に、作成したdumpデータ新開発DBに適用するという流れです。

f:id:vasilyjp:20171208150311p:plain

詳細を時系列順に説明していきます。

  1. 空のDB(新開発DB)を用意しVARIABLESを本番DBに合わせる
  2. 本番DBでは存在せず開発DBにのみ存在するカラムを削除する
  3. 本番DBと型に差異のあるテーブルに対して型をキャストし本番に合わせたテーブルを作成
  4. 開発DBのダンプデータ(データのみ)を作成
  5. RailsプロジェクトにActiveRecord::Mysql::Awesomeを適用
  6. 本番環境のDBからschema.rbを作成
  7. 新開発DBに対してschema.rbを適用
  8. 作成しておいたダンプデータを新開発DBに適用
  9. 確認

1.空のDB(新開発DB)を用意しVARIABLESを本番DBに合わせる

空の新開発DBを作成し、VARIABLESを本番DBに合わせます。VARIABLESはMySQLのシステム変数です。文字コードやCollationのデフォルト値などはVARIABLESで定義されているものが使われます。VARIABLESを合わせることによりMySQLの全体的な状態を本番DBと新開発DBで同一にします。

ここでは、問題1が解決されます。

2.本番DBでは存在せず開発DBにのみ存在するカラムを削除する

データの移行に際してMySQLのdumpデータを利用します。開発DBにのみ存在するカラムがあると、新開発DBにdumpデータを適用する時、存在しないカラムへのinsertが発生しエラーとなってしまいます。そのため、dumpデータ作成の前に開発DBにのみ存在するカラムをすべて削除する必要があります。幸いこのようなカラムは少なかったので以下のようなSQLをカラムごとに発行しました。

ALTER TABLE テーブル名 DROP 削除するカラム;

ここでは、問題8が解決されます。

3.本番DBと型に差異のあるテーブルに対して型をキャストし本番に合わせたテーブルを作成

開発DBと新開発DBのカラムの型が違うことにより、新開発DBへのdumpデータ適用時にエラーが発生してしまいます。そこで、開発DBのカラムを本番DBに合わせた型でキャストしたテーブルを作成します。そのテーブルに対しdumpデータを作ることで、型を本番DBに合わせた状態でdumpデータを作成することができます。以下のように型をキャストしたテーブルを作成します。こちらも数が少なかったので、手動で行いました。

CREATE TABLE tmp_テーブル名
SELECT column1, column2, CAST(column5 AS キャストする型) AS column5, column4
FROM テーブル名;

ここでは、問題5が解決されます。

4.開発DBのダンプデータ(データのみ)を作成

f:id:vasilyjp:20171208150331p:plain

やっとダンプデータ作成の段階に入ります。新開発DBではCREATE TABLEの処理を単独で行いたいので、データのみのdumpデータを作成します。dump時に-cオプションを付けることでデータのみdumpファイルを作成できます。また、新開発DBへのdumpデータ適用時に不要なテーブルの除外をしたいです。そこで、テーブルごとにdumpデータを作成します。テーブルごとに1つずつdumpデータを作るのは大変なので、以下のようなスクリプトを利用しました。

#!/bin/bash
MYSQL_USER=''
MYSQL_HOST=''
MYSQL_PASS=''
MYSQL_DBNAME=''

DIR=dump__${MYSQL_DBNAME}
if [ ! -d ${DIR} ] ; then
mkdir ${DIR}
fi

for TABLE in `mysql -u${MYSQL_USER} -p${MYSQL_PASS} -h${MYSQL_HOST} -N -s -e "show tables in ${MYSQL_DBNAME};"`; do
    echo $TABLE
    mysqldump -c -u ${MYSQL_USER} -p${MYSQL_PASS} -h${MYSQL_HOST} -t ${MYSQL_DBNAME} $TABLE > dump__${MYSQL_DBNAME}/$TABLE.sql
done;

これによりテーブルごとにテーブル名.sqlという名前でdumpファイルが作られます。

5.RailsプロジェクトにActiveRecord::Mysql::Awesomeを適用

IQONではRails 4を利用しています。Railsではschema.rbでDBのスキーマ情報を保持することができます。しかし、Rails 4ではCollationやストレージエンジンなどの情報をschema.rbに含めることができません。そこでActiveRecord::Mysql::Awesomeを適用することでこの問題を解決しました。

6.本番環境のDBからschema.rbを作成

f:id:vasilyjp:20171208150350p:plain

本番DBのテーブル状態を新開発DBに合わせるためschema.rbを利用しました。本番DBのテーブル情報をRailsのdb:schema:dumpを利用しschema.rbに反映します。ActiveRecord::Mysql::Awesomeを適用しているのでCollation情報なども正しくschema.rbに反映されます。

7.新開発DBに対してschema.rbを適用

f:id:vasilyjp:20171208150419p:plain

新開発DBに対しdb:schema:loadを利用することでschema.rbの情報からテーブルを作成します。schema.rbは本番DBのテーブル情報を適用しているので、本番と同じテーブル・カラムのみが生成されます。これにより、本番DBと新開発DBのテーブル状態が同一になります。

ここまでで、問題2,3,4,6が解決されます。

8.作成しておいたダンプデータを新開発DBに適用

f:id:vasilyjp:20171208150449p:plain

新開発DBの状態が整ったので元の開発DBのデータを新開発DBに再現します。作成してあったdumpデータをinsertすることでデータを再現します。作成したdumpデータから新開発DBにのみ存在するテーブルのみをinsertします。ここでも、テーブルごとに1つずつdumpデータを適用するのは大変なので以下のようなスクリプトを利用しました。

#!/bin/bash
MYSQL_USER=''
MYSQL_HOST=''
MYSQL_PASS=''
MYSQL_DBNAME=''
TABLES=(本番DBに存在するテーブルのリスト)

for TABLE in ${TABLES[@]}; do
  echo $TABLE
  mysql -u ${MYSQL_USER} -p${MYSQL_PASS} -h${MYSQL_HOST} -t ${MYSQL_DBNAME} < dump__${MYSQL_DBNAME}/$TABLE.sql
done;

ここでは、問題7が解決されます。

9.確認

最後にデータが適切に移行されたか確認をします。確認はそれぞれのテーブルのデータ数を比較することで確認しました。ここでも、テーブルごとに1つずつ確認するのは大変なので以下のようなスクリプトを利用しました。

#!/bin/bash
MYSQL_USER=''
MYSQL_HOST=''
MYSQL_PASS=''
MYSQL_DBNAME=''
TABLES=(新開発DBに存在するテーブルのリスト)

for TABLE in ${TABLES[@]}; do
  echo $TABLE
  mysql -u ${MYSQL_USER} -p${MYSQL_PASS} -h${MYSQL_HOST} -t ${MYSQL_DBNAME} -e"select count(*) from ${TABLE}"
done;

まとめ

今回のメンテナンスで本番DBと開発DBの差異をなくすことに成功しました。今後の課題として、再びDB同士の差異が出ないようにしなければなりません。そのために、デプロイ前にDB同士での差異がないかの確認の自動化等を検討しています。また、データベースだけでなく改善すべきところはたくさんあります。今後も技術的負債を精算し、より安定しスピード感あるアプリケーション開発めざしていきます。

終わりに

VASILYではアプリケーションの側から開発の安定化、効率化を図れるエンジニアを募集しています。興味がありましたら、以下のリンクからご応募ください。

https://www.wantedly.com/projects/61389www.wantedly.com

カテゴリー