
株式会社Techouseにてインターンをしているmiyatisです。
先日参加したKaigi on Rails 2025で、Samuel Williams氏によるKeynote「Building and Deploying Interactive Rails Applications with Falcon」を聴講しました。
FalconとはRack互換のHTTPサーバです。 Fiberという軽量なスレッドでリクエストを処理することで、特にI/Oバウンドなアプリケーションにおいて優れたパフォーマンスを発揮します。
本講演では「Falcon」というアプリケーションサーバのビルドやデプロイ、監視周りについての最新情報が共有されました。 本記事では、そもそもFiberとは何なのか?FalconサーバはなぜI/Oバウンドな処理において高速なのか?を説明しつつ、講演で示された内容を基にFalconの性能を検証してみました。
Rubyにおける並行・並列処理
Fiberを理解するために、まずはRubyにおける並行・並列処理について説明します。 Rubyの並行処理は主にThreadが使用されています。 RubyのThreadはOSのネイティブスレッドを用いて実装されています。
RubyにはGVL (Giant VM Lock)という仕組みが導入されており、基本的に1つのRubyプロセス内で同時に実行されるThreadは1つだけです。 GVLは複数のThreadが同時にVM内の重要な構造にアクセスすることを防ぐことで、Ruby VM内の整合性を保っています。
例えば以下の2つのコードの実行時間とメモリ使用量を比較してみます。
# 通常のループ処理 10_000.times do |i| 1 + 1 end
# Threadを使用した場合 threads = [] 10_000.times do |i| threads << Thread.new do 1 + 1 end end # 全てのスレッドの終了を待機 threads.each(&:join)
上記の2つのコードの実行時間とメモリ使用量を測定するコードは以下の通りです。
def measure_memory `ps -o rss= -p #{Process.pid}`.to_i / 1024.0 # MB単位 end puts '【通常のループ処理】' puts '-' * 60 memory_before = measure_memory start_time = Time.now # タスクを実行 end_time = Time.now memory_after = measure_memory puts "実行時間: #{(end_time - start_time).round(3)} 秒" puts "メモリ増加量: #{(memory_after - memory_before).round(2)} MB"
実行結果は以下の通りです。
| 通常のループ処理 | Threadを使用 | |
|---|---|---|
| 実行時間 | 0.0002 秒 | 0.6890 秒 |
| メモリ使用量 | 0.02 MB | 334.02 MB |
Threadを使用して並行処理を行おうとしても、通常のループ処理と比較して実行時間が減るどころか大幅に増加し、メモリ使用量も大幅に増加してしまいました。 これは、gvlの影響で複数のthreadが同時に実行されないことが原因です。 むしろ、Threadの生成と管理にかかるオーバヘッドによってパフォーマンスが低下していそうです。
ですが、GVLはI/O処理などのBlocking処理を行う際には解放されるため、他のThreadを実行すること可能です。
そこで、Blocking処理 (sleep) を挟んだ場合で、同様の比較をします。
# 通常のループ処理 (Blocking処理を挟む) 10_000.times do |i| 1 + 1 sleep(0.001) end
# Threadを使用した場合 (Blocking処理を挟む) threads = [] 10_000.times do |i| threads << Thread.new do 1 + 1 sleep(0.001) end end # 全てのスレッドの終了を待機 threads.each(&:join)
結果は以下の通りです。
| 通常のループ処理 | Threadを使用 | |
|---|---|---|
| 実行時間 | 12.992 秒 | 1.416 秒 |
| メモリ使用量 | 0.08 MB | 334.86 MB |
Threadの方が高速に終わりました。
sleep中にGVLが解放され、他のThreadが実行可能になり、並行処理の効果が現れたためです。
Threadごとのスタック管理のためメモリ使用量は依然として高いものの、I/O等のBlocking処理においてはThreadを使用することでパフォーマンスが向上することがわかります。
Fiber
FiberとはRubyにおける軽量な言語スレッドです。 1Threadに対してRubyの言語レベルで複数のFiberを作成できます。 Fiberはユーザランドでの切り替えが可能なので、OSのスレッド切り替えに比べて高速であるという利点があります。
Fiberを使用した非同期のI/Oフレームワークとして、Asyncがあります。
Async
AsyncはFiberを用いた非同期I/Oフレームワークであり、Non-blocking I/Oであるという特徴があります。 そもそもBlocking I/Oとは、I/O操作が完了するまで処理が待機してしまうことを指します。 例えば、DBに対してReadを行う際に、DBからレスポンスが返ってくるまで処理が待機してしまい、後続の処理が進まなくなってしまいます。
それに対して、Non-blocking I/Oでは、I/O操作が完了するまで処理が待機するのではなく、他の処理を進めることができます。 そして、I/O操作が完了した際に、その処理を再開します。 例えば、DBに対してReadを行う際に、DBからレスポンスが返ってくるまで他の処理を進め、DBからレスポンスが帰ってきたらその処理を再開します。
AsyncではこれをFiberを用いて実現しています。 I/O操作が発生した際にそのFiberを一時停止し、他のFiberに制御を移します。 I/O操作が完了した際に、元のFiberに制御を戻します。
以下ではBlocking処理であるsleepを用いて、Asyncの動作を確認してみます。
require 'async' Async do Async do puts "Task A: 開始" sleep(1) puts "Task A: 終了" end Async do puts "Task B: 開始" sleep(1) puts "Task B: 終了" end end
# 実行結果 Task A: 開始 Task B: 開始 Task A: 終了 Task B: 終了
通常であれば、Task A: 開始 -> 1秒待機 -> TaskA: 終了 -> Task B: 開始 -> 1秒待機 -> Task B:終了 の順番になるはずです。 しかし、結果を見てみると、Task Aが終了する前にTaks Bが開始していることが分かります。 Asyncによって、後続の処理が待機せずに実行されていることが分かります。
AsyncとThreadの性能検証
今度はFiberベースのAsyncとThreadによる性能の違いを検証してみましょう。
# Asyncを使用した場合 Async do |task| TASK_COUNT.times do |i| task.async do 1 + 1 sleep SLEEP_TIME end end end
| 通常のループ処理 | Threadを使用 | Asyncを使用 | |
|---|---|---|---|
| 実行時間 | 12.992 秒 | 1.416 秒 | 0.251秒 |
| メモリ使用量 | 0.08 MB | 334.86 MB | 49.92MB |
Asyncの方がThreadよりも実行時間が非常に短く、メモリ使用量も非常に少ないことが分かります。 OSのネイティブスレッドと比較した際のFiberの軽量さを実感できました。
Falcon
FalconはAsyncをベースに構築されたRack互換のHTTPサーバです。 Railsアプリケーションで一般的に使用されているPumaはマルチプロセス/マルチスレッドで、1リクエストに1つのネイティブスレッドで処理します。
一方、Falconはマルチプロセス/マルチFiberで、1リクエストごとに1つのFiberを割り当てて処理することで、同時接続数が増加しても効率的にリクエストを捌くことが可能です。 これにより、特にI/Oバウンドなアプリケーションにおいて優れたパフォーマンスを発揮します。 本発表では、Falcon周りのエコシステムについての様々な発表がありましたが、ここではActive Recordの対応について紹介します。
Active Recordの対応
Railsはリクエストごとにスレッドを割り当てる方式がデフォルトとなっています。 ActiveRecordのDBコネクション管理もスレッド単位で行われており、1スレッドが1つのDBコネクションを永続的に保持する方式となっています。 Railsガイド
よって、Fiberを用いる場合には、ActiveRecordのDBコネクション管理をFiber単位で行うように設定を変更する必要があります。
# config/application.rb class Application < Rails::Application # Fiber単位でのコンテキスト分離をサポート config.active_support.isolation_level = :fiber # DBコネクションの永続的なチェックアウトを禁止 # (Fiber間でコネクションを適切に共有するため) config.active_record.permanent_connection_checkout = :disallowed end
1つ目の設定は、Fiber単位でのコンテキストを分離するためのものです。
デフォルトでは:threadとなりスレッド単位で分離されているため、:fiberに変更します。
2つ目の設定は、スレッドごとにDBコネクションを永続的に取得することを禁止するためのものです。
Fiberを使用する場合は、スレッドによるコネクションの永続的な保持を禁止し、Fiberが必要に応じてコネクションプールから接続を取得・返却できるようにするため、:disallowedに設定します。
性能検証
講演では、実際のRailsアプリケーション(DBアクセスを含む)でFalconとPumaの性能を比較した結果が共有されました。 自分でも同様のアプリケーションを作成して性能を検証してみました。
検証環境
- 環境
- Mac Apple M1 Pro
- macOS Sequoia 15.6.1
- コア数: 8
- メモリ: 32GB
- アプリケーション設定
- Rails 8.0.3
- Ruby 3.4.2
- サーバー設定
- Worker数 (プロセス数): 4
- Pumaのスレッド数: 3
- DBコネクションプール数: 12
- Rails 8.0.3
- Ruby 3.4.2
- DB
- MySQL 8.0
- 測定ツール: Apache Bench (ab)
- リクエスト数: 1000
- 同時接続数: [40, 100]
使用しているRailsアプリケーションは、発表で示されたものと同様に、リクエストに対してレコードを作成/更新処理を行うシンプルなエンドポイントです。
# config.ru require_relative "config/environment" # リクエストに対してPizzaレコードを作成/更新するエンドポイント run do |env| request = Rack::Request.new(env) pizza = Pizza.create!(name: "Margherita", status: "cold") pizza.cook! [200, { "content-type" => "application/json" }, [pizza.to_json]] end
# app/models/pizza.rb class Pizza < ApplicationRecord validates :name, presence: true def cook!(duration = 0.001) update!(status: "cooking") sleep(duration) update!(status: "hot") end end
測定ツールはApache Bench (ab)を使用しました。 以下のようにコマンドを実行することで、多数のリクエストを同時に送信し、スループットを測定することが可能です。
# 1000リクエストを同時接続数40で送信 ab -n 1000 -c 40 http://localhost:3000/ # 1000リクエストを同時接続数100で送信 ab -n 1000 -c 100 http://localhost:3000/
スループット (req/sec) の検証結果
| 同時実行数 | Puma | Falcon | 改善率 |
|---|---|---|---|
| 40 | 42.36 req/sec | 46.84 req/sec | +10.59% |
| 100 | 45.16 req/sec | 49.27 req/sec | +9.10% |
上記の結果から、FalconはPumaと比較して約9〜10%のスループット改善が見られました。 FalconはFiberベースのNon-blocking I/Oを活用することで、I/Oバウンドな処理において高いパフォーマンスを発揮していることが確認できました。
まとめ
今回はKaigi on Rails 2025でのKeynote「Building and Deploying Interactive Rails Applications with Falcon」を受けて、その前提となるFiberやFalconについて紹介しました。 実際に検証を行うことで、FiberやFalconの性能を実感できました。 実際に手を動かして試してみることで、より理解を深められました。 発表では、FalconにおけるActiveJob互換のAsync::Jobや、ActionCableによるWebSocket、Streamingの対応などの紹介もありました。 興味がある方はぜひ発表スライドや公式ドキュメントを参照してみてください。
参考資料
Techouseでは、一緒に社会課題の解決に取り組むエンジニアを募集しております。 ご応募お待ちしております。