技術部の鈴木 (@eagletmt) です。
先日、クックパッドで使われている Ruby のバージョンを 2.0.0 から 2.2 にアップグレードしました。 アップグレードは主に @sorah と私で進めました。 今回はアップグレードまでの過程やアップグレード当日の流れ、そして今のところ見られているアップグレードによる効果などについて紹介します。
アップグレードまでの準備
テストを通す
Ruby 2.1 がリリースされたときから 2.1 にアップグレードできないか検証環境でテストを回していました。 しかし、当時はクックパッドの全テストを実行すると必ず途中で Ruby がクラッシュする現象に悩まされていました。 Ruby の GC のバグ、拡張ライブラリのバグを疑いながら色々やってみたものの結局解決できず、Ruby 2.2 がリリースされてからもこの状況は改善されませんでした。
しかしあるとき、たまたま通常の CI と全く同じ環境でテストを実行したところ、いくつかのテストは失敗したものの Ruby がクラッシュすることなく完走しました。
テストが通るようにアプリケーションコードを直しつつ、なぜ通常の CI 環境ではクラッシュせず検証環境ではクラッシュしたのか比較しているうちに、
どうやらマシンスタックのサイズが影響していることがわかりました。
検証環境では Ruby がクラッシュした場合に調査しやすくするため、最適化オプションを切ったりデバッグ用のフラグをつけて Ruby をコンパイルしており、その状態だとクラッシュするようです。
本番や通常の CI では最適化オプションを有効化してコンパイルした Ruby を使っています。
また、RUBY_THREAD_MACHINE_STACK_SIZE
を大きめの値に設定すると、検証環境の Ruby 2.2 でもクラッシュせずにテストが完走することが後からわかりました。
長いことクラッシュに悩まされてきたけれども、実は本番で使われている Ruby ではクラッシュしないことがわかってからは、地道にテストの失敗を修正していきました。
といっても REE から 2.0.0 に上げたときのような苦労 はなく、2.0.0 でも 2.2 でも動くようなコードに直すことは簡単で、
RUBY_VERSION
や respond_to?
で分岐するコードは書かずに済みました。
具体的には以下のような修正を行いました。
Time.parse(date, now)
の now に Date オブジェクトを渡していた- Ruby 2.2 からは now は Time オブジェクトであることが必須になったようなので、Time に変換して渡すようにしました。
- nil に値を書き込んでいた
- Ruby 2.2 から true/false/nil が freeze されるようになりました。
nil.extend(SomeModule)
ということをしていて、SomeModule によってattr_accessor
が追加され、そこに値を書き込もうとしてエラーになっていました。- これは nil が返るべきではないところで nil が返っていたことで発生していたので、テストコードを修正しました。
- ハッシュリテラルのキーが重複していた
- Ruby 2.2 から警告が表示されるようになりました。
- 依存 gem も含めたクックパッドのコードベース内で、おかしなハッシュリテラルを結構発見することができました。
実際に対応が必要だったのはこの程度だったので、Ruby 2.2 でテストを実行する CI ジョブを設定し、一日に一回実行して確認する程度で十分回っていました。
本番での検証
テストが通るようになってからは、少数の app サーバで Ruby 2.2 にして一時的にサービスインして、エラーやパフォーマンスをチェックしました。
このとき、キャッシュが混ざらないように、dalli の namespace に Ruby バージョンを含めるような工夫をしています。
dalli を使ってキャッシュする場合は Marshal.dump
した値を memcached に保存するため、もし Ruby のバージョンによって Marshal のフォーマットが変わっていた場合、不整合が発生してしまうためです。
また、もし Ruby のアップグレードによる不具合が発生し誤った値がキャッシュに書き込まれてしまった場合に、影響範囲を限定する目的もあります。
Ruby や Rails のバージョンアップのような影響範囲が広くエラーが予測しにくい場合、いつもは数時間ほどサービスインしてエラーとパフォーマンスを確認していますが、
今回は Ruby がクラッシュする懸念があったため、数日間サービスインしたままにしてクラッシュしないことを確認していました。
アップグレード
クックパッドは http://cookpad.com というウェブサイトだけでなく、スマートフォンアプリ向けの API サーバやガラケー向けサイトのモバれぴも提供しています。 この中でウェブサイトと API サーバはとくにサーバ台数が多く影響も大きいため、アップグレード時には app サーバを新規に用意し、そこで Ruby のバージョンを上げ、 リリース時にはロードバランサの設定を切り替える、という方法をとりました。 最初は全体の 50% が Ruby 2.2 になるようにし、その後様子を見ながら 70%、100% と Ruby 2.2 の割合を上げていきました。
他のモバれぴのように比較的サーバ台数が少ないサービスでは、一台ずつ Ruby のバージョンを上げていきました。
今回のアップグレードでは、スタッフ専用のページでエラーが一件発生しただけで、他のエラーやパフォーマンス上の問題などは発生しませんでした。
Ruby 2.2 による効果
まだアップグレードしてからあまり日がたってないので正確なことは言えないのですが、アプリケーションの応答速度が改善しました。 クックパッドではアプリケーションのパフォーマンスの指標の一つとして X-Runtime を記録しています。 その一日の平均値を比較するとおよそ 5% から 10% ほど改善していました。
アプリケーションコードを書く上で便利なものとしては、キーワード引数のデフォルト値を省略できるようになったことが大きそうです。 キーワード引数の文法は 2.0.0 で追加されましたが、2.0.0 では常にデフォルト値を指定する必要があり、必須のパラメータを渡すときには使えませんでした。
Ruby 2.1 以降はデバッグやプロファイリング関連の機能が強化されています。 それを利用した gem が既にいくつもあるので、それらを利用することでアプリケーションのパフォーマンス改善をしやすくなるのではないかと思います。
- https://github.com/tmm1/stackprof
- https://github.com/tmm1/gctools
- https://github.com/ko1/gc_tracer
- https://github.com/ko1/allocation_tracer
まとめ
クックパッドの本番環境で Ruby 2.2 が使われるようになるまでの過程について紹介しました。 最新の Ruby が一番いい Ruby なので、できるだけ最新の Ruby を使うようにして開発者とユーザ双方がより幸せになるようにしていきたいです。