こないだ、と言っても2週間くらい前の話なんだけど、社内でZeroMQMongrel2の勉強会をやった。Mongrelと言えば、俺がRails(たしか当時1.2くらいだったと思う)で仕事してた頃にアプリケーションサーバとして使ってたけど、最近だとThinとかPassengerとかUnicornとかの人気に押されてついぞ聞かなくなったアレだよなぁ、なんでPerlの会社の勉強会でMongrelなんだろう、と思ってたんだけど、Mongrel2はもはやRailsのアプリケーションサーバじゃなくて、通信にZeroMQなるものを使った汎用的なWebサーバになってたらしい。

大分野心的なプロジェクトではあるものの、今はZeroMQもMongrel2も「とりあえず出た」って感じらしく、今年一杯くらいは地雷原を突き進む気がある人だけ触るといいんじゃないかなという話だった。プロダクションで使うようなレベルになるにはもうしばらくかかりそうだけど、今なら各言語の実装も追い付いてないようなので、遊んでおくなら今のうち。いずれMongrel2が大流行したときに「faultierさん、是非本を書いてください!」ってお願いされることを夢見て色々ごにょごにょしてみたよ。

準備

何はともあれZeroMQとMongrel2をインストールする。とは言っても、Getting Startedの通りにインストールするだけ。例に書いてある奴は若干バージョンが古いので、それぞれの最新版を取ってきた方がいいと思う。ちなみにZeroMQはhomebrewにもFormulaがあった。pyzmqは無かったので自分で作るなどした。

今回はRubyで試すので、RubyのZeroMQバインディングも入れておく。これは普通にgem install zmqで入るはず。Rackのハンドラの例の方はffi-rzmqを使ってるんだけど、なんか手元の環境で上手くインストールできなかったのでそっちは試してない。まぁ今回やることにはどっちがどうとかあんまり関係ないのでzmqの方でいいか。

そこまでできたら今度はMongrel2の設定を用意する。なんでCで書かれてるはずのMongrel2がやたらとPythonのライブラリ入れまくるんだろうと思ったんだけど、Mongrel2の操作にはm2shというPythonで書かれたスクリプトを使うかららしい。設定の仕方が面白くて、まずはPythonで書かれた設定ファイルを用意して、それをm2shでsqliteのファイルに書き出し、それを使ってMongrelが起動する、というようになってる。だから多分m2sh相当のものをPerlなりRubyなりで用意してしまえば、別にPythonは必要ないはず。まぁ、面倒なので大人しくm2shを使う。設定ファイルはこんな感じにした。

# m2test.py
from mongrel2.config import *

main = Server(
    uuid ="2f62bd5-9e59-49cd-993c-3b6013c28f05",
    chroot="./",
    access_log="/logs/access.log",
    error_log="/logs/error.log",
    pid_file="/run/mongrel2.pid",
    default_host="localhost",
    name="main",
    port=6767,
    hosts=[
        Host(
            name="localhost",
            routes={
                r'/m2test': Handler(
                    send_spec="tcp://127.0.0.1:9997",
                    send_ident="70D107AB-19F5-44AE-A2D0-2326A167D8D7",
                    recv_spec="tcp://127.0.0.1:9996",
                    recv_ident=""
                )
            }
        )
    ]
)
settings = {"zeromq.threads": 1}
commit([main], settings=settings)

Mongrel2のexamplesに入ってたのを参考にした。なんとなくわかると思うけど、hostsの中のHandlerってやつが今から作るハンドラと通信する為の設定になる。これが出来たら、

$ mkdir run log tmp
$ m2sh init -db m2test.db
$ m2sh load -db m2test.db -config m2test.py
$ m2sh start -db m2test.db -host localhost

とかすると、Mongrel2が立ち上がる。http://localhost:6767/m2testでアクセスすると裏のハンドラに処理が渡るはずだけど、まだ作ってないのでこの時点ではレスポンスが返ってこず、延々待たされる。

プロトコルを調べる

Mongrel2は内部にアプリケーションサーバを持つわけではなく、基本的にやることはZeroMQを使った通信をするだけ。上に書いた設定だと、ローカルの9997ポートと9996ポートにZeroMQのソケットが用意されて、ハンドラは9997ポートからリクエストを受けとり、処理したら9996ポートにレスポンスを送ってやるようにする。ちなみにこのソケットは別にUnixソケットのことではなく、ファイルを経由したりプロセス内通信したりネットワーク越しに通信したり色々できるらしい。詳しくはZeroMQを調べてみるといいと思う。

リクエストのメッセージは、「センダのID コネクションのID パス ヘッダの長さ:ヘッダ,ボディの長さ:ボディ」の形式で飛んでくる。実際にはこんな感じ。

70D107AB-19F5-44AE-A2D0-2326A167D8D7 2 /m2test 542:{"PATH":"/m2test","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/m2test","PATTERN":"/m2test","Accept":"application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5","Accept-Charset":"Shift_JIS,utf-8;q=0.7,*;q=0.3","Accept-Encoding":"gzip,deflate,sdch","Accept-Language":"ja,en-US;q=0.8,en;q=0.6","Cache-Control":"max-age=0","Connection":"keep-alive","Host":"localhost:6767","User-Agent":"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.62 Safari/534.3"},0:,

んで、レスポンスは「センダのID コネクションIDの長さ:コネクションのID HTTP/1.1 ステータスコード ステータスメッセージ ヘッダ ボディ」という形式で返す。例えばokと返すだけのレスポンスならこんな感じ。

70D107AB-19F5-44AE-A2D0-2326A167D8D7 1:3, HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 2

ok

つまりハンドラが何をすればいいかというと、9997ポートから来た上の形式のメッセージをパースしてリクエストを判断し、処理結果を下の形式に加工して9996ポートに送ってやる、とこういうことです。

ミニマムなハンドラを作る

そしてこちらが調理済みのハンドラになります(料理番組風)。

#!/usr/bin/env ruby
# coding: utf-8

require 'zmq'

sid   = "70D107AB-19F5-44AE-A2D0-2326A167D8D7"
con   = ZMQ::Context.new
rsock = con.socket(ZMQ::UPSTREAM)
ssock = con.socket(ZMQ::PUB)

rsock.connect('tcp://127.0.0.1:9997')
ssock.connect('tcp://127.0.0.1:9996')
ssock.setsockopt(ZMQ::IDENTITY, sid)

loop do
  str = rsock.recv
  sender, conn_id, path, str = str.split(' ', 4)
  ssock.send "#{sender} #{conn_id.size}:#{conn_id}, HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nok", 0
end

こいつとMongrel2を起動させといて、http://localhost:6767/m2testにアクセスすると、okとそっけない返事が返ってくる、というだけのハンドラ。「今夜は帰したくない」とか「結婚しよう」とかいうリクエストを投げても「ok」って返してくれます。まぁ「別れよう」でも「ok」って返ってきますけど。

上のコードだとreceiveしたメッセージのセンダIDとコネクションIDしか見てないけど、ヘッダとボディをパースすればRackアプリに渡すENVを作ることができるし、Rackアプリが返すレスポンスの仕様は決まってるのでそれをMongrel用のレスポンスメッセージに変換してやるのも簡単にできるわけで、そこまですればMongrel2と連携できるRackハンドラが作れる、というわけ。あとは、このハンドラ自身でリクエストを処理しなくても、スレッドを一杯作ってその中でRackアプリの処理をさせて、ハンドラ自身はプロセス内通信でリクエスト/レスポンスの中継役になってやるとかすれば、ワーカをがんがん増やせるとか、そんな風にもできる。

あと、Mongrel2とハンドラ間はZeroMQで通信してるだけなので、お互いが生きてるか死んでるか、何個あるのか一個もないのか、などについて何も感知しない。ので、急に負荷が上がったらプロセスやサーバを増やして緊急投入してもMongrel側の設定は変更する必要なかったりとか、デプロイ時にはもう一個ハンドラプロセスを立ち上げて起動し終えたら古いプロセスを殺すとかしてやればダウンタイム無しでデプロイできたりとか、するんじゃないのかな。多分。

あ、ちなみに今回作ったものはGistに置いといた。