Railsアプリつくった

引用元: 秒速5センチメートル (監督: 新海誠)

最近APIサーバ用途でRailsアプリを1個つくったので振り返る。

概要

接続元はiOSやAndroidアプリとか、Webブラウザとか、別のWebアプリケーションとか。1ホストあたり秒間数百リクエスト、平均応答時間10msぐらい。Rails 4.1.0.rc2、Unicorn、Nginxを使ってる。正直Railsは全部入りで重いイメージがあったので何となく平均50ms以内程度であれば良いところだろうと思ってたけど、意外と速い。多分そもそもサーバの性能が良いんだと思う。実装時に気を付けたことは普段の開発と特に変わりない。いつもは大勢でワイワイ開発するものに少し手を加えるということが多いんだけど、今回は珍しく自分一人でつくったから目が行き届いてたのかもしれない。DBへの問合せの効率に気を配るとか、Rubyでの処理の無駄を省くとか、アプリケーションのプロセスに無駄なコードを読み込ませないとか、計測するとか。

インデックス

DBへの問合せで気を付けると言っても、検索用インデックスが効率的に使われているかどうかと、クエリの内容が意図的かどうかを確認するだけ。今回はMySQLを利用していたから、データの更新時に検索用インデックスが生成されているかどうかと、その検索用インデックスを利用できるような問合せ方法になっているかというところ。これをRailsで効率的に調べるために、ActiveRecordに少し (3行ぐらい) 手を加えて、ある環境変数を指定した場合に全てのSELECTクエリに対してExplainで実行効率を検査し、ログに出力するようにした。これではリクエストが来るまで実行されないので、典型的なリクエストを幾つか送信するEnd-to-Endのテストを記述した。Explainの結果はテーブル内のデータ量によって変わるので、テストでは予め幾つかのダミーデータを入れておく。これでテストを実行することでExplainの結果が確認できるようになった。この手のテストは本来のテストの用途としても役に立つため、実装効率が良い。

クエリ

細かいところだと、例えばActiveRecordを利用していると何も指定しない場合全てのカラムの内容を取得しようとするが、必要なカラムのみ都度指定すると実行速度が向上する場合もある。例えば検索インデックスが用意されているカラムしか必要が無い場合、検索インデックスを探索するだけで結果が返せるため、処理が高速になるという具合。他に、pluckメソッドやinverse_ofオプションなどを使うと効率的な処理を短いコードで実現できるようになるので、知っていなくても実装こそできるが知っているとお得。またActiveRecordが非効率なクエリを生成する場合もある (validate uniquenessなど) ので、クエリをよく見て不十分であれば自分で指定して書き換えると良い。

Memoize

若干気を付けたのはmemoizeと呼ばれる類の処理。これも今更言語化して説明するほどのことでとないが、一度計算した結果をそのオブジェクトのインスタンス変数などに入れておくことで、再び計算結果が必要になった際に再度計算を行うコストを省くというもの。例えばスペース区切りで受け取った文字列を配列に変換して利用するような場合、ローカル変数に一旦格納しておけば複数箇所から変換結果を参照できるが、これでは複数箇所から参照する必要性からローカル変数のスコープが長くなり1つのメソッドあたりの行数が増えてしまう。計算効率を向上させるために意図のわかりづらいコードになるという事態は避けたい。今回のアプリケーションではmemというgemを使い、インスタンスメソッド単位で実行結果を保存しておくことで、他の部分のコードに変更を加えることなく計算効率の向上を図った。

メソッドの分割単位

話が逸れるけど、自分はコードをごく微小な単位でメソッドに分けていく傾向があって、結果的に各メソッドの中身の行数が1行ずつになるということも少なくない。単一の箇所からしか呼ばれない場合でもメソッドに分割してる。Rubyではテストしたり名前を付けたりできる最小のコードの単位がメソッド。メソッドに分けることで名前を付けられ、読者に意図を伝えることができるし、前述したmemoizeなどもメソッド単位で指定できる。ある処理をテストのために偽装したいという場合も、指定の単位はメソッド。パフォーマンス測定やメトリクスの測定、エラーのトレースなどでもメソッドが最小単位となるため、それが意味のある最小の単位で分割されていることには利点がある。メソッドの呼出コストが問題になる場合はRubyを使わない。少し話は違うけど、細かなインスタンスメソッドに分けていこうという記事を見かけたことがあるので置いときます。http://blog.codeclimate.com/blog/2012/11/14/why-ruby-class-methods-resist-refactoring/

Metal

他の性能改善策として、RailsのMetalを使っている。Railsのコントローラの親クラスには普通ActionController::Baseが使われるが、その更に先祖のクラスであるMetalを継承して実装することで、本来不要なmoduleをincludeするのを避けられる。あまり意味はなさそうだが、クラスの継承ツリーから不要なmoduleを削除することで、メソッドや定数の探索コスト、メモリの使用量などを抑える効果がある。この改善は恐らく微々たるものだろうが、特にAPIなどの用途が限定されたアプリケーションでは簡単に実装できるしやっておいてあまり損はない。

View

RailsがHTMLを描画する処理は遅い。

計測

計測の方法は、本番環境にNew Relicを入れる、リリース前にjmeterやabなどで簡単に速度や負荷のテストをする、開発時にrack-miniprofilerを使うなどの施策を行った。jmeterやabを利用するのは正直面倒だし手間が掛かる上、あまり効果は得られない。それでも何か問題があった場合にはそこで気付けていたと思う。今回は問題が無かった。New RelicはProライセンスを利用しているけどかなり良い。運用の途中から導入したが、最初から入れておけば良かったと思った。入れた日に改善点が見つかってパフォーマンスが劇的に改善された。各エンドポイントごとに、どのSQLにどれくらいの時間を費やしているのか、どのメソッドに時間が掛かっているのか、リクエスト中にサーバ間でAPI通信を行っている場合は相手のサーバでどれくらいの時間がかかっているのか、いつデプロイがあったのか、先週と比べてどのくらい違いがあるのか、等などの情報が見られる。ボタンを押すと数分間Rubyのプロファイラを実行して結果を報告してくれるような便利機能もある。

テスト

ちゃんとテストされていないと性能改善はできない。これは性能改善のための変更で壊れる可能性があるから。性能改善は正直言うとやってみて改善されるかどうかわからないところがある。やってみて成果が上がるか分からないようなものを、壊れるかもしれない危険を冒してまでやりたい人が居るとは思えない。だからちゃんとテストされていないと性能改善がされる可能性が下がる。ちゃんとテストされている、というのがどういう状態を指すのか。今回はC0 Coverageが100%であるという基準で考えていた。大体真面目にテスト書いてると100%にはなるんだけど、たまに見て100%じゃなかったら、少し時間を費やして100%にしますねと断って作業する。経験的に大体99%と100%の間で不具合が出る。

デプロイ

デプロイが気軽にできる状況のほうが改善しやすい。デプロイに数分とか掛かっていたらつらすぎると思う。前述した通り、性能改善のための変更は経験的に何かを壊す可能性が高い。仕様を変えずに内部実装をリファクタリングしました → 仕様が変わってて壊れましたというパターンはよく見る。気軽にデプロイできると変更を細かく反映できるので、複数の不具合が重なって原因究明の難易度が増すということが無くなる。

割れ窓

複数の要因が重なると原因を調べるのが途端に難しくなる。普段からたまに異常が起きるのが常習化してているような状況だと、本当に異常な状態なのかどうか分からなくなるので、段々複数の要因が重なっていってつらいことになる。エラー監視用の画面にいろいろエラーが出ているけど「このエラーよく出るんでまあ無視していいですよ」的な雰囲気になっているとそうなる。

まとめ

  • とりあえず手は尽くす
  • だるいとやらなくなる
  • 放置すると荒れる
  • トレードオフって言わない