メッセージフォームのサポート (Nginx + FastCGI + spawn-fcgi + Rack + Ruby)
ウェブサイト開発
- TOP
- Old Archives
- メッセージフォームのサポート (Nginx + FastCGI + spawn-fcgi + Rack + Ruby)
あらまし
Mimir Yokohamaでついにお問い合わせ方法として「メッセージフォーム」が追加された。
なにがついになのか、なにをドヤっているのかと思うかもしれない。 まぁ、ドヤってはいないのだが。
実は私はかなり長い間ウェブアプリケーションをほとんど作っていない。 そして、今まで私が作ったウェブアプリケーションは、専用サーバーを持つサーブレットタイプか、もしくはCGIだった。
馬鹿にされがちなCGIだが、利便性は高く、頻繁にアクセスする性質を持たないアプリケーションには適している。
そして、そもそもウェブアプリケーションを作っていなかったのは、私が「事前生成戦略」の研究と実験に注力していたからで、 どちらかといえばウェブアプリケーションからは離れる方向にあった。 そして、ウェブアプリケーションを必要とするとしても大部分は静的ページとして提供できる方式を目指していたため、CGIで十分事足りたのである。
ちなみに、これまでウェブサーバーは
- Apache
- lighttpd
- delegate
- Nginx
という経過をたどっている。 Apacheは言うに及ばずlighttpdとdelegateはApacheよりもCGIが簡単だったので、「ほぼCGI」だった。
だが、時代は変わった。NginxはCGIをそもそもサポートしない。 私も新しい時代に対応する必要がある。
ちなみに、この作業は次の仕事のための実戦テストという意味合いもあった。
方針を考える
最も話が速いのはFastCGI Wrapである。
NginxはFastCGIをサポートしている。 FastCGIはプログラムをデーモンのように起動しっぱなしにする。
だが、通しで実行するプログラムとデーモンではそもそもの前提が違う。 そのためCGIプログラムをFastCGIとして動かすのはそれなりにハードルが高い。
そこでFastCGI Wrapの登場である。 FastCGIとして利用されるプログラムをFastCGI Wrapにする方式だ。 このラッパープログラムは要求に合わせて都度CGIプログラムをCGIインターフェイス経由で起動する。 結果的にFastCGIの意図は無視して従来型CGIを動作させるようにするというものだ。
この方法は結構出てくるのだが、基本的には既存のCGIプログラムを動作させる話である。
個人的な感覚としては、無駄なプロキシを噛ませるような方法を使ってまでCGIに固執したくない…というか、実はfcgi-wrapってそれなりにめんどくさい。
だったらFastCGI直というのもありかなぁ、と考えるわけだ。
ところが、やっぱりFastCGIはデーモン状のプログラムを想定しているわけで、やはり前提が違う。 要求として割と複雑なのか、デーモン化に関してはspawn-fcgiに担ってもらって、さらにRackを使う、というのがどうやら主流らしい。
だいぶ話が複雑になってきた。
サーバーはNginxである。NginxはFastCGIインターフェイスを経由してFastCGIプログラムにパラメータを渡し、応答を受け取る。
FastCGIプログラムはデーモンである。 Rubyでは次のようにしてFastCGIプログラムを書くことができる。
#!/usr/bin/ruby
require "fcgi"
FCGI.each {|request|
= request.out
out .print "Content-Type: text/plain\r\n"
out.print "\r\n"
out.print Time.now.to_s
out.finish
request}
あるいは、CGIライブラリ互換インターフェイスを使うことで、#each_cgi
の中身はまるっきりCGIと同じにすることもできる。
#!/usr/bin/ruby
require "fcgi"
FCGI.each_cgi {|cgi|
= cgi['name'][0]
name puts cgi.header
puts "You are #{name} " if name
puts "Connecting from #{cgi.remote_addr}"
}
spawn-fcgiはこのデーモン部分を担う。
つまりeach
してる部分を担ってくれるわけだ。
プロセスとしてCGIインターフェイスで起動するわけではないので、fcgiwrapほどの互換性はない。 感覚はCGIに近いが、インターフェイスは意識する必要がある。
Rackはミドルウェアと呼ばれている。これはまずFastCGI抜きで話そう。
Rackはインターフェイスを担っている。 今までプログラムはCGIなり、あるいはFCGIなり、さらには各種フレームワークやサーブレットの様式(例えばSinatraとか)で書いていた。
Rackはこれらの違いを吸収するモジュール設計のものだ。 Rackに準拠したプログラムを書いておけば、たとえ愛用のフレームワークがディスコンになっても、サーバーが変わっても安心、というわけだ。
だが、Rack自身はサーバーではないからサーバーがいるのだが、Rack組み込みのサーバーというのはもう完全にRuby世界の住人だ。 だってRackはRubyのWebアプリケーションインターフェイスだから。
Passengerというソフトウェアがあって、これはwebサーバーのモジュールとしてRackに対応する。 Apacheでは比較的簡単だけれど、Nginxだと結構きつい。
そこでRackに対応したサーバーを立ててサーバーとサーバーでやりとりさせる、という方式がすごく現代的。 直接にRack経由でプログラムとやりとりするのはRackに対応したサーバーだけれど、Rackに対応したサーバーにwebサーバーとしての機能を持たせると大変なので、「本物のwebサーバーに矢面に立ってもらって、RackサーバーはあくまでRack対応に特化」というわけである。
Rackに特化したサーバーとしては(別にRackだけではないんだけど)、Webrick, Mongrel, Puma, Thin, Unicornあたりがある。
しかしRackでやりとりする方法があればいいので、FastCGI + Rackという方法もある。 それはRack側でFastCGI経由で受け取って、応答するためのハンドラが用意されている。
つまり、Unicornのようなサーバーを立てる代わりの手段としてFastCGIが使える。 FastCGIもデーモンを必要とするので別にFastCGIにすることで間に挟まってるものを減らす効果はない。 ただ話が楽になるだけである。
Unicornはむちゃくちゃ速いので、UnicornでUnixドメインソケットを使えば形式とししてはspawn-fcgiでUnixドメインソケットを使っているのと一緒だし、やっていることははるかに高度になる。 これが超モダンなやり方である。
が、あえてのFastCGI。 理由は管理する要素数を減らすためである。必要がないのにいかついものを使うことはしない。 これはサーバー運用のコツでもある。
なお、Rackに関してはかなり情報が少ない。 なんらかのフレームワーク…というか、ほぼRailsのバックエンドとしてのRackの話だけで、Rack単独の話ってない。 そして、FastCGIを使う話もない。これもだいたいなんらかのアプリケーションが「使ってる」あるいは「使わせる」話になる。
なんというか、みんなそんなに自分でプログラム作るってことをしてないのか… 世の中エンジニアたくさんいるのに、WordPressとRailsだけで満足なのか…
そんなわけで情報が猛烈に足りていない中、FastCGIとRackについて勉強することになったわけだ。
なお、Nginxでアプリケーションとやりとりする方法に関してはDiscourceで散々やったので経験済みだ。
なぜRackなのか
もちろんこのことからもわかるようにRackはなくても構わない。 spawn-cgiも使用せず単独のFastCGIアプリケーションを開発するのは容易である。
私が気にしたのはRubyのfcgiライブラリは2013年から更新が止まっているとい点だ。 また、Arch LinuxではfcgiライブラリはAURにもなく
# gem install --no-user-install fcgi
とするよりない。
ベーシックな機構であるFastCGIそのものが廃止になるようなことは考えにくいが、NginxのCGIの扱いのように消極的なサポートへと変遷する可能性はある。 その場合にアプリケーションの書き直しが発生してしまう。
Rackは現在主流であり、新規採用例も多い。 Rackが廃止になると影響を受ける範囲も非常に広いので今後10年は安泰だと思われる。
そこでFastCGI+Rackという構成にしたわけだ。 この場合でもRackはFastCGIをネイティブサポートしているわけではく、fcgiライブラリを使ったハンドラを同梱しているだけなのでfcgiライブラリは必要となる。実はこれを回避したかったのだが、結局はできなかった形だ。
とはいえ、この状態であればFastCGIを捨ててUnicornに移行するのも難しくはない。
とりあえずやってみる
Nginx
location / {
root /var/www/testapp;
fastcgi_pass /var/run/fcgi-testapp.sock
fastcgi_index testapp.rb;
include fastcgi_params;
}
Rack Application
#!/usr/bin/ruby
require 'rubygems'
require 'rack'
= ->(env) {
app = Rack::Request.new(env)
req = Rack::Response.new
res
["Content-Type"] = "text/plain; cahrset=UTF-8"
res.status = 200
res.write "Hello, World!"
res.write req.params.inspect
res
.finish
res}
Rack::Handler::FastCGI.run app
Request
のほうはインターフェイスに絡むけれど、
Response
は単純に#finish
でRackに沿った配列を返すための便利クラス。なくてもいい。
spawn-fcgi
# spawn-fcgi -U http -s /var/run/fcgi-testapp.sock /var/www/testapp/testapp.rb -n
試してるうちは-n
つきにしてフォアグラウンドで実行するのが楽
実用的にする
起動スクリプト
#!/bin/bash
exec spawn-fcgi -U http -s /var/run/fcgi-testapp.sock -P /var/run/fcgi-testapp.pid /var/www/testapp/testapp.rb
forkingなので停止・再起動の制御のためPIDファイルを作る。
Systemd Unit
[Unit]
Description = FastCGI Rack Test Application
After = nginx.service
[Service]
Type = forking
PIDFile = /var/run/fcgi-testapp.pid
ExecStart = /usr/local/sbin/fcgi-testapp.bash
ExecStop = kill $MAINPID
[Install]
WantedBy = multi-user.target
forkingなので$MAINPID
がそのままでは使えないため、PIDFile
で指定しておく。
Nginxのあとに起動しておいたほうがいいような気がしたけど、なくても構わない。
アクセスが激しい場合は逆にNginxの前に起動したほうがいいだろう
spawn-fcgi自体にはアプリをリロード、再起動するような機能はない。
おまけ
S-NailがSubjectも本文も、UTF-8をちゃんとエンコードしてくれるのですごくびっくりした。
「mailxとは違うのだよ!!!」ってことか。 さすがSMTPやPOPやIMAPにも対応しているだけのことはある。
ここの部分(MIMEエンコーディング)も自分でやるつもりだったので、かなり省力化された形。
今回の構築は他にも色々やったのだけれど、共有して意味のある部分はこれくらいのものだろう。