尾藤正人(a.k.a BTO)です
先日公開したブラウザだけでネットワーク対戦ゲームができるサイト「プラッシュ」では、 フラッシュとネットワーク通信を行う専用のXMLSocketサーバを開発しました。 このXMLSocketサーバはrubyで書かれています。 LLでデーモンを書く需要が、それほどあるとは思えませんが、デーモンを書く際に気をつけた点、工夫した点をまとめてみたいと思います。
なぜrubyを選んだのか
rubyを選んだのには理由は2つあります。
- Railsを採用した
- LLで早く開発をしたかった
僕も昨今のRailsブームにのって個人的にRailsを使い始めていました。 プラッシュは完全に新規プロジェクトで環境を選択する事ができたので、迷わずRailsを選択しました。
では、なぜCのようなコンパイル言語で書かなかったのか。 速く動くものを開発するよりも、早く開発をしたかったからです。 Webサービスにとって開発スピードは最も重要な項目の1つです。 最初から速いものを作る必要はありません。早く市場に出さないとうまくいくかどうかもわかりません。 速くするのは後からでも全然構わないのです。
デーモンになる
デーモンになるために必要なのは次の4つです。
- fork -> setsid -> fork
- ルートディレクトリに移動する
- umask をクリアする
- 標準入出力、標準エラー出力を /dev/null に向ける
僕は次のようなメソッドを定義して実行してます。
def daemonize exit! 0 if fork Process::setsid exit! 0 if fork Dir::chdir('/') File::umask(0) STDIN.reopen '/dev/null', 'r' STDOUT.reopen '/dev/null', 'w' STDERR.reopen '/dev/null', 'w' end
デーモンについては以前勉強会やった内容を公開していますので、よかったらそちらをご覧下さい。 UNIXデーモンを作ろう
全てのエラーを補足する(堅牢性)
デーモンはバックグランドで24時間動き続けないといけません。 デーモンが落ちてしまうと全くサービスが継続できなくなってしまいますから、多少のエラーが発生しても大丈夫なように作らないといけません。 幸いな事にrubyの場合は、ruby自体が落ちてしまわない限り、あらゆるエラーを補足することができますので、上位のところで全てのエラーを補足するようにするだけで問題ありません。
メモリリークに気をつける
Webアプリのプログラムのように短時間で処理が終了してしまうものは、メモリリークを気にする必要などほとんどありませんが、 デーモンのように24時間動き続けるようなシステムでメモリリークがあると、 最初は調子よく動いていても長い間実行するとメモリをたくさん消費して大変な事になってしまいます。 といってもrubyの場合はruby側でメモリ管理をしてくれるのでメモリリークする心配はあまりありません。
スコープ内にあるローカル変数はスコープから抜けたら勝手にrubyのGCが開放してくれるので気にする必要はありません。 気をつけないといけないのは永続的に使用しているオブジェクトになります。 永続的に使用しているオブジェクトに必要ないものが出てきたら忘れずに、Array#delete、Hash#deleteを呼ぶか、nilを代入するなどして、 rubyのGCが開放できるような状態にしてやる必要があります。
LLのGCのアルゴリズムに参照カウンタを採用しているものがありますが(php, perl, python)、GCのアルゴリズムが参照カウンタの場合、 循環参照のオブジェクトの開放ができなくてメモリリークになってしまう問題があります。 rubyのGCのアルゴリズムはmark-sweepになってるそうなので、この点は安心できます。 pythonは参照カウンタですが、循環参照のオブジェクトを開放する機構があるそうです。
IOを多重化する
ネットワークサーバは(inetdのようなものを使わない限り)複数コネクションを扱えないといけません。 通常ネットワークサーバが複数プロセスを扱う時は1コネクションに対して、 1プロセス or 1スレッドを生成するマルチプロセス/スレッドモデルで処理をするのが一般的です。
rubyで実装するので、プロセスモデルでコネクションをさばくには処理速度やリソース消費の観点から懸念がありました。 実際に試してないのでもしかしたら余計な心配だったのかもしれませんが。 スレッドモデルも考えたのですが、 rubyのスレッドはネイティブスレッドではなくruby自身が持っているもので、 複数コネクションをさばくにはお話にならないくらいコンテキストスイッチの速度が遅くて使い物になりませんでした。
そこでIO#selectを使ってIOを多重化することにしました。 それぞれ単純な役割を持った4つのスレッド「コネクションをacceptする」「ソケットから入力を読み取りキューに登録する」「接続が切れたコネクションを開放する」「キューから命令を取り出して実行する」で処理を行うようにしました。 次のようなプログラムで処理しています。
def main server = TCPServer.new(@port) Thread.fork { while true begin add_connection(server.accept) rescue Exception => error logger.fatal error end end } Thread.fork { while true begin read_instructions rescue Exception => error logger.fatal error end end } Thread.fork { while true begin manage_connection rescue Exception => error logger.fatal error end end } while true begin dispatch rescue Interrupt break rescue Exception => error logger.fatal error end end rescue Exception => error logger.fatal error exit(1) end
このように生成されるスレッドを限定する事でコンテキストスイッチの遅さは問題なくなります。 IOを多重化することでコネクションが増えても、1ソケット分のオブジェクトが増えるだけになり、 コネクションが増えてもそんなに負荷が上がらないような構造になっていると思います。
とはいえ、この方式にもいくつか問題点はあります。 1つは命令実行スレッドが命令をシーケンシャルに実行するので、1つ重い処理があると他の命令が実行されなくなってしまうので、 全体の処理に影響を与えてしまうことです。 これに関しては命令実行スレッドを増やしたり、命令に実行優先順位をつけるなどして対処することができると考えています。
もう一つはマルチコアのCPUで1つのコアしか活用できない点です。 rubyのスレッドはネイティブスレッドではないので、OSから見るとただの1プロセスにしか見えません。 なので、せっかくマルチコアのCPUを搭載していても1つのコアしか使用する事ができません。 これに関しては複数プロセスで動作できるようにしたり、少し作り込みが必要になってきます。
まとめ
rubyでネットワークサーバを書く時に気をつけた点についてまとめてみました。 他にも「こうした方がいいよ」とか「こういうのがあるよ」とかありましたら、いろいろ教えていただければと思います。