読者です 読者をやめる 読者になる 読者になる

開発環境のデータをできるだけ本番に近づける

こんにちは。技術部の吉川です。 今回はクックパッドの開発環境構成、特に開発用データベースの構成についてご紹介します。

開発環境の構成

クックパッドのシステム環境は以下のようなフェイズに分かれています。

f:id:adorechic:20141002194317p:plain

※ これはcookpad.comの構成で、サブシステムや個別のサービスはその規模や特性に応じて構成が異なります。

development

開発者が実際に開発を行う環境です。クックパッドでは仮想環境は用いず、手元のマシンでRailsアプリケーションを動かして開発を行っています。 データベースはローカルではなく、開発者全体で共通の開発用データベースに接続しています。

test

手元でテストを実行する場合は、ローカルマシンのデータベースを利用します。CI(rrrspec)などの場合も同様で、テスト実行サーバーのデータベースが利用されます。

staging

stagingといえば準本番環境として、本番前の最終確認用の環境として位置づけられるのが一般的かと思います。 クックパッドでは後述するproduction-test環境がそれに近い位置づけで、stagingは少し独特な使い方をしています。

開発者は任意のブランチを独立した専用のインスタンスに対して任意のホスト名でデプロイすることができます。

一般的な利用方法としては、開発者のフォークリポジトリの特定のブランチをデプロイして利用します。 指定したホスト名が社内からアクセスできるURLになるため、あるチーム専用のクックパッド環境ができあがります。

通常の開発フローでは、stagingへのデプロイは必須ではありません。基本的にはローカルマシンで動作確認し、またchankoを利用してスタッフベータ公開とすることでproductionで動作確認を行います。

大きめの機能をチームで開発している場合などに、マージ前に非開発者を含めたチーム全員で動作確認したい場合や、 あるいは連携するシステムからのAPIコールを受けたい場合に、常時利用できる環境としてstaging環境が利用されています。

なおこのstaging環境もdevelopment環境と同じ共通の開発用DBに接続されています。

production-test

準本番環境・・・というかデータベースはproductionのものなので実アクセスから切り離された本番環境、といった方が正しいかもしれません。 CIを通ったリビジョンは自動的にこのproduction-testにデプロイされ、常に最新のリビジョンがデプロイされた状態です。 productionへのデプロイ前に開発者はここで最終確認を行います。

stating-dbとproduction-dbの同期

説明した通り、クックパッドでは手元で開発する場合も共通の開発用データベースを利用しています。 しかし一般的には手元の開発環境でのデータベースはローカルのデータベースに接続する構成が多いのではないでしょうか。

クックパッドの開発用データベースは、productionのデータベースと同期されています。 手元で動かせば、常に本番の最新のレシピが表示されるようになっているのです。

本番と同等のデータで開発することで、どういったメリットがあるのでしょうか?

ユーザーと同等の体験をして開発する

手元でテストする場合には”test” のような適当な入力でテストしたり、それらしいデータを自動生成して用意しておく場合が多いのではないでしょうか。 しかしそれはユーザーが実際に体験するクックパッドではないのです。

"testtesttest..." のようなレシピが並ぶのと、実際にユーザーが投稿してくれたレシピが並ぶのでは、見た目の印象や感じ方だけではなく、操作感も変わってきます。 なるべく本番と同じ状態のクックパッドで開発することで、ユーザーと同等の体験のもとに開発ができるのです。

予期せぬデータによるバグに気づきやすい

前述したようなテストデータはあくまで開発者が予期したデータにすぎません。実際のデータはもっと複雑で、想定していないデータが存在してバグの原因になったりします。 開発の時点で本番データで開発を行っていれば、そういった環境間の差異による問題に気づきやすくなります。

重いクエリに気づきやすい

indexなどに気を使っていても、本番と開発環境でcardinalityが異なっていて思った通りにindexが働かなかった、ということはないでしょうか。 事前に予期できていれば検証環境を用意するといったこともできると思いますが、副次的に発生したクエリだったりすると本番にデプロイして初めて気づくということもあるのではないでしょうか。

これも初めから本番データで開発していれば、本番で重いクエリは手元で動かしても重いため、すぐに気がつけます。 また開発者全体でデータベースを共有しているため、たまに誰かが気付かず重いクエリを発行していると他の誰かが気づいたりします。

本番同期の実現方法

本番と同期するためにMySQLのレプリケーションを利用しています。 staging-dbはproduction-dbからレプリケーションしており、その状態でstaging-dbへの開発用書き込み・参照を行っています。

とはいえ、実際にはこれを実現するためには様々な問題があります。

更新時のコンフリクト

slaveに書き込んだら速攻でレプリが止まるのでは・・・と思いきや、クエリベースのレプリケーションを行っていると案外止まりません。 主キーがAUTO_INCREMENTなテーブルに対してそれぞれでINSERTが走ってもコンフリクトしないのです。 しかし実際にはまともに動作しません。INSERTに成功していても、コンフリクトしたレコードの主キーはproduction-dbとstaging-dbで異なってしまいます。 あるいはstaging-dbでINSERTすると、同じidに対するUPDATE文がレプリされてくるため、何も触ってないのに勝手にデータが変更されていきます。 production-dbとstaging-dbで対象データに齟齬が生じてしまっているわけです。

そこでMySQLの行ベースレプリを利用します。行レプリであればレコードが正確に複製されるため、コンフリクトしたINSERTは失敗します。 失敗されては困るので、あわせてstaging-dbのテーブルはAUTO_INCREMENTのオフセットをidが衝突しない程度に進めておきます。

production-dbではidが1, 2, 3…と進み、同じidを持つレコードがstaging-dbに複製されていきます。 オフセットを仮に600000000にしていた場合、開発環境からINSERTされたレコードのidは600000001, 600000002, 600000003… という風に進みます。 これでコンフリクトせず、本番データと開発データが共存できます。

ちなみに社内でのスキーマ管理はRidgepoleを利用していますが、 これを用いてテーブル作成するとこのAUTO_INCREMENTのオフセット設定は自動でやってくれるようになっています。

なお通常のmaster-slave間でのレプリケーションにはステートメントベースレプリケーションを利用しているため、そのままでは行レプリできません。 そこでproduction-db と staging-dbの間に1台コンバーターを挟んで、そこで行レプリ用のバイナリログを出力するようにしています。

f:id:adorechic:20141002194340p:plain

テーブル定義のコンフリクト

行レプリを使って、レコードの更新がコンフリクトしないようにすることができました。 一方でカラムが削除されて開発環境にはなかったり、カラム名が変更されたりして本番と開発環境でテーブル定義が異なっていると行レプリといえどもさすがにレプリケーションできません。

しかし本番と開発環境でテーブル定義が異なる状態は通常一時的なものです。もし長期間テーブル定義が異なったままの状態であるとしたらそれは健全な状態ではありません。 そこでその一時については目をつぶって、コンフリクトした場合はその更新をスキップするようにしています。

具体的にはstaging-db監視デーモンがレプリケーション状態を常に監視しており、もしレプリケーションが停止したらそのクエリをスキップして再開させるようになっています。

MySQLならslave_skip_errorsでよいのでは?と思った方もいらっしゃるでしょう。実はslave_skip_errorsではスキップできないケースがあるのです。行レプリを使っている場合に発生する1677エラーなどがそれです。というか監視デーモンの目的はほぼこの1677エラーをスキップするのが目的です。

セキュリティ

開発環境のデータベースはテーブル追加など開発者が自由に行えるようになっています。ということは強めの権限が開放されているということです。 ローカルだけで閉じていれば問題ないのですが、本番のデータが入ってくるとなると気になるのはセキュアな情報です。

クリティカルな個人情報はそれを扱う専用のバックエンドシステムがあるのですが、そこまでではないがセキュアなデータに関しては社内で命名規則があり、 それに該当するデータは自動でフィルタリングされstaging-dbにはレプリケーションされないようになっています。

なお余談ですが、社内ではこの命名規則に従っていればログ出力などが自動でフィルターされるgemが利用されています。

まとめ

クックパッドにおける開発環境構成と、開発用データベースを本番データベースと同期する方法について紹介しました。 本番環境にできるだけ近づけることで、よりユーザーに近い目線で開発することができます。

本番構成や開発フローについてはよく紹介にあがりますが、こういった開発環境の構成は意外と情報が少なかったりするので、参考となれば幸いです。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/