「Ruby なんて遅くて使えない」という意見が出ます。(昔、Java も似たようなことを言われましたっけ。)これに対して、Ruby 好きな人からは、「大抵の Web アプリではボトルネックは IO になるからアプリの言語は遅くても構わない」「CPU 時間よりも開発者の時間の方が重要」というような反論が展開されます。

Rails 厨にならないためにも、ここは Ruby に批判的な目を持って、この問題を考えてみたいと思います。

■ 前提
Ruby を採用するとなると Rails 絡みで Web アプリでしょうから、Web アプリについて考えてみます。(でも、DLR とか話に出てくるわけですから、クライアントで使う場合もそろそろ検証した方がいいと思いますけどね。)

■ Ruby は遅い
以下のサイトを見るとわかりますが、場合によっては、C 言語の数十倍から数百倍遅くなります。(Ruby 1.9/YARV が出てくれば、少しは事態は改善されるに違いない・・・と信じていますが。)

The Computer Language Benchmarks Game
http://shootout.alioth.debian.org/

本当に Web アプリでは言語の速度は問題にならないのでしょうか? Kodougu(Rails で開発している Web 上で動作するモデリングツール)で図を開く処理を計測してみました。Rails のログを見ると、サーバの処理時間のうち、40% は DB 待ちでした。一回のアクセスで、少なくとも数十の SQL が乱れ飛びます。html や javascript の生成は 40% 程度を占めています。残りは、フレームワークやコントローラで消費されています。(このログは、Rails がとっているログなので Rails の中しか計測されません。)

この手のアプリは、ネットワークがボトルネックになることが多いのですが、今回は ab(Apache Bench)を使って、並列性 1 でベンチマークをとっているので、ネットワークはそれほどボトルネックになっていないと考えています。(処理は重いので、CPU はいっぱいいっぱいでした。Kodougu がそこそこ人気が出たら、今のサーバは即パンクしますね。パンクすると困るのですが、人気が出なくても困るので・・・どうしよう。)

言語の処理速度は一概には言えないのですが、上記のベンチマークによれば、Python だと Ruby の 2 ~ 3 倍、Java だと 20 ~ 30 倍になります。たとえば、Kodougu をそのまま Java で実装すれば、パフォーマンスは今の 2.5 倍くらいにはなります。(プログラミング言語の処理速度の向上がそのままトータルなパフォーマンスには現れないところがミソです。)

もっとも、サーバを 3 台余分に準備すれば(運用コストはサーバ代、人手も合わせて年間 100 万増くらい?)、Ruby のままで同等のパフォーマンスを得られるということでもあります。最近のアプリはステートレスなので、台数はわりと簡単に増やせます。DB サーバは増やすのは難しいですが。

実際のところ、Web アプリはキャッシュを使ったりして高速化をするので、言語のパフォーマンスはアプリのトータルなパフォーマンスの中ではそれほど大きな位置を占めません。まじめに最適化してしまえば、Kodougu を Java で実装してもパフォーマンスは 20 ~ 30% 位の差しか出ないのかなと考えています。Kodougu はモデリングツールということもあって、Web アプリの中では例外的に計算的な(≒ Ruby に不利な)種類のアプリです。通常の Web アプリではもっと言語間の差が小さくなるんじゃないかなと感じています。

■ Ruby の生産性
以下のような意見があります。

日本 Ruby 会議 2007 の Dave Thomas のスピーチのログより

ある研究によれば、生産性はそれぞれのプログラマでそれぞれ違う。でも、あるプログラマに着目すれば、そのプログラマが時間あたりに書けるコードの行数は、プログラミング言語によらず決まっている、たとえば一年に50,000行なのだそうだ。行数が決まっていたら、どの言語で一番多くのことを達成できる?そう、Rubyだよね。



なるほど。そういう意味では、Ruby の生産性は高いです。通常、モデリングツールのコード行数は Java や C# で 10 ~ 100 万行程度(製品コードのみ)になります。Kodougu は 5000 行程度(製品コードのみ)です。Kodougu は Web であるがゆえに、機能的に優れた面とそうでない面とがあるので単純に比較することはできません。少なくとも生産性が 10 倍になることはありませんが、50 ~ 100% 程度の向上はあると思います。

(ただ、アプリの規模が大きくなればなるほど、Java でも Ruby でも変わらなくなる気がしますが。)

■ トータルなコストパフォーマンス
Java を使った場合と比較して、サーバ運用コスト増が開発費の 20% ~ 30% 位に収まってくれるなら、Ruby はアリだと私は考えています。保守フェーズに入ったら、サーバ運用コスト増がもっと大きくても大丈夫なんじゃないかとも考えています。

■ まよめ
さて、破滅的に遅い Ruby ですが、Web アプリで Ruby が許される本当の理由は、Web アプリの負荷の質にあります。たとえば、ゲームのリアルタイム 3DCG の処理を Ruby で書くのは自殺行為です。こうしたアプリは常に CPU に負荷をかけるアプリです。(CPU でやらせること自体が自殺行為といううわさもありますが。)Web アプリは、動的コンテンツとの割合次第ですが、キャッシュなどを仕掛ければ、たまに CPU に負荷がかかるけど、いつもは比較的 CPU が暇なアプリに分類されます。今のところ、Ruby はそういう負荷の質を持った用途で使うべきものです。

クライアントでも、Ruby が使えるかどうかの判断基準は、結局負荷の質をどうとらえるかという問題に落ち着くのかもしれません。ただ、総じていうと、サーバの CPU はいつもそこそこ忙しいですが、クライアントの CPU はいつも暇です。ゲームとかやってない限り。CPU の高速化に伴い、多くの処理が負担でなくなってきていることも事実です。Web アプリのクライアントが JavaScript でも書けることから、クライアントで Ruby が活躍できる範囲はむしろ大きいのではないかなと感じています。

そうそう、Twitter もRails を使っているそう(参考)です。リクエスト数が多そうなサービスなので、Rails を使うのはチャレンジャーだと思うのですが、意外といけているようです。凄い・・・。

Posted by あかさた
Rails を使ったアプリに限った話ではありませんが、ファイルをアップロードする機能を Web アプリにつけたときに、ファイルを DB に入れるか、Web サーバにファイルを保存するかについてです。

私は Web サーバに保存すべきと考えていました。Web サーバとアプリケーションサーバ(Rails のサーバ)を分けている場合、静的ファイルはWeb サーバが処理して、動的コンテンツはアプリケーションサーバが処理してというようなことをする場合があります。DB に入れると、アプリケーションサーバを経由しなくてはならないので、こうした構成が取れなくなります。ただ、Web サーバを複数台にする場合、ファイルアップロード時に全ての Web サーバにファイルをコピーする手間がかかるというデメリットがあります。(バックアップの手間も一手間増えます。)

もともと私は、DB に静的コンテンツをいれると、Web アプリのレスポンスが遅くなるという問題が発生すると考えていました。ただ、この問題は Rails のキャッシュ機構を使えばある程度回避できます。この場合、静的コンテンツも全てアプリケーションサーバが処理することになります。それぞれのサーバが DB からファイルを取り出してキャッシュをするので、ファイルの管理は楽になります。

もっとも、動画アップロード&配信サイトとか、データ量が多い場合は、ファイル置き場とアプリケーションサーバは分けざるを得ない(ストレージにも IO にも問題が発生する可能性があるので)ので、この方法はとりづらいですが。

所詮はケースバイケースですが、一つあたりのファイルがそれほど大きくなくて、ファイルの総容量が 100GB 以内に収まるのなら(最大容量は適当)、DB に入れるというのもアリかなと思うようになりました。Kodougu では、アイコン画像などがアップロード可能なのですが、今は DB に入れています。

Posted by あかさた
Ruby.NET Compiler ですが、C# などの言語との相互接続性(interoperability)を確立したようです。つまり、Ruby 上で C# で宣言したクラスのインスタンスを生成したり、C# 上で Ruby で宣言したクラスのインスタンスを生成したりできるようです。継承などがどうなるのかは試していません。

eval、パフォーマンス、日本語はどうなるのかなど、気になる点は多いですが、5 月には VS.NET 上での開発も実現しており、機能的には使えるレベルまできた気がします。

Rails を移植するつもりもあるようです。個人的には全然うれしくないですけど。長期的には意味があるかもしれませんが、向こう数年間では、.NET で Rails が動いてもそれほど意味がない気がします。Mono があるとはいえ動作する OS が制限される、gem は?、数々のプラグインは?、などなど心配事は尽きません。メリットがあるとすれば、ウェブインタフェース(オンライン処理)は Rails、バッチは C# というような使い分けかもしれません。

それに、Ruby はパフォーマンスがあまり良くないのでマシンを並列することも多いと思いますが、有料の OS はマシンごと or CPU ごとに課金することが多いので、そういう意味でも難しいと感じています。

個人的には、クライアントに可能性を求めて欲しいところです。ただ、.NET DLR の Ruby(IronRuby)との位置づけはどうなるのでしょうか。(処理系が増えること自体は、歓迎すべきことでしょう。)

(相反することをいうようですが、個人的には JRuby 上では Rails は動作して欲しいです。Kodougu を移植したいので。EMF とか使いたいものがたくさんあります。)

ま、今後に期待ということで。

Posted by あかさた
いまさら、Rails の Scaffold の話です。

コントローラの宣言時に以下のように書くと、アクション(メソッド)にモデル名をサフィックスとして追加してくれます。(edit なら、edit_model_name になります。)
scaffold :model_name, :suffix => true

便利ですね・・・。AdminController とか作って、一時的に各種データの出し入れの管理画面を作るときに使用しています。

そもそも、私は Scaffold はコード生成時にしか使っていませんでした。サフィックスをつけられるなんてことも知らずに今まで過ごしていました。なんて損していたんだろうか。(--;

Posted by あかさた
ここしばらく Apollo や Silverlight など、クライアントサイドの新技術が登場しています。Rails のブログを見ていたら、Slingshot という製品が紹介されていました。

Slingshot goes public
http://weblog.rubyonrails.com/2007/5/2/slingshot-goes-public

この製品は、Rails で開発したアプリを、オンラインでもオフラインでもシームレスに切り替えて使えるようにするもののようです。ただ、この製品が筋のいい製品かどうかはわかりません。コンセプトは納得できます。ネットがつながっていないと、Web アプリはまったく役に立たなくなるので、オフラインでも使える Web アプリができたら便利ですね。

ここしばらくのクライアント技術の傾向を思いつくままに上げてみます。
・ Web アプリとデスクトップアプリの境目をなくす(共通)
・ Web アプリとデスクトップアプリの技術的な隔たりをなくす(Apollo、Silverlight)
・ クライアント/サーバの技術的な隔たりを(割と)なくす(Silverlight)
・ アプリのインストールを意識する必要が無い
・ マルチプラットフォーム
・ 一応リッチになってきた(?)

究極的には、コードオンデマンドでインストールを意識することはなく、オンラインオフラインを意識することなく、Web / デスクトップを意識することなく、ブラウザの種類も OS の種類もデバイスの種類も意識することなく、アプリケーションというものは使えるようになるということでしょうか。

すばらしい夢の世界・・・といいたくなるところですが、IT エンジニアにとってはしんどい時代になりそうだなぁ。。。

Posted by あかさた
act_as_authenticated に自動生成された UserTest の test_should_reset_password が失敗するという現象が発生しています。具体的には以下のコードで失敗します。
# 前からこんな現象あったかなー???

def test_should_reset_password
  users(:quentin).update_attributes(:password => 'new password', 
    :password_confirmation => 'new password')
  assert_equal users(:quentin), User.authenticate('quentin', 'new password')
end

エラーの発生条件:RadRails 0.7.1 上で Run Unit Tests を実行する、rake test:units を実行する

ただし、上記テストメソッド単体を実行すると、テストは成功することから、他のテストメソッドの実行結果に影響されて失敗していることがわかります。DB の内容を見ると、users(:quentin) の login(ユーザ名)は 'quentin' ではなく 'quentin2' になっています。このことから、test_should_reset_password は、test_should_not_rehash_password に依存しているようです。

Ruby(と Rails)の UnitTest フレームワークの性質をよく知らないのですが、テストメソッドを実行するたびに DB の初期化が実行されればこういった問題は発生しないと考えられるので、DB の初期化を行うタイミング(=fixtures メソッドが呼ばれるタイミング)はテストクラスを読み込む一度きりということでしょう。

とはいえ、原因がわかっても、テストコードの修正は簡単ではありません。他のテストの実行の有無で結果が変わってしまうテストを変わらないコードにするというのはなかなか難しいことです。とりあえず以下のように考えてみました。

このテストのやりたいこと:レコード更新時にパスワードを変更する(:password_confirmation があるときは、パスワードを更新することを確認したい。※)

※ そういう意味では、更新時に :password のみを変更して :password_confirmation を変更しない or 間違っているテストが無いことは少し疑問です。もっとも、生成時のテストはあるからそれで問題はありませんが。

修正案1:レコード更新時に login も更新する
def test_should_reset_password
  users(:quentin).update_attributes(:login => 'quentin', 
    :password => 'new password', :password_confirmation => 'new password')
  assert_equal users(:quentin), User.authenticate('quentin', 'new password')
end

メリット:修正は簡単
デメリット:テストコードの意図がやや不明瞭になる

修正案2:更新したユーザのインスタンスから名前を取得して、認証を行う
def test_should_reset_password
  users(:quentin).update_attributes(:password => 'new password', 
    :password_confirmation => 'new password')
  assert_equal users(:quentin), User.authenticate(users(:quentin).login, 'new password')
end

メリット:テストの意図はそのまま
デメリット:元と比べてコードがわかりにくい?(というか、なんか気持ち悪い・・・)

とりあえず、修正案2で行きます。


Posted by あかさた
Kodougu で必要だったので、Rails アプリ上で Ruby の eval を使用したときの安全性を調査していました。ご存知のとおり、eval 関数は、文字列で渡された Ruby ソースコードを実行するものです。しかし、外部からスクリプトを渡して実行しようとすると、セキュリティの観点から危険な場合があります。以下のような例(外部から渡された script が DB を操作している)です。

script = <<-EOS
  # ActiveRecord を操作する
  @metaelement = Metaelement.new
  @metaelement.name = "execute_eval"
  @metaelement.save
EOS

Thread.start {
  # $SAFE = 4 <- このコメントアウトを外すと SecurityError 例外発生
  eval(script)
}.join

デフォルトでは上記のコードは動作します。安全ではない eval です。しかし、$SAFE = 4 にして上記のコードを実行すると、SecurityError 例外が発生します。Ruby のセキュリティモデル Level 4 には、「汚染されていないオブジェクトの状態の変更」を禁止するルールがあるので、これに引っかかったようです。Level 4 では以下のようなコードでも SecurityError 例外が発生します。

script = <<-EOS
  @metaelements = Metaelement.find(:all)
EOS

Thread.start {
  $SAFE = 4
  eval(script)
}.join

では、Rails でも Level 4 で eval すれば安全・・・というと、ちょっと自信がありません。たとえば以下のようなコードは動作してしまいます。

script = <<-EOS
  Metaelement.connection # DB のコネクションを取得。
EOS

Thread.start {
  $SAFE = 4
  eval(script)
}.join

上記のようなコードの動作を許可すると、DB のユーザ、パスワードとかを取れてしまいます。見られたくない情報です。eval の用途によっては非常に危険です。危険な例としては、ページ上で eval で評価した結果を表示する場合などが挙げられます。Rails の全メソッドに rb_secure(1) とかを仕込めばいいのでしょうかね。(^^; (ありとあらゆる面から論外でしょうけど。)

Ruby on Rails でサンドボックスというのはさすがに無理ですかね~。。。


Posted by あかさた
Rails を動作させるプラットフォームとしては、scgi よりも Mongrel に力が入っているという話なので、Kodougu と同じくこのブログも Apache2.2 + Mongrel に変更しました。ついでに、Rails 1.2 に対応したのですが、ちゃんと動いているでしょうか・・・?

このブログについてはこちらを参照してください。ソースコードのチェックアウトなどもできます。テストもロクに書いていないひどいコードですが。

■ 2007/3/16 22:44 追記
従来はロードバランスしていなかったので、scgi サーバでは一つのコネクションしか処理できませんでした。アクセス数が少ないので、通常のブラウジングであればそれでも問題ないのですが、他のブログにトラックバック送信を行おうとすると問題になります。

送信先のブログが言及リンクチェックをする場合ですが、チェックのためにこちらのサーバに接続すると、ブログ投稿者がコネクションを占有してしまっているので、送信先のブログはコネクションが空くのを待つようになり、こちらのサーバは送信先のトラックバック完了待ちになって、デッドロック状態になります。投稿者の接続がタイムアウトすると、送信先の言及リンクチェックが完了し、トラックバックが完了します。

こんなばかばかしい現象も、ロードバランスをしたおかげで発生しなくなりました。

Posted by あかさた
もう朝が近いな・・・。

Kodougumod_proxy_balancer でロードバランスしたのですが、Ajax のレスポンスが 10 秒以上待たないと返ってこないという問題に遭遇してしまいました。具体的には、ブラウザ(FireFox)上でモデル要素を動かすと、Ajax リクエストを送信するのですが、これのレスポンスが 10 秒以上たたないと返ってこないという現象です。Apache の httpd.conf をいじって KeepAlive を外したら速くなったのですが、どうも腑に落ちません。

昔、IE では KeepAlive 関係でバグがあってレスポンスが返ってこないというのはあったようですが、今回は FireFox なのでちょっとそれはなさそうな予感がしています。

以下のバグが少し関係ありそうなのですが、効果がないと書いている対処法が、私のケースでは効果があったので別問題でしょうか。時間のあるときに Apache の bugzilla をあさってみますかね。やれやれ・・・。

Bug 37770 - proxy: error reading status line from remote server (null) | Apache | Bugs
http://www.gossamer-threads.com/lists/apache/bugs/314332

■ 追記(2007/3/4 13:24)
上記の Bug 37770 に関係があるかと思い、以下の記述にしたがって httpd.conf を修正してみました(つまり、KeepAlive を On にして、以下の記述を追加した)が、効果はありませんでした。うーん・・・。

mod_proxy - Apache HTTP サーバより:
[code:

SetEnv force-proxy-request-1.0 1
SetEnv proxy-nokeepalive 1]

当面は KeepAlive Off でいくことにします。しばらくはこういった問題がシビアになるほどアクセスが集中するとも思えないので。

○ 問題の整理
Apache(mod_proxy, mod_proxy_balancer)をフロント、Mongrel(Rails)をクラスタリングしてバックエンドに配置した状態で、ブラウザ(FireFox)上でページ遷移のない Ajax を多用した操作を行うと、Ajax レスポンスが返ってくるまで KeepAliveTimeout + サーバの処理時間程度の時間がかかってしまう。(私の環境では、KeepAliveTimeout は 15 に設定しているので、15 秒以上待たされる。)
しかし、ページ遷移のある操作は問題なく動作します。

Posted by あかさた
今日は、Kodougu 公開に向けて、Apache2.2 で mod_proxy_balancer を使ってロードバランスを実現させるということをしていました。つまり、フロントエンドを Apache(mod_proxy, mod_proxy_balancer)、バックエンドは Mongrel という Rails の世界では割と一般的な構成にしました。役割としては、Apache がロードバランスして、Mongrel がリクエストを処理します。Mongrel とは Ruby で書かれた Web サーバで、Rails アプリを動作させることに適しています。

なぜこのようなことをしているのかというと、理由は二つあります。ひとつはパフォーマンスとスケーラビリティに関係するものです。Mongrel は Rails アプリを動作させるには非常によい Web サーバ(Rails と相性がよくて高パフォーマンス)です。しかし、Apache と同じく 1 プロセス 1 コネクションであるため、クライアント(ブラウザ)に直接接続させてしまうと、大量のリクエストが発生したときに(Kodougu では大量のリクエストが発生する)、Web サーバがビジーになって、リクエストを受け付けるまでラグが発生する可能性があります。そこで、複数の Mongrel プロセスを起動して負荷を分散します。Mongrel はロードバランスはしてくれないので、Apache にロードバランスさせているというわけです。
もうひとつの理由は、Mongrel は Rails に特化した Web サーバなので、フロントには別のサーバをおいておきたいという思いがあります。そこで、普段使い慣れている(+ロードバランス機能を持った)Apache をフロントに置き、Mongrel をバックに配置しました。

さて、設定は楽勝なのですが、mod_proxy_balancer で一点はまりました。mod_proxy_balancer を使うために httpd.conf には以下のような記述を追加します。(実際にはもっとたくさんのバランサメンバがいます。)

ProxyPass / balancer://mycluster/
ProxyPassReverse / balancer://mycluster/
<Proxy balancer://mycluster/>
  BalancerMember http://127.0.0.1:3000 loadfactor=20
  BalancerMember http://127.0.0.1:3001 loadfactor=20
</Proxy>


balancer://mycluster/ の最後のスラッシュ(/)を書かなかったがために、ルート以外の URL にアクセスしたときにパーミッションエラーになってしまいました。
# これで何時間もはまってました。。。

気をつけませう。。。orz

Posted by あかさた