limitusus’s diary

主に技術のことを書きます

Server::Starterに対応するとはどういうことか

StarletやStarmanと組み合わせてよく使われているServer::Starterですが、普段気にしないような部分を読む機会があったのでメモ。

Server::Starterは --port (TCP) や --path (Unix Domain Socket) を渡すとこれでlisten(2)して起動するworkerに引き渡してくれる。
これはfork(2)とexec(2)によってファイルディスクリプタを引き継ぐことにより実現されているが、ファイルディスクリプタそのものをどのように引き渡しているのか、という問題。
exec(2)により実行バイナリは差し換わってしまうので、プログラム中の変数により引き継ぐことはできない。

Server::Starterではこれを環境変数SERVER_STARTER_PORTにより実現している。

おおよそ以下のような感じ。

FD = integer
EQ = '='
SEMICOLON = ';'
SERVER_STARTER_PORT = PORT_DESCRIPTION ( SEMICOLON SERVER_STARTER_PORT )
PORT_DESCRIPTION = PORT_NAME EQ FD
PORT_NAME = character+

PORT_NAMEはソケットの種類によって違って、TCPであればポート番号(8080)とかホスト名:ポート番号(localhost:8080)、Unix Domain Socketであればそのパス(/path/to/app.sock)となる。

PODにも書いてあるServer::Starter::server_portsはこの環境変数をほどいてPerlのハッシュにして返してくれる小さなサブルーチンとして実装されているので、workerがPerlであればこれを使えばよい。
workerは環境変数から受け取ったファイルディスクリプタの番号をfdopen(3)して(あるいはそれすらもせずに)そのソケットを利用することができる。

ところで別にworkerがPerlであることは要請されていない。


Rubyunicornというフレームワークがある。
あまり詳しくは知らないが、Webサーバとして動作させるのに用いることができるらしい。
ちょっとソースを読んでみたところ、無停止graceful restartの仕組みとして、

  1. fork(2)する
  2. 子プロセスは自分自身にexec(2)した後、親プロセスからファイルディスクリプタを引き継いでaccept(2)できるようにする
  3. 子プロセスが親プロセスに(自身が無事起動できたことを通知する意味で)シグナルを送信
  4. 親プロセスが終了する

といったプロセスを経ていることがわかった(SIGUSR2を送ったときの挙動)。
このときファイルディスクリプタの引き継ぎにUNICORN_FDという環境変数を用いて、フォーマットは異なるもののServer::Starterと同じようなことを行っていた。

unicorndaemontoolsの配下で動かそうと思ったとき、masterをgraceful restartしようとSIGUSR2を送信すると、親プロセス(旧worker)が終了した時点で子プロセス(新worker)がinitプロセスの養子に入ってしまう。
そうするとdaemontoolsの管理から外れてしまうことから、daemontools対応を諦めていた経緯があるようだ。Unicorn general mailing list ()
foremanというのを使う手もあるらしい(これは全然知らない)最近のRubyなWebアプリの構成 | monoの開発ブログ

daemontools + Server::Starter というのは様々なところで使われており、安心して使えるというのもあって、この仕組みにunicornを載せることを考えてみる。
unicornのおおよそのロジックとして、

  1. ENV['UNICORN_FD'] の存在をチェック
  2. なければここでbind&listen、あればfdopen(3)相当して、旧masterプロセスにシグナル送信
  3. サーバとして動く

となっているため、ENV['SERVER_STARTER_PORT']をENV['UNICORN_FD']に変換してunicornを起動してやるとServer::Starterから起動することができてしまう。
変換コードはunicorn.rb自身に書いてしまってもいいし、アプリ内でhookとして書くこともできる。
Server::StarterがSIGHUPを受け取ったときは旧masterにはSIGTERMを送ってしまって構わない(--signal-on-hup=TERM)。

こうしておくと、unicornアプリの再起動ではSIGUSR2、daemontoolsのアプリではsvc -h(SIGHUP)という混在運用から逃れることができる。

変換は大体以下のようにやればよい。

# こんな風にServer::Starterから環境変数が渡ってくる
ENV['SERVER_STARTER_PORT'] = '80=3;443=4'
# 以下のように環境変数を変換する
ENV['UNICORN_FD'] = ENV['SERVER_STARTER_PORT'].split(';').map { |s|
    pf = s.split('=')
    pf[1]
}.join(',')

p ENV['SERVER_STARTER_PORT']  # ==> "80=3;443=4"
p ENV['UNICORN_FD']  # ==> "3,4"

という、シンプルだけどPerlRubyが融合するお話。