DEV Community

Kaziu
Kaziu

Posted on

レストランで理解する Rack・プロセス・スレッド・Unicorn・Puma・Action Cable

普段 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
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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


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

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

y = 0
Process.wait(fork { y = 100 })
p y  # => 0(別の店舗なので影響しない)
Enter fullscreen mode Exit fullscreen mode

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


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

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

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

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


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

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

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

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

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

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

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

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


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

(Rails上で動きます)

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

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

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

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

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


まとめ

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

Top comments (0)