普段 Controller や Model を書いているぶんには、Rails の下で何が動いているかを意識することはあまりありません。
ところが、Rails アプリにリアルタイムチャットや通知のような「つなぎっぱなし」の機能を入れようとすると、急に「Unicorn だと相性が悪い」「Action Cable なら大丈夫」といった話が出てきます。

この記事では、そうした話を理解するための土台となる Rails の足元の仕組みを、レストラン にたとえて整理します。

1. Rack:注文票の共通フォーマット

Rack は、Ruby の Web サーバーと Web アプリをつなぐ共通の約束事です。

call(env) を持ち、[ステータス, ヘッダ, ボディ] を返すオブジェクトなら Rack アプリ

class HelloApp def call(env) [200, { 'content-type' => 'text/plain' }, ['Hello']] end end 

レストランでいうと 注文票のフォーマット です。フォーマットが決まっているので、ホール(Unicorn、Puma)と厨房(Rails、Sinatra)を自由に組み合わせられます。

注文票が厨房に届くまでには、ミドルウェア(入口の警備員や受付)を通ります。

お客さん → 警備員(Rack::Attack) → 受付(Rack::Cors) → 厨房(Rails) 

2. 層:ホール → 受付 → 厨房

Web サーバー(Unicorn / Puma) ホール:お客さんを迎え、店員を割り当てる Rack + ミドルウェア 注文票と、受付・警備 アプリケーション層(Rails) 厨房:Controller や Model が動く 

ここでいう「アプリケーション層」は、OSI 参照モデルのものとは別物で、Rails のコードが動いている段階のことです。

3. プロセスとスレッド:店舗と店員

プロセス = 店舗(専用の厨房・冷蔵庫を持つ) スレッド = 店員(同じ店の冷蔵庫を共用する) 
プロセス(店舗) スレッド(店員)
メモリ 別々 共有
作るコスト 重い 軽い
怖いところ メモリを食う 共有データの取り合い
x = 0 Thread.new { x = 100 }.join p x # => 100(共有しているので書き換わる) y = 0 Process.wait(fork { y = 100 }) p y # => 0(別の店舗なので影響しない) 

店員同士が冷蔵庫を共有していると、「2人が同時に卵の数を数えて、在庫数がずれる」といった事故が起きます。これが スレッドセーフ問題 です。Ruby では、クラス変数やグローバル変数がこの「共有の冷蔵庫」にあたります。

4. ブロッキング:テーブルの横で立ち尽くす店員

スレッドが何かを待っている間、他のことが何もできずに止まっている状態を ブロッキング と呼びます。

店員:「ご注文は?」 お客さん:(メニューを30分眺めている……) 店員:(ずっとテーブルの横で待っている) 

普通のリクエストの待ち時間は一瞬なので問題になりません。困るのは、ロングポーリングや WebSocket のような「つなぎっぱなし」の通信です。

5. Unicorn:店員1人の店舗をたくさん出す

Unicorn マスター(オーナー) ├ ワーカー1(店員1人) ├ ワーカー2(店員1人) └ ワーカー3(店員1人) 
  • 1ワーカーが同時に処理できるのは1件だけ
  • 店舗が独立しているので安全。ただしメモリを食う

つなぎっぱなしの通信が来ると、こうなります。

ワーカー1〜3:全員がお客さんの横で待機 → 次のお客さんの相手が誰もできない 💀 

6. Puma:1店舗に店員をたくさん置く

Puma プロセス ├ スレッド1(店員) ├ スレッド2(店員) └ スレッド3(店員) 
  • Unicorn より少ないメモリで、多くのお客さんをさばける
  • コードはスレッドセーフである必要がある

Ruby には GVL があり、Ruby のコードを実行できるのは同時に1スレッドだけです(包丁が1本しかないイメージ)。それでも DB などを待っている間は包丁を手放すので、Web アプリなら十分に速く動きます。

ただし、つなぎっぱなしの通信ではスレッドが待機で埋まり、結局 Unicorn と同じく詰まります。

7. Action Cable:呼び出しベルで見回るホール係

(Rails上で動きます)

発想を変えて、テーブルの横で待つのをやめます。

ホール係(1人): 各テーブルに呼び出しベルを置く → ベルが鳴ったテーブルにだけ、さっと対応する 

これが イベントループ です。待機中の接続に誰も張り付かないので、接続が大量にあってもさばけます。

Action Cable は、Rack の rack.hijack で接続そのものを引き取り、このイベントループに渡します。そのため Web サーバーのワーカーはすぐに手が空き、Unicorn の上でも動きます。

弱点:ホール係が1件の重い仕事に没頭すると、全テーブルが待たされます。
→ Action Cable の上では重いクエリや sleep を避け、重い処理はバックグラウンドジョブに任せます。

まとめ

待機中の接続に張り付くもの 接続が増えると 重い処理
Unicorn プロセス メモリが足りなくなる 強い
Puma スレッド スレッドが足りなくなる まあまあ
Action Cable なし ほぼ平気 弱い