AP4R、Rubyで非同期メッセージング

第2回AP4RとRailsでつくる非同期アプリケーション

ちょっとしたニュース

今年11月にアメリカ東海岸のシャーロットで開催されるRubyConf 2007の発表枠応募にAP4Rが通りました! いい機会をもらえたので、がんばって発表してきます。

手を動かしてみましょう

第1回では、AP4Rの開発に至った背景、システムの構成例、信頼性を保証するSAF機能について見てきました。

そして、非同期処理を利用する場合の一般的な利点と注意点に触れた後、AP4Rの「堅牢」かつ「軽量」という特徴について解説しました。

今回は、Ruby on Railsで作られたウェブアプリケーションをAP4Rと連携させて、非同期処理を実装してみましょう。作成するアプリケーションの機能と、非同期化する箇所を選び出した後、コーディングやAP4Rの設定について解説します。

アプリケーションの機能、動作

ウェブ上のお店のアプリケーションを作成します。簡単のため、お店を訪れたお客さんはウェブブラウザから品物の名前を入力して、注文をおこなうことにします。その後、サーバ側では注文情報の保存と決済の2つの処理を順次行います。処理が完了すると、ブラウザに注文完了の情報が表示されます。

仮想的な問題点、非同期化するところ

さて、お客さんの注文処理の解析を行ったところ、注文を送信してから完了が表示されるまでに長い時間がかかっていることが判りました。よりくわしく調べると、決済処理は他社のシステムと連携しており、そこで時間がかかっていました。

そこで、決済処理を非同期化し、注文の処理の後には決済のためのメッセージだけを残すように変更することにしました。この非同期化により、お客さんへより早い応答を返し、お客さんのイライラは減ることでしょう。

加えて、決済のための他社のシステムがメンテナンスや障害により止まってしまうことがあっても、このお店は注文の受け付けを継続することが出来るようになりました。つまり、2つの処理の間を疎結合化したことにより可用性が増した、ということが言えます。

プロセス構成

今回作成するアプリケーションのプロセス構成を図に示します。

図1 プロセス構成図
図1 プロセス構成図

利用するソフトウェア

アプリケーションで利用するソフトウェアをまとめておきます。なお、筆者らの開発環境は次のようになっています。また、Windows XP、Ubuntu 7.04においても動作を確認しました(個々のソフトウェアのバージョンは若干異ります⁠⁠。

  • Mac OS X 10.4.10
  • Ruby 1.8.6 (2007-06-07 patchlevel 36)
  • RubyGems 0.9.4
  • MySQL 5.0.16
  • Ruby on Rails 1.2.3
  • AP4R 0.3.2
  • reliable-msg 1.1.0
  • mongrel 1.0.1

Ruby

Rubyは、オープンソースとして開発されている、オブジェクト指向の動的なプログラミング言語です。Rubyについての情報は、ウェブで検索することで多くの有用な情報を見つけることが出来ます。「オブジェクト指向スクリプト言語Ruby」のページには、ダウンロード、チュートリアル、ドキュメントへのリンクや、コミュニティの紹介など、Rubyに関する情報がまとめられています。

インストール前にRubyのプログラムに触れてみたい場合、try ruby! ⁠in your browser)は、ウェブブラウザだけでRubyの動作を体験することが出来ます。

Rubyのインストールは、Windowsであれば、One-Click Installerの利用が簡単です。Rubyforgeのダウンロードページから、最新のインストーラーをダウンロードしてください。インストーラーを起動したら、ウィザード形式でインストールを行うことが出来ます。Windows以外の場合は、ソースから、またはパッケージからインストールすることが出来ます。Rubyインストールガイドが参考になります。

Ruby on Rails

Ruby on Railsは、Ruby言語で書かれたウェブアプリケーションフレームワークです。画面からデータベースアクセスまでをカバーしていることや、DRY(Don't Repeat Yourself.)原則を徹底的に推し進めていることなどの特徴を持ちます。WEB+DB PRESS Vol.38や、Rubyist Magazineの連載記事Rubyist Magazine - RubyOnRailsを使ってみるなどに日本語でのまとまった情報があります。

インストールは、RubyGemsを用いる方法が簡単です。詳細は、上記のどちらかを参考にインストールをしてください。コマンドをひとつ打つだけでインストールは完了です。

% gem install rails --include-dependencies
(注)
HTTPのアクセスにプロキシを経由する場合、--http-proxy=http://your.proxy.host:port/のオプションを付け加えて下さい。
Unix系のOSで、インストールにroot権限が必要な場合は、suするかsudoで実行してください。

MySQL

業務DBおよびメッセージDBとして、MySQLを利用します。MySQLのインストールは、MySQL AB社のダウンロードサイトから、お使いのプラットフォームに合わせたパッケージ、もしくはソースコードをダウンロードしてインストールしてください。

AP4R

AP4Rは、RubyGemsを用いたインストールが簡単です。

% gem install ap4r --include-dependencies

AP4Rの情報は以下の場所にあります。

また、筆者らのブログにも、随時書き込みをしています。

同期アプリケーションの作成

それでは、アプリケーションの作成開始です。はじめにAP4Rを利用しない、同期アプリケーションをつくりましょう。最終的には、AP4Rと連携した非同期アプリケーションに拡張するので、作業ディレクトリの名前は非同期を彷彿とさせるasync_shopとしておきます。このディレクトリ以下に、Railsのルートディレクトリとしてas_rails、AP4Rのルートディレクトリとしてas_ap4rができる予定です。asはasync_shopの略です。

まずはrailsコマンドでプロジェクトを作成します。

% cd async_shop
% rails as_rails

テーブル設計

テーブル数が多くなると本質的な部分が見えにくくなるので、簡潔なものにしておきます。今回は、注文テーブルorders )と決済テーブルpayments )の2つを用意します。商品や顧客などの管理テーブルや注文明細テーブルは省略します。

図2 注文テーブルと決済テーブル
図2 注文テーブルと決済テーブル
注文テーブル
注文情報を保存するためのテーブルです。注文を受け付けるとレコードが生成されます。注文明細テーブルはつくらないので、1つの注文につき1つの商品となります。カラムは、注文IDid⁠、商品名item⁠、注文日時created_at )です。
決済テーブル
他社のシステムにて決済が無事完了すると、このテーブルにレコードが生成されます。カラムは、決済IDid⁠、注文IDorder_id⁠、決済日時created_at )です。

モデルとテーブルの作成

データベースの準備

MySQLにdevelopment、test、production環境用のデータベースを作成します。それぞれのデータベースに作成されるテーブルへのアクセス権限は、デフォルトで出力されるconfig/database.ymlの値に合わせておきます。必要であれば適宜変更してください。

% mysqladmin ping -u root
mysqld is alive
% mysql -u root
mysql> create database as_rails_development default character set utf8;
mysql> create database as_rails_test default character set utf8;
mysql> create database as_rails_production default character set utf8;
mysql> grant all privileges on as_rails_development.* to root@localhost identified by "";
mysql> grant all privileges on as_rails_test.* to root@localhost identified by "";
mysql> grant all privileges on as_rails_production.* to root@localhost identified by "";
mysql > \q
モデルの作成

以下のコマンドを実行すると、モデル、モデルのテストファイル、migration定義ファイルが作成されます。

% ruby script/generate model Order item:string created_at:datetime
% ruby script/generate model Payment order_id:integer created_at:datetime
テーブルの作成

migration定義ファイルを実行します。

% rake db:migrate

テーブルができたかどうかを確認してみましょう。今はdevelopment環境のみで十分です。

% mysql -u root
mysql> use as_rails_development
Database changed 
mysql> show tables;
+--------------------------------+
| Tables_in_as_rails_development |
+--------------------------------+
| orders                         |
| payments                       |
| schema_info                    |
+--------------------------------+
3 rows in set (0.00 sec)
モデルの関連

注文モデルと決済モデルに関連を定義します。

as_rails/app/models/order.rb
class Order < ActiveRecord::Base
  has_one :payment
end
as_rails/app/models/payment.rb
class Payment < ActiveRecord::Base
  belongs_to :order
end

コントローラとビューの作成

のちの拡張を見越してコントローラ名はAsyncShopとしておきます。

% ruby script/generate controller AsyncShop index order_form order destroy_order payment

本連載はRailsアプリケーションの解説がメインの目的ではありませんので、アクションやビューの実装はscaffoldコマンドで作成されるものを適宜流用してしまいます。

as_rails/app/controllers/async_shop_controller.rb

同期アプリケーションなので、 order アクション内にて注文テーブルと決済テーブルの両方にレコードを生成したのち、ユーザーに応答index アクションの結果)が返ります。 他社システムと連携している決済処理が「重い」という想定なので、該当する payment メソッドのなかで5秒間の sleep をいれています。

class AsyncShopController < ApplicationController

  def index
    @order_pages, @orders = paginate :orders, :per_page => 10
  end

  def order_form
    @order = Order.new
  end

  def order
    begin
      Order.transaction do
        @order = Order.new(params[:order])
        @order.save!
        payment(@order[:id])

        flash[:notice] = 'Order was successfully created.'
        redirect_to :action => 'index'
      end
    rescue Exception
      flash[:notice] = 'Order was failed.'
      render :action => 'order_form'
    end
  end

  def destroy_order
    Order.find(params[:id]).destroy
    redirect_to :action => 'index'
  end

  private
  def payment(order_id)
    sleep 5

    payment = Payment.new
    payment.order_id = order_id
    payment.save!
  end

end
as_rails/app/views/async_shop/index.rhtml

scaffoldで生成したものとの違いは、show/editのリンクがなくなっていることと、注文テーブルと決済テーブルのそれぞれのレコードの作成日時が表示されることです。上記のとおり payment アクションのなかで5秒間の sleep があるので、その差を反映した日時が表示されることになります。

<h1>Listing orders</h1>

<table>
  <tr>
    <th>Item</th>
    <th>Ordered at</th>
    <th>Payed at</th>
  </tr>

<% for order in @orders %>
  <tr>
    <td><%=h order.item %></td>
    <td><%=h order.created_at %></td>
    <td><%=h begin order.payment.created_at rescue "not yet." end %></td>
    <td><%= link_to 'Destroy', { :action => 'destroy_order', :id => order }, 
                                 :confirm => 'Are you sure?', :method => :post %></td>
  </tr>
<% end %>
</table>

<%= link_to 'Previous page', { :page => @order_pages.current.previous } if @order_pages.current.previous %>
<%= link_to 'Next page', { :page => @order_pages.current.next } if @order_pages.current.next %> 

<br />

<%= link_to 'New order', :action => 'order_form' %>

あとは、注文内容の入力画面です。

as_rails/app/views/async_shop/order_form.rhtml
<h1>New order</h1>

<% form_tag :action => 'order' do %>
  <%= error_messages_for 'order' %> 
  <p><label for="order_item">Item</label><br/>
  <%= text_field 'order', 'item'  %></p>
  <%= submit_tag "Order" %>
<% end %>

<%= link_to 'ordered list', :action => 'index' %>
as_rails/app/views/layouts/async_shop.rhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>AsyncShop: <%= controller.action_name %></title>
</head>
<body>

<p style="color: green"><%= flash[:notice] %></p> 

<%= yield  %>

</body>
</html>

同期アプリケーションの一連のコードはこちらから取得できます。

% svn checkout http://ap4r.rubyforge.org/svn/tags/200709_gihyo_sync_shop

実行

動作を確認してみましょう。

% ruby script/server

次のURLにアクセスします。

「New order」を選択し、適当なアイテム名を入力したのち、⁠Order」ボタンで注文を確定します。決済処理に時間がかかっているので、5秒ほど待つと注文結果のリストが表示されます。注文処理と決済処理は続けて一気に処理されていますので、レコードの生成時刻である「Ordered at」「Payed at」の値は、おおむね5秒の差となっていることでしょう。

図3 同期アプリケーションの実行結果
図3 同期アプリケーションの実行結果

ベンチマークをとってみる

irbにて以下のコードを実行してみましょう。アイテム名「hoge」で、⁠Order」ボタンが5回押された状態です。いずれも「重い」決済処理に影響されて実行時間は5秒以上かかっています。

(注)
このアプリケーションでは簡単のために、HTTP GETで注文処理を行いましたが、一般にはPOSTで処理すべきです。
% irb
>> require 'benchmark'
>> require 'open-uri'
>> Benchmark.bm do |b|
?>   5.times {b.report {open "http://localhost:3000/async_shop/order?order[item]=hoge"}}
>> end
    user     system      total        real
0.010000   0.010000   0.020000 (  5.348240)
0.010000   0.000000   0.010000 (  5.760167)
0.000000   0.010000   0.010000 (  5.509365)
0.010000   0.000000   0.010000 (  5.839032)
0.010000   0.010000   0.020000 (  5.377185)
=> true
>>

以上で、同期アプリケーションの作成が終わりました。次にこのアプリケーションをAP4Rを利用して非同期化します。

非同期化の準備

AP4RのRailsプラグインをインストール

下記のコマンドでインストールできます。tags以下のap4r-0.3.2は執筆段階でのAP4Rのバージョンを表しています。バージョンが異なる場合には、適宜読み変えてください。

% cd as_rails
% ruby script/plugin install http://ap4r.rubyforge.org/svn/tags/ap4r-0.3.2/samples/HelloWorld/vendor/plugins/ap4r

また、インターネット環境に接続できない場合は、RubyGemsでインストールされたAP4Rパッケージのgems/ap4r-0.3.2/rails_plugin/以下をコピーしてご利用ください。

AP4R の作業ディレクトリを作成

冒頭で説明したとおり、AP4Rのルートディレクトリとしてas_ap4rを作成します。次のコマンドを実行するだけです。

% cd async_shop
% ap4r_setup as_ap4r

このコマンドにより以下のディレクトリ、およびファイルが生成されます。

as_ap4r
 +-- config
 +-- log
 +-- public
 +-- script
 +-- tmp

メッセージ格納用のテーブルを作成

MySQLにユーザーとデータベースを作成します。

% mysql -u root
mysql> create database ap4r default character set utf8;
mysql> grant all privileges on ap4r.* to ap4r@localhost identified by "ap4r";
mysql> use ap4r

次の2つのSQLを実行してください。メッセージ格納用のテーブルです。ファイルは、gems/reliable-msg-1.1.0/lib/reliable-msg/mysql.sqlにあります。

CREATE TABLE `reliable_msg_queues` (
  `id` varchar(255) NOT NULL default '',
  `queue` varchar(255) NOT NULL default '',
  `headers` text NOT NULL,
  `object` blob NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=binary;

CREATE TABLE `reliable_msg_topics` (                                                              
  `topic` varchar(255) NOT NULL default '',                                                       
  `headers` text NOT NULL,                                                                        
  `object` blob NOT NULL,                                                                         
  PRIMARY KEY  (`topic`)                                                                          
) ENGINE=InnoDB DEFAULT CHARSET=binary;

AP4Rの起動確認

以下のコマンドで、AP4Rを起動します。

% cd as_ap4r
% ruby script/mongrel_ap4r start -A config/queues_mysql.cfg

終了するには、コンソールで Ctrl+C としてください。Unix系のOSでは、 -d オプションをつけるとデーモンモードで起動することもできます。

irbで疎通確認しましょう。

% irb
>> require 'rubygems'
>> require 'reliable-msg'
>> q = ReliableMsg::Queue.new "test"
=> #<ReliableMsg::Queue:0x26fdea0 @queue="test">
>> q.put "hoge"
=> "37a13880-3736-012a-cf51-001IRI6cb9ad524"

irbやAP4Rの起動コンソールになにもエラーが出力されなければ、疎通できているはずです。

UUIDの生成に失敗した場合は、ディレクトリの権限モードを変更, または, UUIDの設定ファイルを手動で生成する必要があります。また、コネクションの生成に失敗した場合は、ネットワーク環境の設定に問題がある可能性があります。詳細はFAQをご覧ください。

FAQhttp://ap4r.rubyforge.org/wiki/wiki.pl?FAQ

次のページからは、いよいよこの同期アプリケーションを非同期に拡張していきます。

非同期アプリケーションへの拡張

非同期化の方針のおさらい

決済処理を注文処理から分離します。注文処理と決済処理の疎結合化です。時間のかかっていた決済処理を実行することなく応答を返すことができるようになるので、お客さんのイライラ解消につながるでしょう。また、他社のシステムと連携した決済処理は、これまで他社のシステムメンテナンスや障害に影響されていましたが、今後はそのような場合でも注文を受け付けることが可能になります。

非同期化のAPI

非同期メッセージ送信のAPIは、Railsプラグインで提供されるヘルパーメソッドを使います。

ap4r.async_to({...},   # 次の処理を指定するオプション
              {...})   # リクエストするデータ

ap4r が、AP4Rのプラグインで提供されるメソッドです。 async_to メソッドには、引数として、非同期処理として実行されるアクションの指定、及びパラメータを渡します。アクションの指定は、Railsの url_for と同様に指定するか、 :url キーにURLを指定することが出来ます。URLを直接指定することで、Railsではないサービスにも非同期処理を行わせることができます。

いざ、コード拡張

今回の同期アプリケーションの場合、変更箇所はasync_shop_controller.rbの order アクションと payment アクションになります。以下に修正前のものと修正後のものを並べます。

修正前
class AsyncShopController < ApplicationController
  (省略)

  def order
    begin
      Order.transaction do
        @order = Order.new(params[:order])
        @order.save!
        payment(@order[:id])                     # ...(1)

        flash[:notice] = 'Order was successfully created.'
        redirect_to :action => 'index'
      end
    rescue Exception
      flash[:notice] = 'Order was failed.'
      render :action => 'order_form'
    end
  end

  private
  def payment(order_id)
    sleep 5

    payment = Payment.new
    payment.order_id = order_id                  # ...(2)
    payment.save!
  end
  
  (省略)
end
修正後
class AsyncShopController < ApplicationController
  (省略)

  def order
    begin
      Order.transaction do
        @order = Order.new(params[:order])
        @order.save

        ap4r.async_to({:action => 'payment'},
                      {:order_id => @order.id})  # ...(3)

        flash[:notice] = 'Order was successfully created.'
        redirect_to :action => 'index'
      end
    rescue Exception
      flash[:notice] = 'Order was failed.'
      render :action => 'order_form'
    end
  end

  def payment
    sleep 5 

    payment = Payment.new
    payment.order_id = params[:order_id]         # ...(4)
    payment.save
    render :text => "true"                       # ...(5)
  end
  
  (省略)
end

いかがでしょう?

修正前後であまり大きな違いはないように見えるのではないでしょうか。変更箇所を順に見ていきます。

修正前に payment アクション(メソッド)を呼び出していた部分(1)が非同期化にともなってなくなり、代わりに ap4r.async_to メソッド(3)となります。次の処理として同じコントローラ内の payment アクションを指定し、 @order.id をリクエストデータとして渡しています。非同期メッセージ送信する側の変更は以上で終了です。

次に非同期として処理される側、 payment アクション(privateメソッドからアクションに変わっています)をみてみましょう。修正前は @order.id を引き数として受け取り、 payment にセット(2)していました。修正後にはこの引き数がなくなっています。リクエストデータとして渡されたものは、画面から渡るデータと同様に params でアクセス可能なので、⁠4)のようにして payment にセットしています。キーは、⁠3)で指定していたものとなります。

最後に、非同期処理が問題なく終了したことをAP4Rサービスに伝えるために(5)の記述が必要になります。

非同期アプリケーションの一連のコードはこちらから取得できます。

% svn checkout http://ap4r.rubyforge.org/svn/tags/200709_gihyo_async_shop

実行

Railsプロセスを起動します。

% cd as_rails
% ruby script/server

AP4Rプロセスを起動します。

% cd as_ap4r
% ruby script/mongrel_ap4r start -A config/queues_mysql.cfg

画面から注文処理を実行します。

「Order」ボタンを押すとすぐに注文結果のリストが表示されるでしょう。決済日時(Payed_at)は"not yet."となっています。非同期アプリケーションに拡張した結果、注文情報の保存処理が実行されるとすぐにユーザーに応答が返るようになりました。⁠重い」決済処理はユーザーの知らぬ間に実行されているはずです。画面をリロードすると、決済日時の表示が変わり、決済処理が無事に終了したことを確認できると思います。

図4 非同期アプリケーションの実行結果
図4 非同期アプリケーションの実行結果

ベンチマークをとってみる

さきほどと同様にirbから実行します。

% irb
>> require 'benchmark'
>> require 'open-uri'
>> Benchmark.bm do |b|
?>   5.times {b.report {open "http://localhost:3000/async_shop/order?order[item]=hoge"}}
>> end
    user     system      total        real
0.010000   0.000000   0.010000 (  0.602230)
0.010000   0.010000   0.020000 (  5.549839)
0.000000   0.000000   0.000000 (  5.395502)
0.010000   0.010000   0.020000 (  5.592141)
0.010000   0.010000   0.020000 (  5.566676)
=> true

タイミングや各自の環境によってベンチマークの結果は変わります。筆者の環境では、はじめのリクエストの返りはたしかに早くなりましたが、それ以外は以前と同じくらいでした。これは、Railsプロセス1台で処理を行なっているため、⁠重い」決済処理が実行されている間、ほかの処理が堰き止められてしまっているからです。つまり、 index アクション後のリダイレクトより先に payment アクションのリクエストが実行されているので、こうした結果になっています。

リダイレクトの時間を含めずに計測すると、サーバ側での処理時間が確実に短縮されていることがよりはっきりします。

% irb
>> require 'benchmark'
>> require 'net/http'
>> Net::HTTP.version_1_2
>> Benchmark.bm do |b|
?>   5.times {b.report {
?>     Net::HTTP.start('localhost', 3000) {|http|
?>       response = http.get('/async_shop/order?order[item]=hoge')
?>     }
?>   } }
>> end
    user     system      total        real
0.000000   0.000000   0.000000 (  0.081017)
0.000000   0.010000   0.010000 (  0.385568)
0.000000   0.000000   0.000000 (  5.103064)
0.010000   0.000000   0.010000 (  0.372144)
0.000000   0.010000   0.010000 (  5.638147)
=> true

payment アクションの実行待ちでやはり時間のかかったものもいくつかありますが、下図のようなリバースプロキシを使った構成では、通常、リクエストが適切に振り分けられることでユーザーは遅延を感じることなく処理を実行できるでしょう。バックエンドにフットプリントの小さいRailsプロセスをたくさん立ちあげておくことで、ユーザーからのリクエストとAP4Rからの非同期メッセージ処理のリクエストをともに分散させ、負荷の平準化を図るのが一般的な構成です。

図5 リバースプロキシを利用した複数プロセス構成
図5 リバースプロキシを利用した複数プロセス構成

非同期処理用のRailsプロセスを分けてみる

また、リバースプロキシを使わずに、非同期メッセージ処理のリクエストを明示的に別のRailsプロセスにリクエストすることもできます。非同期処理用のプロセスへの負荷が少ない場合はもったいない構成ですが、別な機能も紹介したいので、最後にこの方法を試してみましょう。

決済処理を別のRailsプロセスで実行するよう拡張します。構成は下図のようになります。AP4Rでは設定によりこうした構成も簡単に組むことができます。

図6 非同期処理用のRailsプロセスを別にたてる構成
図6 非同期処理用のプロセスを別にたてる構成

設定ファイル

AP4Rの設定ファイルを確認してみましょう。

as_ap4r/config/queues_mysql.cfg
--- 
store: 
  type: mysql
  host: localhost
  database: ap4r
  username: ap4r
  password: ap4r
drb: 
  host: 
  port: 6438
  acl: allow 127.0.0.1 allow ::1
dispatchers:
  -
    targets: queue.*
    threads: 1

store は、メッセージの永続化先の情報です。

drb は、メッセージの受け口となるdruby関連の情報です。 acl ⁠=access control list)は、 allow で接続可能なアドレスを, deny で接続不可のアドレスを指定します。

dispatchers は、非同期メッセージ処理のリクエストを行なうスレッドの設定です。 targets で指定されたチャネルにはいったメッセージを、 threads で指定されたスレッド数で処理します。リクエスト処理が完了するまでスレッドは占有されます。

さて、非同期メッセージ処理のリクエストが、 dispatchers によって行われるのはすでに見てきたとおりですが、リクエスト先のURLはどう決まっているのでしょうか?  ap4r.async_to の第1引き数で次の処理を指定していますが、ここで指定された値をActionControllerの url_for の引数に与えた結果がURLとなります:controller の指定がない場合は、自身のコントローラ名を利用します⁠⁠。今回の例では、下記のURLが非同期メッセージ処理のリクエスト先となります。

http://localhost:3000/{:controller で指定された値}/{:action で指定された値}

localhost:3000が変わらないので、同一のRailsプロセスに非同期メッセージ処理のリクエストは飛ぶことになります。この部分を書き替えるのが次に紹介するURL変換フィルタの機能です。

URL変換フィルタ

その名のとおり、非同期メッセージ処理のリクエスト先URLを書き替えるためのフィルタです。次のように、 dispatchers の設定に追加します。Procオブジェクトの文字列を指定することで、ユーザーが自在に書き替えルールを設定できます。以下の設定では、ホスト名はそのままで、ポート番号のみを4001から4003の範囲でランダムに変化させています。これにより、同期処理用のRailsプロセスと非同期処理用のRailsプロセスが分離されたことになります。また、非同期処理用のプロセスでは「重い」処理が実行されるので複数のRailsプロセスに処理を分散しています。

さらに、 threads の設定値も1から3に変わっています。非同期メッセージング処理のリクエストが完了するまでスレッドが占有されるので、非同期処理用のRailsプロセス分だけスレッドも用意したわけです。

dispatchers:
  -
    targets: queue.*
    threads: 3
    modify_rules:
      url: "proc {|url| url.port = 4001 + rand(3)}"

上の設定をqueues_mysql.cfgに反映し、AP4Rを起動します。

% cd as_ap4r
% ruby script/mongrel_ap4r start -A config/queues_mysql.cfg

複数のRailsプロセスを起動

では、この設定のもとでこれまで作成してきた非同期アプリケーションを動かしてみましょう。複数のRailsプロセスを扱うにはmongrel_clusterが便利です。ただし、Linux環境下である必要があるため、Windows環境で動かしている方は、コマンド プロンプト4つに、それぞれにRailsプロセスをポート指定-p オプション)で起動してください。

% gem install mongrel_cluster
% cd as_rails
% mongrel_rails cluster::configure -e development -p 4000 -N 4
% mongrel_rails cluster::start
starting port 4000
starting port 4001
starting port 4002
starting port 4003

同期処理である注文情報の保存処理には4000番ポートのRailsプロセスが利用され、非同期処理である決済処理には4001から4003番ポートのプロセスが利用されます。

次のURLで画面をひらき、動きを確認してみてください。

ベンチマークをとってみる

さきほどと同様にirbから実行します。ポート番号が3000から4000に変わっている部分だけご注意ください。

% irb
>> require 'benchmark'
>> require 'open-uri'
>> Benchmark.bm do |b|
?>   5.times {b.report {open "http://localhost:4000/async_shop/order?order[item]=hoge"}}
>> end
    user     system      total        real
0.010000   0.000000   0.010000 (  0.567744)
0.010000   0.000000   0.010000 (  0.714265)
0.000000   0.010000   0.010000 (  0.407419)
0.010000   0.000000   0.010000 (  0.715437)
0.000000   0.000000   0.000000 (  0.708147)
=> true

4000番ポートの同期処理用のRailsプロセスでは、軽い、注文情報の保存処理のみが実行されるので、ユーザーへの応答はいずれもすぐに返ってきているのがわかるでしょう。また、後続の非同期処理も3台のRailsプロセスで着々と実行されます。ベンチマークを実行したのち、画面を何度かリロードしているとその様を確認できます。

URL変換フィルタの機能を利用した非同期アプリケーションの一連のコードはこちらから取得できます。

% svn checkout http://ap4r.rubyforge.org/svn/tags/200709_gihyo_async_shop_with_url_rewrite

今回は、同期アプリケーションに対するAP4Rを利用した非同期拡張をみてきました。 async_to メソッドによるコード拡張のポイントと、非同期化によってユーザーのイライラが解消される様子が伝わったでしょうか。リバースプロキシを利用してバックエンドに複数のRailsプロセスが存在する構成では、⁠重い」非同期処理とユーザーにすぐ応答したい処理をうまく両立させながら実行できることになります。また、非同期メッセージ処理のリクエスト先を自在に変更可能なURL変換フィルタ機能を利用した構成も紹介しました。

次回は、AP4Rの「堅牢」さを支えるSAF機能の利用やテストサポートについて触れてみたいと思います。

おすすめ記事

記事・ニュース一覧