プログラミングの楽しさ。オープンソースとの出会い。

が出版され、『私と Ruby と添削と』という内容で寄稿しました。私がどうプログラミング・オープンソースの楽しさを知ったかについての昔話です。公開して良い、とのことなので公開いたします。

なお、文章中に出てくる tdiarytimes.rb のコードは以下です。9年前に書いたコードなので今読み返すと恥ずかしいを通り越してもはや微笑ましいですね!!1これでも当時は、自分なりにできるだけ綺麗なコードにして公開した記憶があります。

私と Ruby と添削と

プログラミング技術の向上させるには、どういう方法があるでしょうか。プログラミングに関する書籍を読む、オープンソースで公開されているコードを読む、優秀な同僚と働くうちにテクニックが身についてくる、などなど様々な方法があるでしょう。

昔話をしましょう。私がプログラミングの楽しさを知り、プログラミングにのめり込んでいったのは大学三年生の時でした。当時プログラミングがあまり好きで無く、ほとんどコードをかけなかった私は、大学の研究室でコードを書く必要に迫られなんらかの方法でプログラミングを習得する必要がありました。

しかしながら、漠然とプログラミングを覚えようと思ってもすっと習得できるわけではありません。大学で習うプログラミングの話は、UML モデリングやオプジェクト思考の考え方で、アニマルクラスを継承して犬や猫が、といった概念的な話であったりしたため、そもそもプログラミングがよく解らない私にとってそのような高級なレイヤーの話はさっぱりでした。

そこで自分が作りたいものから作ってみようと思い、自分が使っていた日記システムに機能を増やしてみよう、と考えました。幸いにも使っていた日記システムが、プラグインで拡張可能な Ruby で書かれた日記『tDiary』であったため、そのプラグインを試行錯誤しながら書いてみました。

実際に書いてみると、常日頃から使っているものが自分が手を加えることにより変化する、という何かを創造しているような万能感が楽しく、みるみるうちにプログラミングに嵌まっていきました。

なお当時(2003年) 周りでは phpJava が流行っていましたが、周りと違ったことをしたいと思っていた自分は、当時ではまだまだマイナーだった Ruby を書ける自分カッコイイ的な中二病的な感覚に酔いしれたいと思って居たのも良い思い出です。

そんなプログラミングの楽しさが徐々に解ってきたときに、MTBlogTimes という投稿した時間帯によってバーコードのような画像にラインを刻む(画像検索するとどんなものかすぐ解ると思います) Movable Type のプラグインがシンプルでかっこよかったため、それを tDiary のプラグインとして実装した tdiarytimes.rb というライブラリを公開したところ、tDiary 作者のただただしさんの目にとまり、tDiaryCVS レポジトリの中に入れてもらえました。

自分が作ったものが本家のコードに入るなんて!と大変喜んだのですがそれだけではありませんでした。本稿で推薦している本の作者、青木峰郎さんが tdiarytimes.rb をクラス分けし継承し、クラスごとに画像の見せ方を変え、オブジェクト思考の実装を入れ、大幅にリファクタリングしてくださったのです。このリファクタリングにより、クラスの使い方、継承の概念、よりシンプルなコードの書き方など様々なプログラミングのエッセンスを学ぶことができました。

またオープンソースでコードを公開することのメリットや楽しさも知ることができ、外に出すのも恥ずかしいようなコードでもとりあえず公開しておくと、自分にとっても役立つし、誰かのためにもひょっとしたら役に立つかも、と思うようになりどんどんコードを公開するようになりました。

その後、別のイベントで青木さんにまたコードを添削していただく機会に恵まれました。昔よりはプログラミングができるようになったけど、いまいちどこを改善して良いのか解らないと思っていたいましたが、様々な箇所に指摘をもらい、またプログラミングのテクニックを学ぶことができました。

Rubyist Magazine 出張版 正しいRubyコードの書き方講座」では、私のコード改善も含めた系6本の具体的なリファクタリング手法が載っています。実践的な内容になっており、実際のライブラリをどう改善するかというのがストーリーとして書かれており、読み物としても面白いです。リファクタリングのポイントも「コーディングスタイル」「プログラミング一般」「Rubyプログラミング一般」「オブジェクト指向プログラミング」「リフレクション(Ruby では今ではメタプログラミングという表記が一般的でしょうか)」と様々な視点が書かれており、Ruby のみならず標準的なプログラミングの考え方についても解説されています。

また、優秀なプログラマはそれぞれ、ポリシーを持ってコード書いており、たとえば空白スペースやインデント、メソッド名のつけ方一つとっても「なんでそのようなコードになってるか」一つ一つ説明できるような意味を持ってつけています。私が尊敬しているプログラマの一人である青木さんが、どんなポリシーを持ってコードを書いているかも解説されているのも嬉しいですね。

コードが書けるようになったは良いけど、どうやったらより良いコードになるか解らない、そんなプログラミング初心者は手にとって読んでみると、新しい発見があると思います。また本の内容も難しくなく「リファクタリングの本を読んでみたけど、正直よく解らなかった…」という方も読んでみてはいかがでしょうか。本自体は今風ではないですが、プログラミングのエッセンスは十分生かせる内容となっています。

プログラミング技術を向上させるには様々な方法があります。自分の場合は書籍を読んだり公開されているコードを読んだ時にも確かに技術や考え方は向上したと思うのですが、何よりも確実に向上したと思えたのが誰かに「添削」されたときでした。青木さんの添削もそうですし、同僚からのコードレビューもそうでしたし、オープンソースで公開したコードがリファクタリングされたときもそうでした。

今現在は当時とは違い、github を中心としたコミニュティベースでのソースコードの公開、添削、改善のサイクルが生まれています。あなたの公開したコードが昔よりもさらにより誰かが改善してくれやすい環境になっているでしょう。書いたコードが手元で眠っているなら、それをパブリックな場所で公開してみてはどうでしょうか。そのコードを添削してくれる誰かが現れ、よりあなたのプログラミング技術が向上するかもしれませんよ。

ちなみに本稿の話に出てきた tdiarytimes.rb、青木さんがリファクタリングされた tdiarytimes2.rb は現在も tDiary の git レポジトリに入っており、github 等から取得することができます。今見返したら tdiaritymes.rb はメソッドでもなんでもない箇所に、ベタで70行以上もコードが書いてあり歴史を感じることができました…。

例えば GC を止める・Ruby ウェブアプリケーションの高速化

最近クックパッドでは、アプリケーションサーバの大半が Rails 2.3 から Rails 3 に置き換わったのですが*1、リリース前のベンチマークの時点ではあまりパフォーマンスが出ず四苦八苦していました。具体的には Rails 2.3 の時と比べ MRI 1.8.7 だとレスポンスタムが200%ぐらい遅い結果でした。Rails 3 になって実装が Merb core を取り入れ疎結合で綺麗になった反面、より多くのオブジェクトと・メモリを利用する様になった影響かと思います。

そこで Ruby インタプリタの変更*2を行い検証をしたところ

  • MRI 1.8.7
    • (Rails 2.3と比べ) 約200%遅い
  • MRI 1.8.7 -> Ruby Enterprise Edition 1.8.7 2011.03 (tcmalloc 無効)
    • 約180%低速
  • MRI 1.8.7 -> Ruby Enterprise Edition 1.8.7 2011.03 (tcmalloc 有効)
    • 約140%低速

のような感じで、無視できないほど速度が遅く、これではリリースが危ぶまれました。なお、どこで速度が遅いのかというと、GC の実行でだいぶ時間を使っていました。Rails3 でオブジェクトが増えたため、より GC の実行時間がかかる様になってしまったようです。

Unicorn の導入

次にアプリケーションサーバApache + Passenger の組み合わせから Nginx + Unicorn の組み合わせに変更しました。Unicorn について簡単に説明すると、Rack 環境をロードした master プロセスが fork して子の worker を作るため、以下の様な特性を持ちます。

  • fork による worker 生成のため、子 worker の起動が高速
    • そのため再起動が高速になり、再起動時のサーバ負荷が少ない
    • ただユーザからの処理を捌かない master プロセスが必ず存在するため、メモリ使用量が1プロセス分増える
  • epoll / libev 等のイベントでうまく複数接続を捌くモデルでなく、worker 1プロセス1接続のため安定性が向上する
    • そのため、大量のコネクションを要求する必要なウェブサービスには向かない

Unicorn の特徴について、より詳しくは以下のエントリーで述べられてます。

実際に Apache + Passenger の組み合わせから Nginx + Unicorn の組み合わせにすると、わずかにスループットが上がるなあ、といった感じで大きなパフォーマンス改善は見られませんでした。これは Unicorn の特性が高速化よりも安定性や再起動時のメリット*3、シグナルを送るだけで操作できるシンプルさに重みが置かれてるからで、Rack というインターフェイスを通して操作される Ruby アプリケーションの直接的な速度にはあまり関わってこないことを考えても納得の結果でした。

明示的な GC.start

しかし Unicorn について調べている途中、Tuning UnicornUnicorn::OobGC という面白いアプローチを見つけました。
これは指定のリクエスト回数アプリケーションが処理を終えたら、ユーザにコンテンツを返した後に明示的に GC.start するというアプローチです。何が良いのかというと、GC がユーザのリクエスト外で実行するため、リクエスト中の処理では GC が走りにくくなり、結果ユーザへのレスポンス返却速度が高速化されるというわけです。
これを適用したところ、140%ぐらい遅かった速度が 130%ほどへとわずかに高速化されました。たぶん小規模なアプリケーションだと効果が結構見込めそうなのですが、メモリをだいぶ食ってる大規模なアプリケーションだとこれを使っても GC が頻繁に実行され、結果あまり速くならないようでした。

GC を止める

ここでいっそ GC.disable でユーザからのリクエスト処理中は GC を止めてしまい、リクエスト外で GC をしたらどうなるか、と思いついたので試してみました。
Unicorn は worker が master から fork される特徴があり、fork した直後のプロセスに対して設定で処理を書けるので、unicorn.conf.rb の設定に

after_fork do |server, worker|
  GC.disable if RAILS_ENV == 'production'

の、GC.disable で GC が走らない様にする処理を入れ、OobGC のコードを GC.disable 時にも GC.start を実行できる様に変更し、その後再度 GC.disable で止める処理を入れました。

この GC.disable のベンチマークを結果は以下です。

赤のグラフが GC.disable しなかったサーバで、青のグラフが GC.disable するサーバです。あからさまに GC をユーザからのリクエスト処理中は無効化した方が断然高速な結果に終わりました。Rails 2.3 のサーバと比べても65%遅い、もとい150%ほど高速な結果となりました!!

GC を止める Production のアプリケーションサーバに適用

というわけで、このユーザのリクエスト処理中は GC を無効化する方法でだいぶ高速化することが解ったので、一足先に Unicorn 化していた Rails 2.3 サーバ(Ruby のバージョンは MRI 1.8.7 で Ruby Enterprise ではない)に適用したところ、それだけで 130% ほど高速化 & CPU 消費量が減る *4 という素晴らしい結果になりました。

なお Rails3 ほど顕著に高速化しなかったのは、Rails3 にくらべメモリ使用量が少ないからかと思います。その後 Rails3 に刷新後は当初の Unicorn 化する以前の Rails 2.3 アプリに比べ、最終的に 150% ほど高速化しました。

まとめ

Web アプリケーションの速度の高速化は、IO 処理の高速化を除いてしまうと、チューニングすればするほど大きく改善する方法はなくなっていくと思ってましたが、ユーザのレスポンス処理中は GC を止め、その外で実行するだけで大きくパフォーマンスが改善しました。Ruby 1.8 系統を使ってるサービスは試してみる価値があると思います。
なお今後リリースされる予定の Ruby 1.9.3 では nari3 が実装された Lazy Sweep GC が載るので、OobGC 的な事を Ruby 自体が行ってくれ、GC の最大実行時間が減ると思うので、Ruby 1.9 なサービスは 1.9.3 に変えるだけでパフォーマンス改善がされそうですね。

おまけ・運用ノウハウなど

メモリリークの対応

GC.disable している期間が長いと、GC.start したときに元のコードがメモリリークしてると、よりそのメモリーを解放できない様な感じで、どんどん Rails アプリケーションのプロセスサイズが肥大化して行ってしまいました。
Unicorn は worker プロセスに SIGQUIT を送ると、ユーザのリクエスト処理が終わった直後にプロセスが死んで、master プロセスがそれを検知してすぐに fork する仕組みがあるので、以下のユーティリティを書いて Rack レイヤーで対応しました。

use UnicornKiller::Oom, 400 * 1024 # 使用メモリが400Mを超えると自分自身に SIGQUIT
use UnicornKiller::MaxRequests, 1000 # 1000 回リクエストを超えると自分自身に SIGQUIT

プロセスを終了しても即座に fork で新しい worker が作られるため、こんな感じのざっくりとした対応でも問題無く運用できてます。

Unicorn + bundler でのデプロイ時の問題

突然はまるのでちゅうい…。特に Unicorn は master プロセスへの SIGUSR2 (graceful な再起動)で再起動に失敗した場合、何事もなかったのように昔のプロセスは正常に生き続けるため、ぱっと見問題に気づきません…。

アプリケーションサーバベンチマークとパラメータの調整

ベンチマークには最初 ab (apache bench) でいくつかの URL でベンチ、次に JMeter で本番リクエストの上位80%のリクエストをエミュレートしてベンチをとりました。環境さえ作ればあとは叩くだけなので、ab / JMeter を使いつつ、アプリケーションサーバのメモリー数、CPU の利用状況を見て Unicorn のプロセス数の調整と Ruby Enterprise Edition の GC 周りの環境変数を調整しました。
この辺はサービスごとに最適値が違うと思うので、ちまちま数字変えてやるのが良いと思います。

*1:Rails 2.3 -> 3 移行はクックパッド規模になるとかなり大変でしたが、いろいろな面白いアプローチを試せたので、これはこれで何処かでお話ししたいですね

*2: Ruby 1.9.2 化も考えましたが、テスト通すまでまただいぶ時間がかかる&いっぺんにやると問題切り分けが難しいので一緒にアップグレードすることは見送りました

*3: graceful な再起動がほとんどエラーや負荷無く行える重要性は、ウェブサービスを運用してる方なら解ると思います

*4: CPU 使用率はあまり変わらないと思ってたんですが大きく減ったのが不思議な感じです…

さいきんの Rails サービスを高速化をしてみた

先日のももクロハッカソンで出会った wantedly を作ってる仲さんが

と言ってたので、面白そうなので wantedly を速くしてみました。

ちなみにデータが数百万オーダーもなさそうなのに、どのページもログインすると2-5秒ぐらいかかっていたので、確実に速くできそうだなぁという感覚はやる前からありました。

アプリケーションサイドのチューニング

初心者*1にありがちな問題として

  • SQL に適切にインデックス張ってない
  • キャッシュすべき場所をキャッシュしていない
  • 無駄なデータを引きすぎてる

ことがよくあります。ので順に実装を見ていきました。

SQLに適切なインデックスを張ってない

張ってありました!びっくり!\(^o^)/

キャッシュすべき場所をキャッシュしていない

Facebook API を利用したアプリケーションなんですが、ユーザのデータの取得を毎回馬鹿正直に HTTP を叩いてとっていたため、適切にキャッシュを入れました。これで1秒強ぐらい速くなりました。

無駄なデータを引きすぎている

うっかり全件データ(!) を引いていたり、引かなくて良いデータを引いていたり箇所をログと New Relic *2から見ていって直していきました。これでログインユーザが1-3秒ぐらい速くなりました。
SQL のインデックス張ってない問題やこれの問題は、データが少ない当初は遅いと感じなくても、データが増えれば増えるほど徐々に重くなっていき、また開発環境では本番と異なるデータを使ってるため、データ量が少ないこともあるため、初心者には気づきにくい & どこを直したら良いか解らないといったことが発生しがちでしょう。

結果

約6倍速\(^o^)/


体感速度のチューニング

続いてユーザが実際に感じる速度を向上させました。

wantedly は heroku にあるためサーバが遠く、1ファイルの取得に 200ms-300ms のタイムラグが発生します。そのため、以下のアプローチをとりました。

リダイレクトは極力減らす

リダイレクトするとそれだけで200-300ms遅く感じてしまうため、よくクリックするリンクはリダイレクトで無く、URL の生成でちゃんとリダイレクト先を生成するように変更しました。

静的ファイルをまとめる

Rails 3.1 からは 3.1 の目玉機能の一つ、asset pipeline が利用できるようになり、何も考えなくともルールに従えば js / css ファイルを一つにまとめられるようになったため、Rails 3.0 系から Rails 3.1.0 にアップグレードして asset pipeline を有効化しました。なお Rails 3.1.0 化はコード上 Rails 本体を弄る等無理な書き方をしてなかったため、割とすんなりアップグレードできました。

なお、heroku で asset pipeline をうまく使うには memcached plugin を入れ

# production.rb
  config.serve_static_assets = true
  config.assets.compress = true
  config.assets.compile = true

と設定するとうまく動くと思います。

静的ファイルをブラウザにキャッシュさせる

いわゆる Expires / Cache-Control ヘッダの指定ですね。Rails 3.1 + Asset Pipipele + Heroku でうまくキャッシュさせるには

config.assets.digest = true

で asset の URL が内容が変更があったら変わるようにし(じゃないとキャッシュされ続けるため)

config.static_cache_control = "public, max-age=#{1.day}"

で、Rack の ActionDispatch::Static で Cache-Controle ヘッダをはかせるようにします。

結果

以前

以後

ちょっとわかりにくいですが、青のラインが250msぐらい速くなり、体感的にもロードが速くなりました。特に Heroku のようなサーバが遠い場合は効果がだいぶ出ると思います。

終わりに

これ以上の高速化には国内のサーバにする & 動的では無いコンテンツにフロントでもう一段階ちゃんと Varnish 等を入れキャッシュ設計するが必要になりますが、それをするのはコストがかかりすぎるので、やるとしてもサービスがだいぶ伸びてからになると感じました。

あとよく Rails アプリ遅いと言われてますが、200ms 前後の速度でしたらサービスがだいぶ大きくならない限りは、Heroku 等の PaaS でも簡単に維持できるなぁと改めて感じました。*3

あとあと、初めてちゃんと Heroku 触ってみたんですがすごく良くできてますね。ただやっぱり太平洋超えるのは日本向けのサービスだとするとつらいなー、とも感じたので Heroku の ec2 の東京リージョン版が出たらより日本でも流行りそうですね〜。

*1:仲さんはプログラミング初めて一年、Rails 初めて数ヶ月ですが、Ruby の実装から html / css / js ほぼすべて一人で wantedly 作ってすごい…

*2:Heroku 無料で New Relic プラグインが使えて便利

*3:200ms だと遅いという方も居ると思いますが、だいたい重いと感じるサービスはもっともっと処理コストがかかってると思うので…