Chienomi

Ruby RackとFastCGI

FastCGIは今に至る礎にして、滅びかけている文化である。

検索すると散見される情報からすると、FastCGIがなんであるかを多くの人が理解していないようだ。

そのため、そもそもFastCGIの解説から、フレームワークなしに説明されることの少ないRackの話、そしてこれらを連携させた話をしたいと思う。

FastCGIとは何か

FastCGIそのものはネットワーク・プロトコルである。 FastCGIアプリケーションはデーモンであるため、必然的にFastCGIアプリケーションとやりとりするためにはネットワークが使われる。 そのため、ネットワーク・プロトコルなのだ。 CGIはプログラムの実行を前提としたインターフェイスなのでネットワーク・プロトコルではない。

FastCGIのプロトコルは結構ややこしい。 今日の一般的なアプリケーションサーバーはHTTPサーバーになっており、HTTPを理解する。 だから、アプリケーションサーバーに対して直接HTTPでリクエストすることもできるし、前段にウェブサーバーがいる場合はHTTPのリクエストを転送し、その応答を転送する、あるいはその応答を行うためのファイルディスクリプタを接続するだけで良い。

だが、FastCGIは専用のプロトコルを採用しているということだ。それがFastCGIである。

FastCGIがプロトコルであるということは、その両端に何者かがいなくてはいけない。 片方は当然ながらウェブサーバーである。そして、もう片方はアプリケーションである。

FastCGIはネットワークプロトコルであり、UNIXドメインソケットやTCPによって接続される。 ウェブサーバーから見ると、これらのコネクションを確立し、読み書きできる必要がある。

UNIXドメインソケットの場合、ソケットをお残しすることも可能だが、基本的にはこれら接続を受け付ける者とはデーモンである。 つまり、FastCGIアプリケーションは基本的にデーモンでなければならない。

以下はごく簡単なRuby FCGIアプリケーションである。

require 'fcgi'

FCGI.each do |fcgi|
  out = fcgi.out
  out.puts "Content-Type: text/plain; charset=ASCII"
  out.puts
  out.puts "Hello, world!"
end

FCGI.eachはソケットからの接続を待ち受け、接続を受けたらyieldする。 これを手動で書くと次のようになる。

require 'fcgi'

while fcgi = FCGI.accept
  out = fcgi.out
  out.puts "Content-Type: text/plain; charset=ASCII"
  out.puts
  out.puts "Hello, world!"
  fcgi.finish
end

いずれにせよ接続待ちでループするようになっており、このスクリプトを実行するとプロセスは走り続けるようになっている。 つまり、 FastCGIはプロトコルで、FastCGIアプリケーションはサーバーである

もちろん、FastCGIアプリケーションはサーバーなのだが、単なるサーバーではなく、webアプリケーションとしての機能を備えたサーバーであるのが普通だ。

なぜFastCGIか

そもそもFastCGIが登場した当時である1990年中頃というのはアプリケーションサーバーという概念は一般的ではなかった。

1994年にRFCで規格化されたCGIだが、UNIXの基本的な仕組みを利用しており、シンプルで合理的である一方、パフォーマンス面に問題がある。 だから、プロセスを永続化される手法を求めることになつた。

Perl/CGIに慣れ親しんだ世代であれば、mod_perl名に聞き覚えがあるはずだ。 これはApacheにPerlを組み込み、スクリプトもロードした永続的なPerlプロセスを生成することで生成コストを削減するものだ。 言い換えれば、汎用ウェブサーバーの一部分をアプリケーションサーバーにしてしまうものだと言っていい。

FastCGIはそのような直接的な手法とは違い、汎用性のある手法である。 統一的なインターフェイスによって、より高いスループットが求められる状況で永続的なプロセスを活用できるようになった。

しかし、人類はその後、「別に特別なプロトコルを用意しなくてもHTTPプロトコルがあれば良い」ということに気づいてしまった。そのため、HTTPプロトコルに追加ヘッダーを加える方式が採用されるようになるとFastCGIは使われなくなっていった。

同時に少し方式も変わった。その言語にとって利便性・親和性の高い機能を持つアプリケーションサーバーが使われるようになったのだ。 FastCGIでは専用のサーバーを使うわけではなく、アプリケーション自体がサーバー機能を持つようになっていた。 このサーバー機能が外に出されると共に、フレームワークとしても機能するようになった。

もちろん、FastCGIのアプリケーションサーバーというのも生まれてもよかった。 だが、生まれなかった。その流れの中で、既にサーバーはHTTPでやりとりするようになっていたのだ。

そして、そうしたアプリケーションサーバーが一般化していく中でFastCGIはあまり使われなくなってしまった。 非常に大きな点としてOpen MarketによるFastCGI仕様書が消滅してしまったことだ。(ただし、アーカイブサイトは残っている)

一般的に各言語のFastCGIライブラリはC(libfcgi.so)またはC++(libfcgi++.so)のラッパーになっているのだが、この本家ライブラリの「本家」が残っていない。 そして、各言語のラッパーライブラリもだいたい放置されているのが現状だ。

そのため滅びゆくテクノロジーだが、完全になくなったものというわけではない。

例えば、アプリケーションサーバーという存在以上にモダンなRackは、標準でFastCGIをサポートしており、共有ライブラリがない場合は、自前で書かれたFastCGIのpure Rubyライブラリを用いてやりとりするようになっている。

Rack

Rackは母体となるサーバーの差異を吸収するためのRubyのライブラリである。 アプリケーションサーバーが変更された場合などでもコードに対しては最小限の変更で済むようになっさている。

Rackには標準でCGI, FastCGI, SCGI, LiteSpeed, Thin, Webrickのハンドラが付属している。 また、PumaやUnicornには、サーバーライブラリ側にRackのハンドラが付属している。

RackのCGIハンドラはRubyディストリビューションに含まれるCGIライブラリを使用していない。 FastCGIハンドラもfcgi gemを利用していない。

Rackを用いたCGIアプリケーションは次のようにして書ける。

require 'rack'

class HelloWorld
  def call(env)
    res = Rack::Response.new
    res.status = 200
    res["Content-Type"] = "text/plain"
    res.write "Hello, world!"
    res.finish
  end
end

Rack::Handler::CGI.run HelloWorld.new

Rack::Response#finishはRackの戻り値として適切な配列を返すだけなので、次のようにしても良い。

require 'rack'

class HelloWorld
  def call(env)
    [200, {"Content-Type" => "text/plain"}, ["Hello, wolrd!"]]
  end
end

Rack::Handler::CGI.run HelloWorld.new

これはCGIアプリケーションとして実行することができる。

RackとFastCGI

同じような要領でFastCGIアプリケーションを書くこともできる。

require 'rack'

class HelloWorld
  def call(env)
    [200, {"Content-Type" => "text/plain"}, "Hello, wolrd!"]
  end
end

Rack::Handler::FastCGI.run HelloWorld.new

見ての通り、CGIからFastCGIに変わっただけである(ハンドラの自動判別を使えば全く書き換えない方法もある)。 だが、それだけでは十分ではない。FastCGIはウェブサーバーと通信できる必要があるのだが、通信するためのプロセスがないのだ。そこでそのプロセスを起動する必要がある。

汎用性のある手法としてspaws-fcgiというプログラムがある。 これはサーバー機能を持たないFastCGIアプリケーションに対して通信を取り次いでくれる。

spawn-fcgi -s /tmp/testfcgi.sock /usr/local/bin/fcgibin.rb

これは外部のプログラムを使う例だが、Rackはその機能も自前で持っている。 それがrackupだ。FastCGIで必要なパラメータに関する情報が少ないが、

rackup -s fastcgi -OFile=/tmp/testfcgi.sock config.ru

のようにしてFastCGIインスタンスを動作させることができる。

ただ、spawn-fcgiが自分だけで必要なことを一通りまかなってくれるのに対し、rackupはそうはなっていない。 特に大きいのが、ソケットファイルを消してくれないことである。 この点から言えば、spaws-fcgiを使うほうが楽である。

ただし、Systemdを利用すればスクリプトを書く必要もなく、単にユニットを書くだけで目的は達成できる。

[Unit]
Description=FastCGI application for testing.
After=network.target

[Install]
WantedBy=default.target

[Service]
Type=simple
ExecStart=/usr/lib/ruby/gem/rack-2.2.2/bin/rackup -s fastcgi -OFile=/run/fcgi-test.sock /usr/local/opt/fcgitest/config.ru
ExecStopPost=/bin/rm /run/fcgi-test.sock
UMask=002
User=httpd
Group=httpd
Restart=always

こうした「Systemdと相性のいいプログラム」は意外と多い。

注意点として、先日の記事で示した通り、Rackを使えば常に変更が不要になるわけではない。 単純にcallメソッドを呼ばれれば、どういう条件であっても同じ結果になるスクリプトであれば変更は不要なのだが、マルチプロセスかどうか、マルチスレッドかどうか、プロセスが永続化するかどうかという点に左右されるプログラムの場合は、その部分は修正しなければならないことに注意が必要である。

Wrote on: 2020-02-12 17:01:00 +09:00