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

クックパッドにおける最近のActiveRecord運用事情

インフラストラクチャー部の成田(@mirakui)です。

Rails の OR マッパーである ActiveRecord ですが、みなさんどのように運用していますか?

ActiveRecord を使うと、 SQL を直接扱うことなく、抽象化された表現で RDB にアクセスできるので、アプリケーションの開発効率という観点ではメリットが大きいです。

一方で、 ActiveRecord が駆使されているアプリケーションをサーバに配置してプロダクションとして運用する立場からすると、いくつかの問題に突き当たります。

まずはクックパッド本体アプリケーションにおける、最新の rake stats をご覧ください。

+----------------------+-------+-------+---------+---------+-----+-------+
| Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
...
| Models               | 87190 | 68871 |    1525 |    7568 |   4 |     7 |
...
+----------------------+-------+-------+---------+---------+-----+-------+

今回は、このような規模で ActiveRecord を運用しているエンジニアという立場から、いくつかの知見を紹介します。

なお、この記事を執筆している現在は、クックパッド本体アプリケーションは Rails 4.1 で動いているので、 ActiveRecord 4.1 系の事情として読んでいただけると幸いです。

マイグレーションを使わない

クックパッドの本体アプリケーションでは標準のマイグレーションを使っていません。

クックパッドでは数十名のエンジニアが、ほとんどの人はお互いにどんな作業をしているかを知らずに、一つの巨大なモノリシックアプリケーションを開発しています。テーブル追加やカラム変更も毎日何回も行われています。Rails 標準のマイグレーションの仕組みでは、schema_migrations は1アプリケーションごとに1つしか持てないため、われわれのような状況ではマイグレーションファイルを全員で共有し、互いの作業をブロックせず運用するのはなかなか難しいと考えています。

また、Rails のマイグレーションは db:migrate をデプロイ時に実行するのが基本ですが、ALTER TABLE は、ものによっては DB サーバの負荷が高い場合があり、必ずしもデプロイ時に実行するのが好ましいとは限りません。そこで、サーバ負荷をリアルタイムに把握している、私の所属するインフラストラクチャー部のエンジニアが実行タイミングをコントロールしたいという要求もあります。

アプリケーションの分割

こうした状況を解決する方法のひとつは、1チーム1アプリケーションになるように、マイクロサービスにアプリケーションを分割することです。 アプリケーションが単機能であり、それに関わっている開発者も数名という状況であれば、マイグレーションはうまく機能することが分かっています。クックパッドでも、本体以外のいくつかのアプリケーションでは、デプロイ時に db:migrate する、普通の Rails のやり方で開発されています。

一部の機能はマイクロサービス化され、クックパッド本体からは分離されましたが、依然としてクックパッド本体については巨大なモノリシックアプリケーションです。 このような開発状況で効率的にスキーマを共有し、運用するために、クックパッドでは、古くからさまざまなスキーマ管理ツールを作ったりしてきましたが、2014年の現在は Ridgepole を使っています。

Ridgepole

Ridgepole は弊社の菅原が開発した、スキーマ管理用のコマンドラインツールです。 Ridgepole では Schemafile というファイルに Ruby DSL でテーブル定義を書き、 ridgepole --apply コマンドでそれをデータベースに反映します。

Ridgepole の記法や使い方は、以下の github の README を参照してください。

https://github.com/winebarrel/ridgepole

Ridgepole の DSL 自体は ActiveRecord のマイグレーションと同じものですが、 Ridgepole が優れているのは、カラムを追加したければ、カラム追加のマイグレーションを書くわけではなく、テーブル定義にカラム定義を一行足すだけでいいという点です。

ridgepole --apply コマンドを実行したとき、 Ridgepole は Schemafile に書かれたテーブル定義を、実際の DB に接続して DB 上のテーブル定義と比較します。もし Schemafile に書かれた定義と DB 上の定義に差分があれば、その差分だけが ALTER TABLE 文となって、DB に適用されます。

つまり、 Ridgepole におけるテーブル定義には、冪等性が保証されています。

現在、Ridgepole を以下のようなフローで運用しています。

  1. 開発者は全開発者共有の開発用DBに、自由に ALTER TABLE を発行し、スキーマを試行錯誤する。
  2. スキーマが決まったら、そのスキーマを ridgepole --export して Schemafile に出力し、インフラ部に pull request する。
  3. Schemafile への pull request を Jenkins がフックし、本番と近い性能の DB サーバに対して実際に ALTER TABLE が発行される。もし実行エラーになったり、ALTER TABLE に時間がかかった場合、CI は Fail する。
  4. インフラ部のエンジニアは、CIの結果およびスキーマの差分を見て、レビューを行う。問題なければマージし、本番 DB に対して ridgepole --apply を実行する。つまり、この時点で本番に ALTER TABLE が発行される。

このフローでは特に CI で ALTER TABLE の実行時間を計測しているのが特徴的だと思います。この CI に利用される DB サーバは、最新の DB スナップショットから Jenkins がビルドのたびに kumogata で起動しています。

ただ、マイグレーションについては Rails を導入した 2008 年の時点で早々に諦めているので、会社としてはモダンなマイグレーション運用ノウハウがほとんどないというのが実情です。最近のマイグレーションなら本当はうまく回せるのかもしれないので、ある程度の規模でも標準のマイグレーションで問題なくやっているという事例がありましたら教えていただけると助かります。

複数 DB、R/W splitting

ActiveRecord で多くの人が困っているのは、複数の DB サーバをうまく扱えないという点じゃないでしょうか。

そのわりに ActiveRecord 関係の拡張 gem はあまり状況がよくなく、2014年現在、有名なものの中でアクティブに更新されている gem は octopus くらいです。なにしろ Rails 側の更新が激しいのでついていくのが大変というのが大きいと思います。 クックパッドでは古くから acts_as_readonlyable を使ってきたのですが、この gem は 2007 年の Rails 1.2 (!)対応を最後にメンテナンスされていないため、クックパッドでは原形をとどめないほどのモンキーパッチを当ててなんとか Rails 3.2 まで持ちこたえてきました。

しかしこのたび Rails 4 にアップデートする際に、さすがにやってられなくなったので、弊社の @eagletmt が開発した switch_point に移行しました。

acts_as_readonlyable をクックパッドで6年も使うことができたのは、もとはたった100行前後のシンプルなコードだったという点が大きいと考えています。octopus や db_charmer などはコードの規模が大きいため、こういったライブラリがネックで Rails のバージョンが上げられないということも起こりうるでしょう。

switch_point は、今後の Rails アップデートにも追従しやすいように、シンプルなコードで、クックパッドに必要最小限の機能を ActiveRecord に追加しています。

クックパッドで必要としているのは以下の要件です。

  • 1アプリケーションで複数のDBを扱う必要がある
  • 複数のDBがそれぞれマスタースレーブ構成になっていて、書き込みはマスター、読み込みはスレーブに振り分ける必要がある(R/W Splitting)
  • マイグレーションは不要
  • シャーディングは不要(いまのところ)

現在、クックパッドでは、本体アプリケーションおよび、いくつかのサービスがこの switch_point を使っています。

https://github.com/eagletmt/switch_point

今後の課題

今後の運用上の課題として、コネクションプーリングをどうしていくかという点が挙げられます。

ActiveRecord にはコネクションプーリングの仕組みがあります。デフォルトでは、Rails 1プロセスあたり最大5本のコネクションを1つの RDB に張りっぱなしにし、使いまわします。これは接続時のコストを下げるためのものですが、 複数スレーブに負荷分散したり、MHA でフェイルオーバーしたりする場合には相性が悪いです。 しかも ActiveRecord 標準の reconnect 機能が MHA でうまく働かないので、クックパッドでは gem で拡張してうまく再接続されるようにしたりしています。

http://so-wh.at/entry/2014/01/05/activerecord-mysql-reconnect_0.2.0

負荷的に問題ないことが前提ですが、個人的にはコネクションプーリングを無効化したほうが都合がいいかもしれないと考えています。ただし、無効化する機能は ActiveRecord にはなく、めぼしい gem もまだ見つけられていないので、なんらかの gem を作ることになるかもしれません。また、Amazon RDS を使っていると、DNS ベースのフェイルオーバーなので、名前の再解決もよしなにやらなければならず、なかなか難易度が高まります。

MHA や RDS を Rails からうまく扱えているという皆様、情報をお待ちしております。

追記 2014/08/28

@sonots 先生からコネクションプーリング切るやつを教えていただきました。

[Ruby] 例えば ActiveRecord の connection_pool を止める - sonots:blog

sonots/activerecord-refresh_connection

コードを見るとかなりシンプルで、 rack middleware でリクエストのたびに ActiveRecord::Base.clear_all_connections! しているだけでした。これならとてもシンプルですし、 DeNA 社さんの ActiveRecord 4.1 + switch_point な環境で運用実績があるとのことなので、弊社でも使っていこうと思います! ありがとうございます!

/* */ @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;*/ /*}*/