0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails は HTTP をしゃべれない - Puma との関係を分解する

0
Posted at

はじめに

「Rails をデプロイした」というけど、サーバーで実際に動いているプロセスって何か、パッと答えられますか。
「Rails と Puma、何が違うの?」と聞かれると意外に詰まる。ここを分解していくと、他フレームワークにも通じる基本の型が見えてきます。

「Rails をデプロイした」の実態

ps を叩くと Puma しか見えない

本番サーバーで ps を叩くと、このような出力が出てきます。

$ ps aux | grep -E 'puma|rails' | grep -v grep
deploy  12345  puma 6.4.2 (tcp://0.0.0.0:3000) [my_app]
deploy  12346  puma: cluster worker 0: 12345 [my_app]
deploy  12347  puma: cluster worker 1: 12345 [my_app]

Puma しかいません。「Rails」という名前のプロセスは、どこにも存在しない。

見えているのは Puma のマスタープロセスと、その子プロセスの worker が2本。実際にリクエストを処理しているのは worker たちです。「Rails のコード」はこの worker の中に、ただロードされているだけ。プロセスとしては Puma、というのが実像です。

Gemfile を見ると別々に書かれている

新規 Rails アプリを作ると、Gemfile にこう書いてあります。

gem "rails"
gem "puma"

別々の gem として書かれている。Rails と Puma は、別のプロダクトで、別のリポジトリで開発されてます。

  • rails/rails — Rails 本体
  • puma/puma — Puma 本体

チームも別。しかも Rails 本体の gemspec を見ると、Puma は必須依存ではなく "デフォルトの選択肢" として Gemfile に書かれているだけです。要するに、Rails は Puma がなくても gem としてはインストールできる

Puma は差し替えられる

gem "puma" を消して gem "unicorn" に置き換えても、Rails は動きます。設定ファイル(config/puma.rbconfig/unicorn.rb)を用意し直せば、サーバーだけを丸ごと入れ替えられる。

この差し替え可能性が、両者が別物であることの何よりの証拠です。もし密結合していたら、こんな交換は不可能なはず。

Rails 本体は HTTP をしゃべれない

そもそも TCP を listen できない

Rails 単体では、こういうことができません。

  • TCP ポート(3000番など)を listen する
  • 生の HTTP バイト列(GET /users/1 HTTP/1.1\r\n...)をパースする
  • クライアントに HTTP バイト列を送り返す

Rails のソースコードには、これらの実装が存在しない

試しに bin/rails console を叩いてみると、Rails のクラスは全部ロードされるのに、ポートは何も開きません。つまり Rails は、サーバー機能を持たない Ruby ライブラリの集合体、というのが正しい姿です。

HTTP は全部 Puma が扱ってる

TCP を listen して、HTTP を解釈して、Ruby が扱える形に変換するのは、全部 Puma の仕事です。

ブラウザ
   │  HTTP over TCP
   ▼
┌──────────────────────┐
│  Puma プロセス         │  ← HTTP をしゃべる担当
│                       │
│  ┌─────────────────┐  │
│  │  Rails のコード   │  │  ← Ruby メソッド呼び出しに反応
│  │  (Ruby の世界)   │  │
│  └─────────────────┘  │
└──────────────────────┘

Puma が外の HTTP の世界と内側の Ruby の世界の境目に立って、翻訳係をやっている。

役割分担を並べるとこんな感じ。

やること Puma Rails
TCP ポートを listen する ×
HTTP バイト列をパースする ×
Ruby のメソッド呼び出しに変換する ×
ルーティング(URL からアクション決定) ×
DB からデータを取る ×
Ruby オブジェクトを返す ×
レスポンスを HTTP バイト列に組み立てる ×
TCP で書き戻す ×

入りと出はいつも Puma、真ん中の Ruby の仕事だけが Rails。この分担を覚えておくと、いろんな場面で効きます。

Rails ができるのは "呼ばれたら返す" だけ

Rails 側から見ると、外の世界はこう見えます。

  • 誰か(=Puma)が Ruby のメソッドを呼んでくれる
  • リクエスト情報が Ruby のデータで渡される
  • Rails は Ruby のオブジェクトを返す

これだけ。HTTP のことは何も知らないし、扱えない

技術的には Rails.application.call(env) がこの "入口" になっています。試しに rails console から自分でこれを呼び出すと、HTTP サーバーを起動していないのに Rails のレスポンスが返ってくるのが確認できます。Rails が本当に "Ruby メソッド呼び出し" にしか反応しないことの証明です。

それでも両者はつながっている、Rack という薄い規約

じゃあ Puma と Rails、どうやってやり取りしてるのか。

その間には Rack という薄い規約が挟まっています。

  HTTP 世界              Ruby 世界
     │                       │
  [Puma]  ─── Rack ───  [Rails]
              ↑
       両者をつなぐ薄い規約

Rack は「データの形式だけを決めた約束事」です。「渡すもの」と「返すもの」の形がキッチリ決まっているだけ。この規約に従っていれば、どのサーバーとどのフレームワークでも組み合わせられます。

  • 同じ Puma で Rails でも Sinatra でも Hanami でも動く
  • 同じ Rails を Puma でも Unicorn でも Passenger でも動かせる

規約が挟まっているから、両者は疎結合になっている。「Puma と Rails の間には橋がある」という感じです。

まとめ

  • 「Rails をデプロイする」の実態は、Puma プロセスの中に Rails のコードをロードして立てているだけ
  • Rails 本体は HTTP をしゃべれない。TCP listen も HTTP パースも Puma がやっている
  • Puma と Rails は 別の gem、別リポジトリ、差し替え可能なくらい疎結合
  • 両者は Rack という薄い規約でつながっている

「Rails は魔法の巨大なフレームワーク」というイメージから、少し輪郭が見えたんじゃないかと思います。実態は「Puma プロセスの中にロードされている、Ruby オブジェクトの集まり」です。

この分解ができていると、実務でも役に立ちます。「500 エラーが出た」ときも、HTTP レイヤーの問題(Puma 側の設定?)か Ruby レイヤーの問題(Rails のコード?)かで切り分けられる。「Rails が遅い」も、Puma のスレッド不足なのか、Rails の中の DB クエリなのかで見るところが変わる。

そしてこの構造は他フレームワークでも同じ。Django なら Gunicorn、Node.js なら Express の実行環境、Java なら Tomcat。「サーバー」と「フレームワーク」は必ず分かれていて、規約でつながっている。言語が変わっても、この形は繰り返し出てきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?