こんにちは、hachi8833です。
今回は、大著「The Complete Guide to Rails Performance」で知られるNate Berkopec氏の記事を何回かに分けてお送りいたします。
- 第1回: BootsnapやPumaなど(本記事) -- 開発中のRailsをBootsnapで倍速起動するなど
- 第2回: rack-freezeやsnip_snipなど -- マルチスレッドバクや未使用カラムを検出するgem
- 第3回: 「あなたのアプリサーバーの設定は間違っている」など
概要
原著者より許諾を得て翻訳・公開いたします。
- 元記事: Railsconf 2017: The Performance Update
- 著者: Nate Berkopec (@nateberkopec): Railsのパフォーマンスコンサルタントです。
- 主著: The Complete Guide to Rails Performance
楽しい画像はすべて元記事からの引用です。
RailsConf 2017のパフォーマンス関連の話題(1)BootsnapやPumaなど(翻訳)
概要: この記事は、RailsConf 2017に参加できなかった皆さまや、参加したものの先端的なパフォーマンス関連の話題を何か見落としてないかと気になっている皆さまにお送りいたします(2330 word、12分程度)。
RailsConfを惜しくも逃した方、当ブログへようこそ!RailsConf 2017は既に終了しましたので、RailsConf 2016のときと同様、私が会場で見かけたり話したりしたパフォーマンス関連のあらゆる話題を手短に盛り込みました。
1. Bootsnap
Shopifyが先ごろリリースしたBootsnapは、巨大なRubyアプリを高速に起動できるgemです。リリースはカンファレンス開催の一週間ほど前でしたが、DiscourseのSam Saffron氏はこのgemの素晴らしさについて皆に語っていました。「Gemfileにポンと書き込めばあら不思議、アプリが速くなったよ!」という便利なgemはそうそうありませんが、Bootsnapはまさにそうしたタイプのgemのようです。Discourseでは開発時の起動時間を50%削減したそうです。
bootscaleというgemについては既にご存じの方もいるかもしれません。Bootsnapはこのgemの進化版であり、bootscaleに取って代わることを意図しています。
Bootsnapはどのような仕組みになっているのでしょうか。よくあるパフォーマンス改善プロジェクトとは異なり、BootsnapのREADMEではそうした点について非常に詳しく述べられてるのがありがたい点です。基本的には2つの点を改善します: require
の高速化と、コンパイルされたRubyコードのキャッシュです。
require
の高速化はかなりストレートです。bootsnap
では、Rubyで発生するシステムコールの数を減らすためにキャッシュを使います。たとえばrequire 'mygem'
すると、Rubyはmygem.rbという名前のファイルをLOAD_PATH上のすべてのフォルダについて開こうとします。何ということでしょう。Bootsnapはこれを先回りします。アプリのコードがキャッシュされる期間はわずか30秒なので、ファイルの変更が見逃される心配はありません。
2つめの機能は、コンパイル済みRubyコードのキャッシュです。このアイデアはいっとき取沙汰されていたことがありました。たしかEileen UchitelleとAaron Pattersonもある時期こうした作業を行っていたことがありましたが、いずれも挫折または横道にそれてしまっていたと思います。基本的にBootsnapではあらゆるRubyコードのコンパイル結果を、ファイル自身が持つ拡張ファイル属性に保存します。これはなかなかうまいハックです。残念ながら、いくつかの理由によってこの仕組みはLinux上ではうまく動きません。ファイルシステムがext2またはext3の場合は拡張ファイル属性がオンにならず、たとえできたとしてもLinuxのxattrsの最大サイズは非常に限られているので、Bootsnapが生成するデータを収納しきれないかもしれません。
カンファレンスでもこれについていくつか議論になっていましたが、読み込みパスのキャッシュ機能はBundlerかRubygemsにマージできるのではないかという見解に落ち着きました。
訳注: _ko1さんのツイートがありました。
"RailsConf 2017のパフォーマンス関連の話題(1)BootsnapやPumaなど(翻訳)" https://t.co/aAB2qtLh9b 「たしかEileen UchitelleとAaron Pattersonもある時期こうした作業を行っていたことがありましたが、いずれも挫折または横道にそれてしまっていたと思います。」これ実現したの俺なんだけど...
— _ko1 (@_ko1) December 26, 2017
2. フロントエンドのパフォーマンス
今回私は「フルスタック開発者のためのフロントエンドパフォーマンス」というタイトルでワークショップを開催しました。内容はChrome Developer Toolsで最初のページの読み込み速度の体感のプロファイリングと診断を行う方法の紹介でした。
準備万端かと思われたのですが、どっこい、会場のWiFiで突然サンプル用のWebページの大半で振る舞いが激変してしまい、演習どころではありませんでした。私も少々動転してしまいましたが、幸いなことにRichardが頑張ってCodeTriageでJavaScriptバンドルをasync
とマークし、CodeTriageの描画時間を半減させてくれました。
3. アプリケーションサーバーのパフォーマンス
私はある顧客で最近経験したことを元に、RailsConfでPumaのパフォーマンス上のいくつかの問題の診断と改善を行うというちょっとした役割を引き受けました。
その問題とは、処理するリクエストをPumaがどのようにして受け付けるかというものでした。Pumaのすべてのプロセス(ワーカー)にはそれぞれの内部に「リアクター」というものがあります。このリアクターの役割はソケットのリッスン、リクエストのバッファ、そしてリクエストを手すきのスレッドに渡すことです。
Pumaの問題というのは、デフォルトでリクエストを際限なしにめいいっぱい受け付けてしまうリアクターの振る舞いです。こうなると、Pumaのワーカープロセス間でロードバランシングがうまく機能しなくなります。この問題はリブート中が特に顕著です。
Pumaを導入したRailsアプリを再起動したとしましょう。再起動中にはソケットにリクエストが100個積み上げられ、処理待ちになります。そしてPumaではときどき、処理すべき山積みのリクエストのほんの一部しか受け付けられなくなってしまいます。こうなると、Pumaのリクエストのキュー滞留時間が異常に長くなってしまいます。
この振る舞いについてはこれまでよくわかっていませんでした。Pumaのワーカーにスレッドが5つあるとすると、どうしてリクエストを5つ以上も受け付けてしまうのか。他のワーカープロセスは完全に手すきで待ちぼうけだというのに。この待ちぼうけプロセスには仕事を割り当てるべきでしょう。
そしてEvanがこの問題を修正してくれました。もうPumaは一度に処理可能なリクエスト数を超えて受け付けることはありません。これによってシングルスレッドのPumaアプリのパフォーマンスは著しく向上したはずです。もちろん、マルチスレッドのPumaアプリもです。
私は長い間、そして今もPumaでのリクエストのロードバランシングにはまだ改良の余地があるはずだと考えています。たとえば、Pumaのワーカープロセスが5つあり、うち4つがリクエストを処理中で、1つが完全に暇だとしましょう。このとき、処理中のワーカーがたまたま新しいリクエストを受け取る可能性があります。たとえばMRI/CRubyの環境で、処理中のワーカーの1つがIOブロックしてしまったとしましょう(=データベースからの結果待ち)。このI/O待ちのワーカーが、完全に暇なワーカーの代わりにリクエストを受け付けてしまうことがあります。これは望ましくありません。しかし私の知る限り、「ソケットのリッスン」と「処理受け付け可能な全プロセス」との間のルーティングは完全にランダムです。
唯一の方法は、Pumaにもっと賢くなってもらうことでしょう。つまり、Pumaのワーカーがソケットを自分でリッスンするのではなく、Pumaのリクエストルーティングにソケットリッスン用の何らかの「マスタールーティングプロセス」を置くことです。Evanの提案の1つは、Pumaの「マスタープロセス」に単にリアクター(=新しいリクエストのバッファとリッスンを行う)を置くという方法です。処理を行う子プロセスをこのリアクターが決定するわけです。こうするとPumaのルーティングアルゴリズムの実装が複雑になるかもしれません。たとえばラウンドロビンや、Passengerで採用されている「least-busy-process-first」(一番暇なプロセスに優先的にルーティングする)アルゴリズムのように。
Passengerについても少し触れておきましょう。Phusionの創立者であるHongli氏はこのアイデアを逆手に取り、PassengerがPumaのリバースプロキシやロードバランサーとしても機能するようにしています。これは確実に動作しますし、Pumaが静的ファイルのサービスをpassengerに任せることでPumaの負荷も軽減できるなどのメリットもあります。しかし私は、Pumaに「マスターリアクター」的なマスタープロセスを導入する方がさらに効果があるのではないかと考えています。
続き: RailsConf 2017のパフォーマンス関連の話題(2)rack-freezeやsnip_snipなど(翻訳)