概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: We Made Puma Faster With Sleep Sort
- 原文公開日: 2020/09/17
- 著者: Nate Berkopec
日本語タイトルは内容に即したものにしました。
Puma 5がリリース!スリープソートによる高速化など(翻訳)
概要: Puma 5は当プロジェクトの大きなメジャーリリースであり、実験的な新パフォーマンス機能がいくつも導入されたほか、多数のバグ修正や機能追加も行われました。その中でも最も重要な目玉機能についていくつかお話しいたします(1839 word/7分)。
Puma 5(コードネーム Spoony Bard1)が本日リリースされました(私の誕生日です!)。このリリースにはさまざまなものが盛り込まれていますので、Pumeユーザーの皆さまが自信を持ってアップグレードできるよう、Pumaのさまざまな機能や変更点についてお話しいたします。
MRI + クラスタモードでの実験的パフォーマンス機能
今回のリリースの見出しを飾るのはたぶんこれでしょう。メモリ使用量を削減する機能が2つと、レイテンシを削減する機能が1つ加えられました。
Puma 5には以下の3つの実験的パフォーマンス向上機能が含まれています。
wait_for_less_busy_worker
設定- ワーカーがビジー状態の場合、ソケットの再リッスン前にわずかな遅延(スリープソート!)を挿入することでMRIでのレイテンシを削減できる可能性があります。意図する結果:「この機能を有効にすると、Pumaクラスタの負荷が高い(利用率50%越え)場合のレイテンシを削減するはず」。
fork_worker
オプションとrefork
コマンド- fork元をマスタープロセスではなくワーカープロセスのひとつとすることでメモリ使用量を削減します。意図する結果:「この機能を有効にするとメモリ使用量が削減されるはず」。
nakayoshi_fork
設定オプションの追加- (可能な場所で)forkとコンパクションの前にGCを行うことで、プリロード済みのクラスタモードアプリのメモリ使用量を削減します。意図する結果:「この機能を有効にするとメモリ使用量が削減されるはず」。
3つの実験的機能は、いずれもMRI上で動作するクラスタモードのPuma設定でのみ利用可能です。
これらの機能を「実験的」と呼ぶ理由は、これらの機能で実際にメリットを得られるかどうかについて確信がないためです。これらの機能が安定していることと何かを壊したりしないことについてはかなり確信していますが、現実世界で実際に大きなメリットを得られるかどうかについてはまだわかりません。一般的な負荷というものは私たちの予想どおりにならないことが多く、ベンチマークをあれこれツギハギしたところで、ある変更点が有用かどうかを見極める手がかりとしては役に立たないのが普通です。
私たちは、これらの新機能がアプリケーションに悪影響を及ぼしたり安定性を損なったりすることはないと信じています。これらの実験的機能は単に「効果あり」か「効果なし」のどちらかになります。
これらの機能のいずれかが特に有用であることが判明すれば、Pumaの今後のバージョンでデフォルトにする可能性もあります。
Pumaをアップグレードして3つの実験的機能のどれかを試してくださる方がいらっしゃいましたら、ぜひアップグレード前とアップグレード後のスクショをGitHub issue #2258に貼ってお知らせください。「何も変わらなかった」という情報も有用なレポートです。「アップグレード前の24時間」と「アップグレード後の24時間」のデータを投稿いただけると一番助かります。
wait_for_less_busy_worker
: スリープソートによるアプリ高速化とは?
この機能はGitLabがPumaに貢献してくださったものです。Puma設定ファイルにwait_for_less_busy_worker
を追加することで機能をオンにできます。
Pumaクラスタから何かひとつリクエストが到着すると、OSはリクエストを拾うために「フリー(free)」かつ「リッスン中(listening)」のPumaワーカープロセスをランダムにひとつ選択します。ここで重要なのは「フリー」と「リッスン中」という用語で、Pumaプロセスは「何もすることがない」場合にのみソケットをリッスンします。しかしPumaをマルチスレッドで実行する場合、Pumaは「ビジースレッドがすべてI/O待ち状態」の場合や「GVL(Global VM Lock)が解放される」場合にもソケットをリッスンします。
GitLabはUnicornからPumaへの乗り換えを調査していたときに、この振る舞いを伴う問題に遭遇しました。ほどほどのスレッド設定(ここでは最大プールサイズが5)で高負荷をかけるとリクエストの平均レイテンシが増加したのです。なぜでしょう?
先ほど私が、OSはリクエストを「リッスン中の」ワーカープロセスにランダムに割り当てると申し上げたことを思い出してください。すなわち、何かを処理中のビジーなワーカープロセスには決してリクエストが送信されないことになりますが、では「ひとつのワーカープロセスで4つのスレッドが他のリクエストを処理している」「しかし4つのスレッドがすべてI/O待ち状態」の場合はどうなるでしょうか?
1つのPumaクラスタ上に以下のような3つのワーカーがあるとします。
- ワーカー1(ビジースレッド数: 0/5)
- ワーカー2(ビジースレッド数: 1/5)
- ワーカー3(ビジースレッド数: 4/5)
ここで、ワーカー3にある4つのアクティブなスレッドがたまたま一斉にGVLを解放し、ワーカーがソケットをリッスン可能になり、それから新しいリクエストが1つやってきたとすると、このリクエストはどのワーカーに振り分けるのが理想的でしょうか?ワーカー1が正解だと思いますよね?残念ながら、ほとんどのOSは33%の場合ワーカー3にリクエストを割り振ります。
ではどう対処すればよいでしょうか?ここでは、負荷の小さいワーカーをOSに選んでもらいたいわけです。もしここでソケットをリッスン中のワーカーリストをソートできれば、そしてOSがそれを元にして負荷が最も小さいワーカーにリクエストを割り当てられればクールだと思いませんか?これをそのまま実現するのは簡単ではありませんが、実はもうひとつ手があるのです。
wait_for_less_busy_worker
は、ワーカーのスレッドプールが完全に空でなければ、ワーカーによるソケットの再リッスンに「ウエイト」をかけます。これによって、高負荷時のシナリオでOSが負荷の小さいワーカーにリクエストが割り振られるようになります。
これがワーカーの「スリープソート」の基本的な考え方です。
[].tap { |a| workers.map { |e| Thread.new{ sleep worker_busyness.to_f/1000; a << e} }.each{|t| t.join} }
つまり上のような感じにすることで、最初に負荷の小さいワーカーにリッスンさせて「負荷の大きい」ワーカーをOSからうまいこと隠蔽するというわけです。
訳注: 画期的(?)なソートアルゴリズム「Sleep Sort」:濃縮還元オレンジニュース|gihyo.jp … 技術評論社
元々この手法は、もっと込み入ったソートの代替手段として提案されました(ビジーなスレッドが増えたらプロセスをより長くスリープさせる)が、単にスリープをオンオフするだけで十分効くことが判明したので、その時点でこの提案は削除されました。
この手法の真の効用は、高負荷シナリオにおいてリクエストのレイテンシが削減されることです。その理由は「ビジーなスレッドが多いワーカーは、ビジーなスレッドを持たないワーカーより遅い」という単純なものです。より高速なワーカーにリクエストが割り当てられるようにしました。このパッチを当てる前は、PumaのレイテンシがUnicornより増加することがGitLabによって観察されましたが、パッチ適用後の両者のレイテンシは同じになりました(Pumaのメモリ節約型マルチスレッド設計のおかげで、これらのフリートサイズも30%削減できました)。
今後、この振る舞いをさらに効果的に実装する方法が見つかる可能性もあります。libev
を用いるマジックがいくつかあるのも確かですし、さもなければ他のスリープ/ウエイト戦略を実装すれば済むことです。
fork_worker
puma.rb設定ファイルにfork_worker
を追加するか、CLIで--fork-worker
オプションを指定することでこの機能をオンにできます。このモードでは、Pumaは追加のワーカーを(マスタープロセスから直接forkするのではなく)「ワーカー0」からforkするようになります。
10000 \_ puma 5.0.0 (tcp://0.0.0.0:9292) [puma]
10001 \_ puma: cluster worker 0: 10000 [puma]
10002 \_ puma: cluster worker 1: 10000 [puma]
10003 \_ puma: cluster worker 2: 10000 [puma]
10004 \_ puma: cluster worker 3: 10000 [puma]
preload_app!
オプションと同様、fork_worker
はコピーオンライトによるメモリー削減
のために一度だけ初期化されます。これには以下の2つのメリットがあります。
- 1. Pumaのphased restart(段階的再起動)と互換性がある: マスタープロセス自身はアプリケーションをプリロードしないので、
preload_app!
と異なりこのモードはphased restart(SIGUSR1
またはpumactl phased-restart
)で効きます。phased restartの一環としてワーカー0が再読み込みされると、Pumaはアプリケーションの新しいコピーを初期化してから他のワーカーを再読み込みします。この再読み込みは、新しいプリロード済みアプリケーションを既に含む新しいワーカーからforkすることで行われます。
これによって、phased restartはホットリスタート(SIGUSR2
またはpumactl restart
)と同じぐらい短時間で完了しつつ、再起動をクラスタ上のワーカーに分散することでダウンタイムを最小化できるようになります。
- 2. アプリケーション実行時の追加コピーオンライトを改善する
refork
コマンド:「fork-worker」モードで新しく導入されたrefork
コマンドは、ワーカー0以外のすべてのワーカーをワーカー0からforkすることで再読み込みします。
このコマンドは、事前初期化が起動時だけでは完了しないような大規模または複雑なアプリにおけるメモリ使用量を潜在的に改善します。その理由は、再度forkされたワーカーがコピーオンライトのメモリーを、既に動作中でリクエストを処理しているワーカーと共有できるためです。
SIGURG
シグナルをクラスタに送信するかpumactl refork
コマンドを実行することでいつでもrefork
をトリガーできます。refork
は、ワーカー0で一定数のリクエスト(デフォルトでは1000)を処理したときにも自動的に1回トリガーされます。自動refork
される前のリクエスト数を設定するには、fork_worker
に正の整数をひとつ渡す(例: fork_worker 1000
)か、0
を渡して無効にします。
nakayoshi_fork
puma.rb設定にnakayoshi_fork
を追加することでこのオプションをお試しできます。
nakayoshiは日本語の「仲良し」すなわちフレンドリーという意味です。元々nakayoshiという概念は、とあるgemでMRIスーパーコントリビューターKoichi Sasadaによって実装されましたが、これをもっとシンプルにしたものをPumaに導入したらどうなるかやってみたかったのです。
基本的には、あるワーカーをforkする前に以下を行うだけです。
4.times { GC.start }
GC.compact # 可能な場合
ここでのコンセプトは、コピーオンライトのメリットを最大化する目的で、fork前にできる限りRubyのヒープをクリーンにしておこうというものです。これによってメモリ使用量が削減されるはずです。
その他の新機能
福袋に入っているその他の機能も軽くご紹介しましょう。
- OpenSSLがインストールされていないコンピュータでもPumaをコンパイルできるようになりました。
- アクティブなスレッドバックトレースをすべて出力する
thread-backtraces
コマンドがpumactlに追加されました。これはDarwin環境では既にSIGINFO経由で利用可能でしたが、この新しいコマンドによってLinuxでも使えるようになりました。 Puma.stats
にrequests_count
カウンタが追加されました。lowlevel_error_handler
が若干拡張され、ステータスコードも渡せるようになりました。- phased restartやワーカーのタイムアウトが高速になったはずです。
Puma.stats_hash
でPumaの統計情報を(JSONではなく)ハッシュでも取れるようになりました。
多くのバグ修正
今回のリリースでは相当多くのバグが修正されました。中でも重要な修正を以下にリストアップします。
- シャットダウンの信頼性が向上したはずです。
- シャットダウン時のソケットのクローズに関連する問題が修正されました。
- Reactor内でのコンカレンシーバグがいくつか修正されました。
out_of_band
の信頼性が向上したはずです。- Action Cableユーザーから報告されていた、サーバーを起動できなくなる問題が修正されました。
prune_bundler
のさまざまな安定性が向上しました。
内部やテストの改善
今回のリリースではテストのカバレッジで多大な改善が施されました。Puma 4.0時代と比べてテストの量が倍近く増え、安定性や再現性も向上しました。
今回のメジャーリリースでは多くの破壊的変更も発生しています。完全なリストについてはHistory.mdファイルをどうぞ。
コントリビューターに感謝です!
今回は私たちのチームに新しいメンテナーであるMSP-Gregを迎えて以来初のメジャーおよびマイナーリリースとなりました。Gregはテストスイートの信頼性向上のためにおびただしい成果を達成し、SSL機能を最新化して拡張性を高める作業でも多くの成果を残しました。Gregは私たちのチームのWindowsエキスパートでもあります。
以下は、今回のリリースで10コミット以上のコントリビューションをいただいたメンバーのリストです。
- Tim Morgan
- Vyacheslav Alexeev
- Will Jordan
- Jeff Levin
- Patrik Ragnarsson(issueトラッキングでも大活躍でした)
Pumaに貢献してみたい方は、こちらのコントリビューションファイルをお読みください。私たちは常に皆さんのお力添えを求めており、そのためにも皆さんができるだけ気楽に貢献できるよう取り計らっています。
Puma 5をエンジョイしましょう!
関連記事
- Pumaでは、プロジェクトで重要な貢献を多数行ってくれた「スーパーコントリビューター」が新たに加わるたびに、次回リリースの命名権を彼らに委ねています。今回のリリースではWill Jordanのコードが多数フィーチャーされており、Spoony Bardは彼の命名です。Will曰く「Final Fantasy IVはぼくにとってとりわけ懐かしいゲームで、自分が初めて関わった大規模オープンソースプロジェクトは、90年代後半にこのゲームの再翻訳に趣味で携わったときでした」(訳注: モンスター/【ぎんゆうしじん】 - ファイナルファンタジー用語辞典 Wiki*) ↩