unoh.github.com

Rubyでネットワークサーバを書く

Mon Sep 24 23:24:57 -0700 2007

尾藤正人(a.k.a BTO)です

先日公開したブラウザだけでネットワーク対戦ゲームができるサイト「プラッシュ」では、 フラッシュとネットワーク通信を行う専用のXMLSocketサーバを開発しました。 このXMLSocketサーバはrubyで書かれています。 LLでデーモンを書く需要が、それほどあるとは思えませんが、デーモンを書く際に気をつけた点、工夫した点をまとめてみたいと思います。

なぜrubyを選んだのか

rubyを選んだのには理由は2つあります。

僕も昨今のRailsブームにのって個人的にRailsを使い始めていました。 プラッシュは完全に新規プロジェクトで環境を選択する事ができたので、迷わずRailsを選択しました。

では、なぜCのようなコンパイル言語で書かなかったのか。 速く動くものを開発するよりも、早く開発をしたかったからです。 Webサービスにとって開発スピードは最も重要な項目の1つです。 最初から速いものを作る必要はありません。早く市場に出さないとうまくいくかどうかもわかりません。 速くするのは後からでも全然構わないのです。

デーモンになる

デーモンになるために必要なのは次の4つです。

僕は次のようなメソッドを定義して実行してます。

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でネットワークサーバを書く時に気をつけた点についてまとめてみました。 他にも「こうした方がいいよ」とか「こういうのがあるよ」とかありましたら、いろいろ教えていただければと思います。