#CookpadTechConf 2016で「Railsアプリ開発環境の高速化」について話した

クックパッドの社員が発表するCookpad TechConfというイベントの第一回が今日行われ、「Railsアプリ開発環境の高速化」というテーマで話してきた。

開発環境の改善について

僕が技術部に入る前、サービス開発をやる中で一番不満だったのが開発環境のパフォーマンスだったので、技術部に配属されたころからこの仕事をやりたいと思っていた。

今回は先輩方が既に行っていた開発環境のパフォーマンスチューニング - クックパッド開発者ブログの一部を紹介しつつ、その続きとして自分がやってきたことを発表した。 業務で出した成果のうちいままで外部で発表したのはbyebugの高速化くらいだったので、普段僕がどんな仕事をやっているのか紹介する良い機会になった。

発表内容の補足

思ったより15分の枠で話せたことが少なかったので、発表内で話し足りなかったことについて書く。

libsassおすすめです

急いでて全然ちゃんと紹介できなかったんだけど、この発表の中で普段Railsアプリの開発をやっている人にとって目新しいことは多分libsassしかない。これはsassのC++実装で、今週sass gemからの移行が完了し、いれた次の日に同僚から突然速くなってよかったというフィードバックがあった。

libsassが良い理由

おすすめする理由は以下の4つある。

  • とにかく速い
  • Cookpadのcss全体のうち挙動に影響のある非互換が、関数による色の計算が少しズレることだけ
  • gcc 4.6以上かclangならいまのところ普通にコンパイルできている
  • Sprockets 4で標準サポートされるのでレールに乗ってる

導入効果

Sprockets 3だとなぜかlibsass以外のレイヤー*1が遅く、期待より効果が少し低かったのだけれど、それでも改善前のアセットプリコンパイルの71%がcssに費されていたので資料に書いた通りそこそこインパクトがあった。Sprockets 2だと cssを生成する時間が604秒から140秒に短縮される *2

railsで使う場合は今はsassc-railsを使うと良い。なおsassc-rubyを普通に使うとscssのシンタックスエラーでscssでないコードがエラー画面に出てしまうのでパッチを投げていて、これを反映したフォークを運用している。

RubyのGC使います

発表内容的に完全に脱線なので質問されたら話そうと思っていた内容なんだけど、Cookpadでは例えば GC を止める・Ruby ウェブアプリケーションの高速化 - 2nd lifeにある通り1/23現在リクエスト処理中はGCが止まっていて、これを来週変える予定であるという話。

gctoolsがRuby 2.1でしか動かない

開発環境でOut-of-band GCもどき *3 を試していたとき、Unicorn::OobGCのコードを読んでいたらRuby2.1以上ならtmm1/gctoolsがおすすめというコメントが書いてあった。そこで興味を持ってCookpadのRubyが2.0.0から2.2にアップグレードされた時gctoolsの検証を行ったのだけど、unicornのエラーがガンガン出たのですぐにロールバックした。

Ruby 2.2のRIncGCよさそう

それでgctoolsのissueを見てみたところ、どうもgctoolsの作者によると、Ruby 2.2で導入されたRIncGCのおかげでOobGCは不要であり、Ruby 2.2サポートはしないらしい。ちゃんとした説明はRIncGC jaを読んで欲しいのだけど、メジャーGCの長いマーク時間を解決するための改善が入ったようなので、普通にGC.enableしてOut-of-band GCもしない状態を本番の一部のサーバーで検証したところ、それほどパフォーマンスも劣化せず、かつCPU使用率が1割程度下げられそうな計測結果が得られた。

迷ったら健全な方

そこでインフラ部と相談し、「GC を無効化する運用は健全じゃないと考えています。Ruby の GC の改善がクックパッドにどう影響するのかというのをちゃんとコミュニティに還元するべき」という理由からGC.enableの許可をインフラ部長のmirakuiさんからいただいたので、特に何も起こらなければ来週からGCが有効になる予定。*4 結果は何かの機会にコミュニティに還元されるでしょう。

*1:アセットプリコンパイルにおいて、Sprockets 2だとjsがボトルネックになり、Sprockets 3だとcssがボトルネックになるというのが根拠。なぜ遅いのかは調査中。

*2:独自パッチによる並列化なしの状態での計測

*3:Unicorn::OobGCとかと違ってGC.startしないのでOobGCと呼ぶのを控えている

*4:僕は本番サーバーには触れないので実際の作業はsora_hにお願いしている

2015年にやったこと

今年は卒論を書いて大学卒業後社会人になり、新卒研修を3ヶ月やり、部署に配属されて6ヶ月働いた。
以下、2015年にやったことをまとめる。

発表

2014年はどの勉強会にいっても誰にも知られてなくて寂しかったので、2015年はたくさん外部で発表するというのが目標だった。 今年は社外向けには10本発表した。

RubyKaigi 2015

1日目

k0kubun/hamlitの宣伝をした。発表練習をたくさんやっていろんな人に資料のレビューをいただいたので満足な発表ができた。関係者各位には本当にお世話になりました。LTじゃない発表するの初めてだったけど、いろんなことを伝えられるので長い発表の方がやってて面白いなあと思った。

2日目

頭がおかしくなって2日連続発表した。2日目も意外と評判が良くてホッとした。

Go Conference 2015 summer

k0kubun/ppの宣伝とメタプロの話をした。LTなんだけど、後でこの発表で僕を知ってくれた人に会ったのでGoConは大きくて良いなあと思った(小並)。

TokyuRuby会議09

k0kubun/rebuildk0kubun/karabiner-dslの宣伝をした。とにかく上手い飯と酒が無限に提供される最高の会だった。

Itamae Meetup #1

普段itamaeを使ってて工夫していることを色々話した。itamae大好きな人がいっぱいいて、僕もこういうOSSが作れるようになりたいと思った。

Shibuya.rb 20150715

k0kubun/activerecord-precountの宣伝をした。結構気に入っているgemで、発表して以降割と良い評価をもらえているので良かった。

hikarie.go #4

k0kubun/go-ansiの宣伝と、GoをWindowsで動かすには結局自分でWindows APIを叩くしかないという話をした。つらい話。

マルチプラットフォームなインタラクティブシェルを楽に作る - Speaker Deck

その他

特に資料は公開してないけど、社外の人向けにはあと3回くらいLTしている。

  • Cookpad x CyberAgent x DeNA 15卒エンジニア交流会
    • Railsアプリ開発環境高速化の話をした。
  • Donuts, DeNA, Yahoo, 弊社の交流会
    • RubyKaigiのLTと同じくbyebugの話をした。
  • Cookpad TechBar -vol.3-
    • 学生時代と今やっていることについて話した。

ホッテントリ

タイトル
1位 ElectronでYoruFukurou風のTwitterクライアントを作った - k0kubun's blog
2位 gdbを使ったrubyのデバッグ - クックパッド開発者ブログ
3位 k0kubun/md2key · GitHub
4位 byebugやpry-byebugを使った後の挙動を10倍高速にしました - k0kubun's blog
5位 PCを自作してArch Linuxを入れた - k0kubun's blog
6位 Slimより高速なHaml実装「Hamlit」をリリースしました - k0kubun's blog
7位 Electronを初めて触った時にハマった5つのこと - Qiita

今年はブログの読者とかQiitaのフォロワーがそこそこ増えたので嬉しい。 あと去年書いた奴だけど、うるう秒が挿入された時に Linux - topコマンドの使い方 - Qiita がやたらとブクマついた。

OSS活動

今年リリースしたOSS

Star リポジトリ
★365 k0kubun/md2key
★304 k0kubun/hamlit
★102 k0kubun/Nocturn

Hamlitは最初の実装に1ヵ月、その後のフルスクラッチに2ヵ月かけてるけど、Nocturnは2日で作ったし、md2keyは2時間で完成した。でも書いてて楽しいのはHamlitみたいな奴なので、こういうのを作り続けたい。

それ以外だと去年作った奴を引き続きメンテしたり、誰かのOSSにプルリを投げたりしていた。
Itamaeで色々やってたらコミット権をもらえた。

あと今年は初めてCRubyにコミットして、以下の貢献はNEWSに入れてもらえた。

2016年は

入社後業務をやっていてわからないことだらけなので、2016年はインプット側に力を入れて地に足をつけたい。 アウトプットの目標としては、書籍になるようなOSSを作って自作OSSの単著を出したいとずっと思っているんだけど、逆算して計画を立ててどうにかなる話でもないと思っているので、引き続き自分が好きなこと、興味のあることをやって毎日成長するのを楽しむ生活を送りたい。

来年もよろしくお願いします。

#RubyKaigi 2015 でテンプレートエンジンの高速化について話しました

FamlとHamlitがなぜ高速かについて話しました

RubyKaigi 2015の1日目に、「High Performance Template Engine」というタイトルで発表しました。 @eagletmtさんが作ったFamlと、僕が作ったHamlitがなぜオリジナルのHamlに比べ高速か話しました。

発表は以下のような内容でした。

  • テンプレートエンジンとは何か
    • テンプレートエンジンの例
    • テンプレートエンジンの仕組み
    • パフォーマンス
  • どのようにRubyのコードを速くするか
    • ベンチマークがなぜ必要か
    • プロファイリングの方法
    • 改善の仕方
  • 高速化のために何をしたか
    • Famlがやったこと
      • Attributeの最適化
      • 高速なRuntime Attribute Builder
    • Hamlitがやったこと
      • String Interpolationのコンパイル
      • 使われていない挙動の削除

以下、発表内で触れられなかったことについて書きたいと思います。

協力しています

同じ会社の人が別々に作って社内で競争しているように見えたかもしれないですがそれは気のせいで、Hamlitは完全に自分の勉強のための趣味プロジェクトとして作っていて、特に社内向けに作っていたわけではないです。

逆にFamlは社内のプロジェクトで使われているため、Famlのユーザーに影響のあるバグを発見した時に僕が直したりとか、Famlの開発初期の段階でKaminariが動くように対応したこともありました。

なので、業務レベルではHamlitを作った経験を生かしてFamlの開発に協力しているという状況であり、特に対立関係にあるわけではないというか、eagletmt先輩は会社では僕の教育担当です。

本家Hamlへの還元に関して

僕はOSSを改善する際は、社内モンキーパッチや野良forkを作るのではなく、元の実装を使わさせていただいたお礼としてupstreamに還元するべきだと思っていて、結構Haml本家を良くしたいという意思は強いです。

実際2ヶ月かけてHamlitをフルスクラッチしたときはパーサーを本家のものに置き変え、次のバージョンのHamlになるつもりで作っていたのですが、HamlにはTempleだと実装しにくい挙動があまりに多く、本家を一度に全てリプレースするのは途中で断念しました。

ただ作っている過程で、現在の挙動を完全に維持しつつ(FamlやHamlitまではいかないものの)ある程度高速化できそうなイメージがついたので、高速化の根幹となるTemple移行のプルリを出したのですが、まだマージされていないというステータスです。

今日Hamlコミッタの松田さんに聞いたところ、現在のメインのコミッタであるNormanしか基本的にマージボタンを押さない運用のため、彼が見るの待ちだそうです。

感想

eagletmtさんと一緒に発表の準備とかしたらテンプレートエンジンに関する面白い話や技術的に深い話が聞けるかなと思って一緒に発表しようと誘ったのですが、準備をしていくうち、本家の実装や仕様に対してどう思っていて何を考えてFamlを作っていたかが聞けてとても面白かったし、大成功でした。

その面白さの一部でも発表を聞いてくれた皆さんに伝わっていればよいなと思います。資料の修正や発表練習に協力いただいた皆さま、RubyKaigiを運営している皆さま、発表を聞いてくださった皆さま、そしてeagletmtさん、ありがとうございました!

#itamae_meetup で「itamaeを楽しく使うための工夫」について発表してきた

Itamae meetup #1 で発表した

趣味でちょくちょくコミットしているItamaeという構成管理ツールの第一回Meetupがあり、Itamaeについてどこかで話したいな〜とずっと思ってたので話してきた。個人的には、僕が作った機能のうち特に最近実装した以下のものを各位に使ってもらうことによって、Itamaeレシピの書かれ方やItamae自体をいい感じにしていきたくて発表した。

話した内容

普段5分だけ話すことが多くてあまり10分話すのには慣れてなかったのだけれど、ノリと勢いで以下のような雰囲気のことを口走っていたら10分経っていた。

落ちついて話したい

感想

いろんな人にItamaeが愛されてるのが伝わってきて、発表も面白いのが多くて良い会だった。弊社メンバーと発表を聞いてくださった皆さま、ありがとうございました!!

Hamlit v2.0をリリースしました & RubyKaigi登壇します

Slimより高速なHaml実装「Hamlit」

RubyでHTMLを生成するのにERB以外でよく使われるテンプレート言語にHamlSlimがあります。haml *1 をやめて高速なslimに移行する人が多かったのですが、私はHamlのシンタックスの方が好きなので、slimが用意したベンチマークでslimより高速なHaml実装「Hamlit」を3月にリリースしました。 *2

これはslimが提供しているベンチマークでHTML escapeを有効にし *3、FamlとHamlitを追加したベンチマーク結果です。なおHamlitは完全にHaml互換の仕様ではなく、この非互換が有利に働いています。

互換性と性能が大幅に向上したHamlit v2.0

Hamlitの互換性の問題

Hamlitは最初のv0.1の時点で上記のようなベンチでSlimより高速ではあったのですが、以下のような欠陥がありました。

  • HTMLエスケープがない
  • エラーがコンパイル時に出る & ランタイムで出ても行番号がズレる
  • コーナーケースで属性のレンダリングが重複したり、消えたりする
  • 最適化の影響で、書き方が静的か動的かでHTMLのレンダリング結果が変わってしまう
  • 空白除去オペレータの挙動がおかしい

社内の20,000個以上のテストケースがあるRailsアプリで使ってみたら5,000個くらいのテストが落ちる状態でした。 以前、これを地道に潰しながら致命的な仕様の見直しを行ったHamlit v1.0.0をリリースしました。v1のどこかのバージョンでそのアプリの全てのテストが通っています。

それでも上記の問題が完全に潰せていたわけではなく、保守性的にも様々な問題を抱えていたため、2ヶ月かけてフルスクラッチを行い、大幅に互換性を改善したHamlit v2.0をリリースしました。

もう一つのHaml実装「Faml」との戦い

Hamlitより先にリリースされたhamlより高速なHaml実装としてFamlがあります。 slimが提供しているベンチマーク(をFamlが公平に参加できるようにしたもの)ではHamlitの非互換性が有利に働きHamlitの方が高速に動作しますが、その他の多くのベンチマークではC拡張を持っているFamlの方が高速に動作する状態でした。

Famlが恣意的に用意したベンチマークでは、1.35倍程度Hamlitの方が遅い状態でした。 さて、 v2.0ではHamlitもC拡張を用意 してC言語レベルでの最適化を行い、上記のような動的なテンプレートでも以下のようにHamlitの方が高速に動作するようになりました。 (Travisでの結果)

      hamlit2 v2.0.1:    24225.3 i/s (0.041ms)
         faml v0.7.0:    19405.8 i/s (0.052ms) - 1.25x slower
      hamlit1 v1.7.2:    10450.2 i/s (0.096ms) - 2.32x slower
  haml v5.0.0.beta.2:     5260.5 i/s (0.190ms) - 4.61x slower

でも昨日FamlがC++になって速くなってるので、明日には抜かれているかもしれません。

静的解析エンジン

FamlはRubyの式が静的であればコンパイル時にレンダリング結果を確定させる機構を持っているのですが、Hamlitのそれはかなり貧弱のため、そういったケースでもレンダリング速度で劣っていました。 真面目に実装しなおしたため、上記のテンプレートの値をFamlやHamlitが解析可能な位置にズラしたベンチマークでは、 本家hamlの569倍 の速度が出るようになりました。 (Travis)

      hamlit2 v2.0.1:  3639877.1 i/s (0.000ms)
         faml v0.7.0:  1434853.5 i/s (0.001ms) - 2.54x slower
      hamlit1 v1.7.2:    17734.8 i/s (0.056ms) - 205.24x slower
  haml v5.0.0.beta.2:     6388.2 i/s (0.157ms) - 569.78x slower

コンパイル速度の問題

これまで出てきたのは全てレンダリング速度に関するベンチマークですが、本来はコンパイル速度も全体のパフォーマンスに影響し得ます。 Webサービスではあるテンプレートのコンパイル回数に比べてレンダリング回数の方がかなり多くなるためコンパイル速度は無視できますが、Hamlitはコンパイルが非常に遅く、レンダリング回数がそれほど多くないタスクで使うには必ずしもHamlitをオススメできない状態でした。

これはhamlのリポジトリにあるstandard.hamlという少し大きめのテンプレートですが、コンパイル速度でFamlに勝てるようになりました。(Travis) コンパイル時に最適化をあまり行わないhamlにはまだ勝てていません。

  haml v5.0.0.beta.2:      297.5 i/s (3.361ms)
      hamlit2 v2.0.1:      152.6 i/s (6.551ms) - 1.95x slower
         faml v0.7.0:      139.4 i/s (7.173ms) - 2.13x slower
      hamlit1 v1.7.2:       78.0 i/s (12.827ms) - 3.82x slower

RubyKaigiでどうやって速くしたか話します

RubyKaigi 2015に、Famlの作者である @eagletmt さんと一緒に登壇します。一つの枠で二人のアイデアが聞けるのでお得です。 どう速くしたのかの詳細はあえてこの記事では書きませんでしたが、何を考えて作ったのかとかも含めRubyKaigiで話す予定です。是非聞きに来てください!!

*1:言語に関してはHaml、gemに関してはhamlで表記します

*2:Hamlit v0.1.0リリース時の記事: http://k0kubun.hatenablog.com/entry/2015/03/31/004021

*3:元のベンチではSlimもERBもRubyのスクリプトに対してHTML escapeを行わないオペレータを使っているんだけど、HamlにはHTML escapeされるオペレータを使っているのでSlimやERBでもHTML escapeされるように直してる。HTML escapeしない側に倒さないのは、ベンチに加えたいFamlにはHTML escapeしないオプションがないのと、Railsの標準だとescapeされるため。

byebugやpry-byebugを使った後の挙動を10倍高速にしました

byebugとpry-byebugのbundle updateをしましょう

byebugはRails標準でインストールされるRuby 2.1, 2.2向けのデバッガで、pry-byebugはpry *1 にデバッガの機能を追加するpryのプラグインです。 一昨日から今日にかけて、以下のパフォーマンス改善を含む byebug v8.0.0 と pry-byebug v3.3.0 がリリースされました。

github.com github.com

byebugとpry-byebugには、一度byebugbinding.pryを叩くとそれ以降ずっとアプリケーションの挙動が10倍遅くなるという問題がありました。 これが上記の変更により改善されたので、 Railsアプリやgemのデバッグにbyebugやpry-byebugを利用している方はそれらのbundle updateをおすすめします。

binding.pryすると10倍遅くなる問題

Byebugは行の移動やブレークポイントで処理を止めたりするため、Ruby 2.0から導入されたTracePoint *2 *3 というRubyのVMで起きるイベントに処理をフックさせるためのAPIを使っています。

byebugとpry-byebugはbyebugbinding.pryを叩いたときにByebug.startというメソッドの中で必要なTracePointを設定することによってデバッグを可能にしているのですが、 いままでのbyebugやpry-byebugはデバッグ終了後にデバッグ用のイベントフックを無効にしていなかった ため、一度byebugbinding.pryをするとそれ以降どんなアプリでも10倍程度遅くなってしまう状態でした。

github.com

なぜそんな状態だったのか

byebugとpry-byebugの作者はこの問題とどうしたら直るかを認識していたのですが、以下の理由により簡単には解決できない状態でした。

  • TracePointを無効にするためのByebug.stopがバグっていて、使うと大量にテストがfailする
  • TracePointを複数設定した後それらを無効にすると、freeしたTracePointを参照してしまうRuby自体のバグがありランダムにSEGVする

これらの問題をgdbを使って地道にデバッグを行って原因を特定し冒頭に載せたパッチによって問題が解決したため、デバッグ終了時にByebug.stopを呼ぶことが可能になりパフォーマンスが大幅に改善されました。なお、Ruby自体のSEGVに関しても原因を特定しパッチを投げています。

github.com

注意点

byebugやpry-byebugにはブレークポイントを設定する機能があるのですが、それを設定している間はByebug.stopしない(したらブレークポイントが動かなくなる)ためブレークポイントを使っていると遅いままになります。解除すればまた速くなります。

なお、Byebug.stopされているかはアプリケーション上でByebug.started?falseを返すかを見ることで確認することができます。 falseが返れば高速に動作しています。

所感

直すの結構大変だったので是非最新のbyebugとpry-byebugを使っていただきたい気持ちです。

ISUCON5予選に「railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。 」チームで参加してきた #isucon

学生枠で出場した去年に引き続き、今年は新卒社員になった@cnosuke@rkmathiと僕@k0kubunの3人でISUCON5予選に出場した。
僕らが最後に確認できたスコアは11045で、その時のスクショだと2日目の16位という感じで、予選通過はなりませんでした。

id:rkmathiの記事: http://rkmathi.hatenablog.com/entry/2015/09/28/004734

戦略

最初にコードは読まず、nginxのアクセスログをパースして集計する奴とnewrelicを使ってベンチ中最も時間を使っているエンドポイントを特定し、そのエンドポイントの中でどこが遅いかはrack-lineprofで把握してそこだけ読んで潰していく感じでやった。
去年の感じだと予選はいきなりキャッシュとか入れなくても落ちついて普通のチューニングをしていれば突破できると思っていたので、地道にSQLのチューニングをしていた。

僕らのチームがやったこと

時系列で並べるのが好きなのでそれで書く。
cnosukeがインフラレイヤーをやって、rkmathiがMySQLのログ集計とかインデックス直しをやって、僕がクエリ変形させたりひたすらN+1を潰すのをやっていた。
他は思いついた奴を出しあって手があいてる人がやる感じだった。

score やったこと
200 Rubyの初期状態(大体)
800 nginxで静的ファイル配信
926 relationsのINSERTをone < another保証にしてクエリの条件減らした。created_atのソートを全てidにした。
1192 footprintsにインデックスを貼った
1264 InnoDBのバッファのサイズを上げた
2947 relationsのoneとanotherにインデックス貼った。セッションのクッキーストアの暗号化をやめた。
6202 "/" へのリクエスト用にis_friend?のN+1用preloader (JOINではなくクエリ2つで引く)を書いた。entriesのレスポンスをRedisにキャッシュするようにした。
9557 LIMIT 1000を10個に絞るクエリ2つを両方LIMIT 10で引き、commentsのSELECTのrowsが減るインデックスを貼り、Redisのチューニングをした
9610 firstしてる系にLIMIT 1を付加
10194 friend idsを引くN+1をなんとかした
7200 再起動しただけで点数が減った
7963 N+1になるget_userの汎用的なpreloaderを書いて適用
8402 ベンチを回す度に点数が回復していた
10000 friend idsを引くクエリをRedisのsetに変えた(けどこの辺でボトルネックがIOになっていて、期待よりスコアが伸びていない)
11045 遅いエンドポイントのビューの中のget_userとかのN+1を潰した。

最初に10000点まで行くのは結構早くて順調だったんだけど、RedisとMySQLのウォームアップがちゃんと書けず、CPUもメモリも使いきらない状態のまま提出となった。
この辺の経験不足が敗因なのかなあと思っているのと、正直このラインまでやれば予選抜けられると思っていたので、レベルが上がってるような気もした。

感想

めちゃくちゃ楽しかったし、ずっと集中しててすごい疲れた。去年は学生枠でギリギリ本戦に出してもらった感じだったけど、今年は去年よりちゃんとチームで連携してスコアが上げられて、メンバーの成長が感じられてよかった。
今年結構テンプレートエンジン周りを触ってて、その辺練習の時は最適化とかやってて今回の問題でも直せるところあるなと思ったけど、本番ではそのレイヤーにボトルネックが移せなかったのが悔しかった。

次から次へと直すところが出てくる面白い問題で、出題や運営をしてくださった方々には感謝ばかりです。是非来年も参加したいです。