Hatena::ブログ(Diary)

Limitの日記 このページをアンテナに追加 RSSフィード

2013-12-25

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

| 02:38 | Server::Starterに対応するとはどういうことかを含むブックマーク 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が融合するお話。

sonotssonots 2014/09/17 21:17 人のブログにメモ:bundle exec unicorn すると pipe が引き継がれない

sonotssonots 2014/09/17 21:23 s/pipe/socket/

sonotssonots 2014/09/18 00:49 ちゃんと記事にしました => http://blog.livedoor.jp/sonots/archives/40248661.html

トラックバック - http://d.hatena.ne.jp/limitusus/20131225/1387993119
リンク元