2007-06-05 優しいRailsの育て方 [長年日記]

[Rails] mod_proxy_balancer と Rails との連携まとめ

熊井ちゃん、Apache2.2 + mod_proxy_balancer + mongrel_cluster というRails 運用環境のデファクト(※1)の設定に関する調査結果をまとめるよ。

(※1 2007年日本モデル)

かなり長いので、答えだけ知りたい方は末尾へどうぞ。頑張って読む人は、お茶とお菓子を準備してからゆっくりどうぞ。ちなみに、その場合のお菓子のお奨めは、セブンイレブンの「卵たっぷりふわふわロール」(155円/個)です。湯布院のB-speakのようなふわふわ感が紅茶にピッタリです。

用語定義

用語説明
サーバ二層型リクエストの割り振り用と処理用の2つのサーバに分ける構成
フロントサーバクライアントからのリクエストを受け取るWebサーバ
バックサーバ実際にリクエストを処理するWebサーバ

今回の場合、Apache2.2 がフロント、mongrel(cluster) がバックとなる。

メリット

リクエストの入り口を1箇所にしつつ、実際の処理は異なるプロセスまたは別ホストに委譲することができるため、スケールアウトが容易になり、アクセス数の増加に強くなるのが二層型のメリットである。

種類

ホストで稼動するWebアプリケーションに着目すると、設定は以下の3つに分類できる。

番号種類ホストの条件難易度
1一戸建てタイプこのRailsアプリのみが稼動 (port80を独占)
2マンションタイプ複数のWebアプリが稼動☆☆
3二世帯住宅タイプ同一ホスト名で複数のWebアプリが稼動☆☆☆☆☆☆☆☆☆☆☆

以下、それぞれのタイプにおける設定方法を記す。

1. 一戸建てタイプ

そのアプリ用に専用のマシンを準備できるケース。例えば、アクセス数が少ないβリリース時などは mongrel を直接80ポートで運用することもあるだろう。そして、負荷の増加、またはマルチコアを活かすという次の段階で、cluster 化した mongrel を扱う必要に迫られた場合、このタイプになる。この場合、フロントの仕事はバック(Rails)への割り振りだけだが、そのためにわざわざ Apache2 を持ち出すのは仰々しいと感じるかもしれない。そんな人にお奨めしたいのが Pound サーバだ。いきなり Apache から話が逸れてしまうが、このケースだとリアルでお奨めである。

Pound + mongrel

Pound はリバースプロキシ用のWebサーバであり、特化しているだけあって、必要最低限かつ直感的で簡単な設定で済むため、敷居が低いのが魅力だ。それでいて、デジタル証明書も扱うことができるため、個人的にお気に入りのフロントである。例えば、ホスト 219.106.253.22:80 でリクエストを待ち、localhost:3000 のバックへ投げる場合の設定ファイルはこれだけある。

config/pound.cfg

ListenHTTP
  Address 219.106.253.22
  Port    80
END

Service
  BackEnd
    Address 127.0.0.1
    Port    3000
  END
End

起動

# pound -f config/pound.cfg

フルスクラッチでも書けそうなくらい簡単である。

Pound + mongrel(cluster) + SSL

mongrel がクラスタ化してる、かつ、デジタル証明書を使う場合はこうなる。

config/pound.cfg

ListenHTTPS
  Address 219.106.253.22
  Port    443
  Cert    "/usr/share/ssl/certs/server.pem"
End

Service
  BackEnd
    Address 127.0.0.1
    Port    3000
  END
  BackEnd
    Address 127.0.0.1
    Port    3001
  END
  # 以下、好きなだけ列挙
End

2. マンションタイプ

1のような恵まれた環境はまれで、βバージョンで運用する場合など、1つのホストに複数のWebアプリを稼動させることが多い。ここでいよいよ、Apache2.2 + mod_proxy_balancer の出番となる。こういう場合、各Webアプリケーションにサービス用のホスト名を割り振り、VirtualHost 化するのが無難である。IPアドレスは有限だが、ホスト名を増やすのはタダだから遠慮することはない。

名前
アプリケーション名nksk (適当な識別子)
VirutalHost名nksk.wota.jp
フロントのIPアドレス219.106.253.22
バックサーバlocalhost:3000 - 3002

このようなアプリケーションがあるとして、nksk.wota.jp へのアクセスを全てバックへ投げる設定は以下のようになる。

httpd-vhosts.conf

<VirtualHost 219.106.253.22:80>
  ServerName   nksk.wota.jp

  ProxyPass        / balancer://nksk/
  ProxyPassReverse / balancer://nksk/

  <Proxy balancer://nksk/>
    BalancerMember http://127.0.0.1:3000 loadfactor=20
    BalancerMember http://127.0.0.1:3001 loadfactor=20
    BalancerMember http://127.0.0.1:3002 loadfactor=20
  </Proxy>
</VirtualHost>

balancer の後ろにある "nksk" はバランサの識別子である。設定ファイル内で同じ文字列でありさえすればよいため、アプリケーション名と同じである必要はない。だが、混乱を避けるために一緒にしておく方がよいだろう。バックの各サーバに優先度をつけたい場合は、loadfactor の値で調節する。

3. 二世帯住宅タイプ

そして一番やっかいなのが二世帯住宅タイプだ。これは現実世界もWebサーバ界も一緒である。このタイプは「2. マンションタイプ (VirtualHost)」に落とし込むことが可能であるため、特別な事情(多くの場合は金銭面)がない限りはそちらの構成にする方がよい。しかし、残念ながらビジネスにおいては時としてその金銭面が一番重要なファクターになってしまう。具体的には、「デジタル証明書がその稼動ホスト用の1つしかない(買えない)」という状況があるだろう。この場合、VirtualHost で運用することができないため、証明書があるホストにエイリアスまたはディレクトリを切るというパラサイト作戦しかない。ここで我々の前に立ちはだかるのが「パスのギャップ」という大問題である。

パスのギャップ

アクセスサーバ言い分
クライアントhttp://wota.jp/nksk/フロント从*・ゥ・从<ガーとそのまま "/nksk/" をバックパス
バックリl|;´∀`l|<でも、nkskコントローラはないんだよ

このギャップを埋めるために、以下のどちらかを選ぶことになる。

  • A) バックでパスを考慮する
  • B) フロントでパスを変換する

3. 二世帯住宅タイプA (バックでパスを考慮する)

まずは、フロントは本当に割り振るだけで、バックで "/nksk/" を考慮する方向でやってみる。proxy_balancer で "nksk" を素通し(明示)する設定は以下の通りである。

httpd.conf

ProxyPass        /nksk/ balancer://nksk/
ProxyPassReverse /nksk/ balancer://nksk/

<Proxy balancer://nksk/>
  BalancerMember http://127.0.0.1:3000/nksk loadfactor=20
  BalancerMember http://127.0.0.1:3001/nksk loadfactor=20
</Proxy>

まず、1,2行目で、受理するパスを "/" から "/nksk/" に変更する。Alias みたいなものだと思えばよい。そして、BalancerMember の設定で、バックを呼び出すときに "/nksk" プレフィクスを明示する。解説は割愛するが(※1)、前者の末尾には "/" があり、後者の末尾には "/" がない点には注意が必要である。

(※1 理解してないから)

これにより、先ほどのギャップの図の通り、バックへはトップページへのアクセスに "/nksk/" が渡されることになる。Rails 側でこれを受理する一番楽な方法は、routes をいじってそういうアプリにしてしまうことだ。

通常のroutes

ActionController::Routing::Routes.draw do |map|
  map.top     '', :controller=>"top", :action=>"index"
  map.connect ':controller/:action/:id'
end

nkskプレフィクス型のroutes

ActionController::Routing::Routes.draw do |map|
  map.top     'nksk/', :controller=>"top", :action=>"index"
  map.connect 'nksk/:controller/:action/:id'
end

これは確実であるが、3つの問題点がある。

  1. パス情報が埋め込まれている
  2. 画像、CSS、JSといった public 以下の静的ファイルが見えない
  3. ダサイ

問題1

1は、運用するパスに変更があった場合に(例えば "nksk" -> "nacky")、routes ファイルを修正する必要があるからだ。また、パスの情報が httpd.conf と routes.rb の両方にあるのがDRYでない。だが、変更があったら修正すればよい、と割り切れば、問題はない。

問題2

通常のページ遷移は問題ないが、画像ファイルを表示しようとすると"/nksk/images" へのアクセスとなり、問題2に直面する。しかし、これには "public/nksk" → "public" への symlink を用意するという裏技がある。いやいや、うち Windows ですけど、みたいな?という場合にも、"public/*" を "public/nksk/*" へ移動させることで際どく受かっている(※1)。

(※1 "public/*" へのアクセスは発生しないため)

問題3

そして問題3であるが、やっぱりなんかダサイ気がする。しかし、これも気分の問題だけなので割り切ることも可能である。泥臭い手法を拒むのはプログラマの資質であり欠点でもある。時として、舞美のように気にせず、何も考えず行動すべきだろう。

以上より、「バックでパスを考慮する」方法は、プログラマとして選択し辛いものの、完全かつ健全で確実に今すぐできる、という点においては十分な選択肢である。

3. 二世帯住宅タイプB (フロントでパスを変換する)

で、プログラマが選択したくなるのはこちらだろう。フロント側で "/nksk/" を削って、バックには今まで通り "/" を渡す作戦である。

httpd.conf

ProxyPass        /nksk/ balancer://nksk/
ProxyPassReverse /nksk/ balancer://nksk/

<Proxy balancer://nksk/>
  BalancerMember http://127.0.0.1:3000 loadfactor=20
  BalancerMember http://127.0.0.1:3001 loadfactor=20
</Proxy>

これだとバックは nksk フリー状態で、従来通りの routes で、従来通りの処理を行うだけで済む。しかし、これには一つ問題がある。Rails 特有の問題と言ってもよいが、Rails では url_for などに代表されるリンク情報には絶対パスが用いられるのである。例えばこの状態で、"/member/list/" → "/member/show/1" という一般的なリンクを辿ってみると、問題は一目瞭然である。(なお、外からアクセスするため、最初の URL は "/nksk/member/list/" になる)

パスのギャップ

アクセスサーバ処理
クライアントhttp://wota.jp/nksk/member/list/フロント从*・ゥ・从< "/nksk/" を削ってバックパス
バックリl|*´∀`l|<"/member/list/" を処理。リンク先は "/member/show/1" だよ♪
リンク辿るhttp://wota.jp/member/show/1フロント从*・ゥ・从< "/member/" なんてサイトないよ

relative_path plugin

これを解決するのが cuzic さん作の relative_path plugin である。バック(Rails)がURL作成時に絶対パス("/member/show/1")を利用するのが問題なので、相対パス("../show/1")を作るようにすれば解決するはずだ、という主張である。同 plugin を利用すると、url_for が plugin 提供の相対パスバージョンに置き換わり、リンクや静的ファイルへのパスが全て相対パス化されるのだ。設定レスで nksk パス問題が一気に解決されるとは、なんとエレガント!

relative_path によるギャップの解消

アクセスサーバ処理
クライアントhttp://wota.jp/nksk/member/list/フロント从*・ゥ・从< "/nksk/" を削ってバックパス
バックリl|*´∀`l|<"/member/list/" を処理。リンク先は "../show/1" だよ♪
リンク辿るhttp://wota.jp/nksk/member/show/1フロント从*・ゥ・从< "/nksk/" を削ってバックパス
バックリl|*´∀`l|<"/member/show/1" を処理♪

しかし、使ってみると relative_path にはいくつか問題があることがわかった。

問題点

  1. RFC 違反
  2. named route, Controller#url_for に未対応
  3. absolute_path => true の処理

問題1は、RFC的にはリダイレクトに相対パスを利用することは許されていないという点である。しかし、世の中のブラウザでは殆ど動作するし、実害はないのでこれは気にしなくてよいと思う(※1)。問題2が一番やっかいである。先ほどの routes の例で行けば、"top_url" などが使えないのだ(絶対パスになる)。これは routes で色々やってる人には致命的である。さらに細かく言えば、"absolute_path => true" を無視する処理も、特別な事情があってそうしたい場合には厳しい。

(※1 いや、RFCには絶対に従うべきだ!という人は、その元気で相対パスを許すドラフトを是非作って下さい)

absolute_path plugin

relative がだめなら absolute!ということで、やはり最初の路線に戻ることになるが、実は Rails にはこの辺の相対パスを処理する機構が組み込まれている。具体的には apache + fastcgi の場合には、上手く相対パスを処理してくれるのである。absolute_path は、その仕組みを上手く乗っ取りつつ、パスの情報も apache 側から渡してしまおうというプラグインだ。必然的に、二世帯住宅タイプAにあったDRY原則違反という問題点も解消される。このプラグインは "HTTP_X_URL_ROOT" という環境変数に入っている文字列をURLのプレフィクスとして利用するので、httpd.conf は以下のようになる。

httpd.conf

ProxyPass        /nksk/ balancer://nksk/
ProxyPassReverse /nksk/ balancer://nksk/

<Proxy balancer://nksk/>
  RequestHeader set X_URL_ROOT '/nksk'
  BalancerMember http://127.0.0.1:3000 loadfactor=20
  BalancerMember http://127.0.0.1:3001 loadfactor=20
</Proxy>

環境変数をユーザが渡してきた場合に穴がありそうな気がしたけど、最終的なURL情報にINSERTされるだけなので実害はないよね、熊井ちゃん。

SSL との融合

これまでは説明を簡単にするために http での例を示したが、話の根本は SSL の場合であった。その場合、今までの設定を 443 のディレクティブへ移動させるだけでよい気がするが、それだけだと、バックが生成するURLが http のままになってよろしくない。この問題は、フロントがSSLの情報を削ってしまうためで、バック側としては http なのか https なのか判断できない点に起因する。従って、一般的な解決策は「httpsでのアクセスである」という情報をフロントからバックへ通知することとなり、Rails では "HTTP_X_FORWARDED_PROTO" という環境変数で実装されている。よって、apache2.2 + mod_proxy_balancer + mongrel(cluster) + absolute_path plugin + sslという環境における最終的な設定ファイルは以下のようになる。

httpd-ssl.conf

<VirtualHost _default_:443>
  # 中略
  ProxyPass        /nksk/ balancer://nksk/
  ProxyPassReverse /nksk/ balancer://nksk/

  <Proxy balancer://nksk/>
    RequestHeader set X_FORWARDED_PROTO 'https'
    RequestHeader set X_URL_ROOT '/nksk'
    BalancerMember http://127.0.0.1:3000 loadfactor=20
    BalancerMember http://127.0.0.1:3001 loadfactor=20
  </Proxy>

httpd-vhosts.conf

<VirtualHost 219.106.253.22:80>
  # 中略
  ProxyPass        /nksk/ balancer://nksk/
  ProxyPassReverse /nksk/ balancer://nksk/

  <Proxy balancer://nksk/>
    RequestHeader set X_URL_ROOT '/nksk'
    BalancerMember http://127.0.0.1:3000 loadfactor=20
    BalancerMember http://127.0.0.1:3001 loadfactor=20
  </Proxy>

バランサの識別子は全体でユニークのような気もするので、その場合は一方を "balancer://nkskssl/" とかで。

参考

本日のツッコミ(全5件) [ツッコミを入れる]
_ minimum2scp (2007-06-06 10:27)

3. 二世帯住宅タイプA (バックでパスを考慮する)<br>のケースですが、<br>バックサーバの mongrel_rails に --prefix /nksk オプションをつけてあげるとうまくいきませんか?

_ Yugui (2007-06-06 10:47)

> いやいや、うち Windows ですけど、みたいな?という場合<br>Windowsでもジャンクションでしょう! ジャンクション大好き。<br><br>Vistaにはsymlinkがあるらしいと噂に聞いた。

_ dara (2007-06-06 15:20)

2.マンションタイプの場合ですが、pound+mongrel 構成でもpound.cfg に HeadRequire "Host: .*vhost1.example.jp.*" や HeadRequire "Host: .*vhost2.example.jp.*" を指定することで VirtualHost の振り分けができます。うちではこれを使っています。<br><br>また、pound の設定次第で3Aタイプのフロントにも使えるような気がします(実際に試したことはないです)。

_ 舞波 (2007-06-06 18:31)

> minimum2scp<br>バカな。--prefix なんて都合のいいオプションがあるわけ・・・ほんまや!しかも relative_url_root 使ってるから動作は100%保障されてるわけだ。<br>plugin で頑張ってた僕たちの立場は一体?強いて plugin のメリットを上げれば、バックの種類に依存しないから lighty にしてもそのまま動作する。みたいな?<br>もし lighty にも prefix みたいなオプションがあったら、速攻で plugin をレポジトリから消して、二人で駆け落ちしようね、熊井ちゃん。<br><br>> Yugui<br>ジャンクションて何すか?(...ググり中...)。ほー、NTFSにはハードリンクあるんですか!いいね、これ。<br>あー、でも世の中にはまだ Windows3.1 ユーザが多いからちょっと無理かも。。。(必死)<br><br>> dara<br>Pound にも VirutalHost の設定があるんすね。<br>でも、マンションの他の住人が mod_php とかな人で Apache 民族だったら・・・、みたいなねぇ。<br>いや、その場合は二層でなくて、ハイブリッド三層ステンレス方式(未定義語)でいいのか!<br><br>Request<br>`-- Pound(Front)<br> |-- Apache2(mod_proxy_balancer)<br> | |-- Apache2(mod_php)<br> | `-- misc(Back)<br> |-- MongrelCluster1<br> `-- MongrelCluster2<br><br>Pound の 3A フロントは今度実験してみる。あざっす!<br><br>あとはフロント候補として、最速らしい Nginx を試してみたいけど、Aapache は遅いとして(脳内)、<br>Pound, LiteSpeed, Nginx あたりのフロントの速度差は、バックに来た時点で誤差になっちゃう予感。<br>なんとなれば、Rails の routes がボトルネックだから。よって、routes の c 化を早く!>ドリコムの中の人。

_ cuzic (2007-08-06 14:22)

ここに書かれていた、Controller で url_for がうまく動かない件については修正しておきました。。<br><br>対応が遅れてしまいまして、すいません。


サイト内検索 (by Google)

| JRuby | Rails | Berryz | ℃-ute | エッグ | jQuery |

過去

2007年
6月
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30

未来

コンタクト