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

第1回軽量さと堅牢さを兼ね備えたメッセージング

はじめに

みなさん、はじめまして。

今回からRubyによるオープンソースのメッセージングライブラリ、AP4Rの連載をさせていただくことになった加藤です。一緒にAP4Rの開発を進めている篠原とともに 4回にわたってご紹介させていただきます。

筆者らはフューチャーアーキテクト株式会社にて、自社製のJavaによるメッセージングミドルウェアの開発、メンテナンスを行なってきました。大小さまざまなプロジェクトで稼動してきたものであり、数十台規模での導入実績もあります。そこで培った実装や経験をもとにRubyで書きあげたものが、AP4Rです。Ruby 会議 2007でも取りあげてもらえたので、名前くらいは聞いたことあるよ、という方もいるかもしれません。以下、RubyForgeのプロジェクトサイトと日本語ホームページのURLです。

本連載では、AP4Rの開発にいたった背景にはじまり、AP4Rを利用したアプリケーション作成を例にとりあげながら、実装、テスト、運用上のポイントに触れていきたいと思います。

AP4Rとは

AP4Rとは、Asynchronous Processing for Rubyの略です。その名のとおり、Rubyで非同期処理を実現するためのライブラリです。

2006年初頭より篠原、加藤にて開発を進め、同年 9月にオープンソースとして公開しました(このときのバージョンは 0.1.0⁠⁠。その後いく度かの機能追加を経て、執筆時点ではバージョン0.3.2となっています。

非同期処理というと文脈によっていろいろな意味にとれますが、ここではアプリケーションを疎結合化することにあたります。非同期処理自体がピンとこない場合は、電話とメールの関係をイメージしてみてください。すぐにでも相手の答えが必要な場合には電話、それほど急がない場合にはメール、といったようにみなさんも使いわけていると思います。電話の場合はそのときに相手の返事がもらえますが、相手がとても忙しいときにかけてしまうかもしれません。一方、メールであれば、受け手はある程度自由なタイミング(余裕のあるときなど)に依頼内容をこなすことができます。また、送り手も受け手側での処理が終わるまで待たずに、次の作業に移ることができます。いずれかが優れているわけではなく、状況に応じて使いわけることで、より柔軟かつ効率的に対応できると思います。

システム上でも非同期処理をうまく使うことで効率的な処理が可能になります。ログ出力やメール送信、重いサマリデータの作成やシステム間連携など、ユーザーに直接的に関係のない処理を切り離すことで、ユーザーへの素早い応答が実現できるでしょう。また、連携しているシステムの障害に影響されてしまい、問題の起きてないシステムでも処理が正常に終了できないといったこともなくなります。

図1 同期処理のみのシステム
図1 同期処理のみのシステム
図2 非同期化したシステム
図2 非同期化したシステム

AP4Rでは、メッセージングという技術で非同期処理を実現しています。

メッセージングを介して複数のサーバに処理を分散することで、負荷増大時のスケールアウトにも比較的容易に対応できるでしょう。

図3 複数サーバに処理を分散
図3 複数サーバに処理を分散

キーワードは「堅牢」かつ「軽量」

前節では、メッセージングを利用してアプリケーションを非同期化することのメリットをみてきました。単に処理を切り離すだけなら簡単にできそうに見えたでしょうか? ひょっとしたらそういったものをすでに利用あるいは実装している方もいるかもしれません。

実装する上で注意が必要なのは、メッセージが受け手に確実に配送されることです。途中でメッセージが消えてしまったり、重複してしまったら大変なことになります。

たとえば、みなさんがあるオンラインのショッピングサイトで商品を購入したとしましょう(この例はこのあともよく登場します⁠⁠。決済処理が重たいので、注文の受付処理から非同期化されていたとします。すなわち、注文の受付処理と決済処理がメッセージングを介して切り離されている状態です。このとき、メッセージの配送がしっかり保証されていないと、お金を支払ったにもかかわらず商品が届かなかったり、あるいは二重にお金が引き落とされているといった事態が起きてしまいます。後者に関しては、アプリケーションで対応できますが(むしろ本来そうするべきだとも思いますが⁠⁠、前者に関してはメッセージングライブラリで確実に保証する必要があります。

システムが常に正常に動いていれば上記のようなことはなかなか起きないかもしれません。しかし、世の中そうはいかないものです。

データベースやネットワークの障害、サーバの突然のダウンは、絶対にないとは言いきれません。あるいは、連携しているシステムの定期メンテナンスを見落とすといった人為的なミスもあるかもしれません。

たとえなにがあろうとも、メッセージを死守する。それが信頼性のあるメッセージングに求められる必須要件となります。

また、AP4RはRubyで書かれています。Rubyのもつ簡潔さ、柔軟さをうまくいかし、設定やAPIができるだけ使いやすいものになることを狙っています。実装/テスト/運用をオールインワンでサポートした、導入しやすいライブラリとなるよう開発を進めています。

信頼できるメッセージングとして「堅牢」でありながら、Rubyらしさをいかした「軽量」なライブラリ。それがAP4Rの目指す姿です。

非同期処理の活躍の場

さて、非同期処理の用途としてすでにいくつか例をあげてきましたが、ユーザーを待たせている/待たせる可能性のある処理は非同期化によって改善できる可能性があります。チューニングのひとつの方法としても検討できるでしょう。

tDiaryの作者としてもお馴染のただただしさんがRubyKaigi2007での発表直後に書かれていたエントリRuby会議2007 2日目 - 非同期重要 - ただのにっき (2007-06-10)も興味深いです。

ここでは、エンタープライズの分野における非同期の重要性が説かれ、それとともに ウェブの分野でも、非同期処理への潜在的なニーズがあることがうかがえます。ユーザビリティと効率性の向上はいずれの分野でも大きな課題ともいえます。導入と運用が容易になれば、非同期処理の利用はまだまだ広がる可能性が感じられます。

AP4R を利用したシステム構成例

具体的なアプリケーション作成に入る前に、システム全体におけるAP4Rの位置付けをつかんでみましょう。

Ruby on Rails上で動作しているウェブアプリケーションに対し、AP4Rを追加したシステムの構成例を説明します。その後、派生的な構成と、APIと設定にかるく触れます。

典型的な構成

リバースプロキシとしてのウェブサーバ、及びRailsが動作するプロセス群に加えて、AP4Rプロセスが必要です。まず、AP4Rプロセスが1つだけ加わった構成を下図に示します。

図4 リバースプロキシ+Railsプロセス群のウェブアプリの構成に、AP4Rを1つ加えた構成
図4 リバースプロキシ+Railsプロセス群のウェブアプリの構成に、AP4Rを1つ加えた構成

動作の流れ

上図を見ながら、非同期が含まれた処理の流れをみてみましょう。

  1. クライアントが、Apache、Lighttpdなどのウェブサーバへリクエストを送信します。
  2. mongrel、FastCGI等のバックエンドプロセス上のRailsが同期処理を実行します。
  3. 同期処理の最後でAP4Rに非同期処理のメッセージを送信します。
  4. 同期処理終了後、すぐにクライアントにレスポンスを返します。
  5. AP4Rに入ったメッセージは、
    1. 一旦キューに入った後、
    2. ウェブサーバにリクエストとして送信されます。
  6. Rails 上で非同期処理が実行されます。アクション自体は通常の同期処理と同様です。

以上の全ての通信は、TCP/IP上で行われます。そのため、それぞれのプロセスは、一つの筐体に乗せることも、複数の筐体に分割して配置することも可能です。より上位のプロコトルは、⁠AP4Rへのメッセージ送信」⁠上の流れの3)drubyプロトコルを、⁠非同期メッセージ処理のリクエスト送信」⁠上の流れの5)にはHTTPを用います。

「非同期メッセージ処理のリクエスト送信」については、HTTP以外に、XML-RPC、またはSOAPを用いて送信を行うことが可能です。また、⁠AP4Rへのメッセージ送信」については、今後、drubyに加えて、STOMPHTTPによる通信を追加する予定です。

派生的な構成をいくつか

AP4R を複数のプロセスにする

AP4Rを複数プロセスで動かすことにより、メッセージング処理の負荷分散が可能です。

図5 複数のAP4Rプロセスで、メッセージング処理を分散 図5 複数のAP4Rプロセスで、メッセージング処理を分散

非同期を分割

非同期処理用のプロセスを、リバースプロキシも含めて、分割することが可能です。

図6 非同期に関わるプロセス(リバースプロキシ+Rails)を分割
図6 非同期に関わるプロセス(リバースプロキシ+Rails)を分割

非同期を別のサービスで処理

非同期メッセージ処理のリクエストは、HTTPなど、標準的なプロトコルで送信されます。従って、実際に処理を行うプロセスは、Railsに限らず、HTTPで受け付けているサービスであれば何でも構いません。

例として、Tomcatの上で動作するJava サーブレットを非同期処理に使う場合、下の図のようになります。

図7 非同期の処理が Java のプロセス(Tomcat)で行われる
図7 非同期の処理が Java のプロセス(Tomcat)で行われる

API、設定

「動作の流れ」の中で、AP4Rに関わる部分として、以下の3種類があります。

  • 3の非同期メッセージ送信
  • 5の非同期メッセージ処理のリクエスト
  • 6の非同期メッセージ処理

これらを順にAPI、設定と絡めて説明します。

非同期メッセージ送信

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

最も基本的な形は以下のコードのようになります。

class ShopController < ApplicationController
  def order
    ...
    ap4r.async_to({:action => 'payment'},
                  {:id => 1})
    ...
  end
  ...

ap4rが、AP4Rのプラグインで提供されるメソッドです。 async_to メソッドには、引数として、非同期処理として実行されるアクションの指定、及びパラメータを渡します。

アクションの指定は、Railsの url_for と同様に指定するか、 :url キーにURLを指定することが出来ます。

URLを直接指定することで、Railsではないサービスにも非同期処理を行わせることができます。

非同期メッセージ処理のリクエスト

リクエストは、AP4Rプロセスの内部で、キューを監視しているスレッドにより実行されます。

AP4Rの設定ファイル(YAML形式)から、抜粋したものが以下になります。

  dispatchers:
    -
      targets: queue.*
      threads: 1

この設定では、queue.で始まるチャネルを、1つのスレッドで監視して、非同期メッセージ処理のリクエストを投げるようになっています。

非同期メッセージ処理

Railsで非同期メッセージ処理を行う場合、アクションの実装には、特別な点はほとんどありません。

実際に、先程orderアクションにて送信された非同期メッセージを処理するアクションのコードは、このようになります。

class ShopController < ApplicationController
   def payment
     id = params[:id]
     ...
     render :text => "true"
   end

Railsでアプリケーションを書いたことがある方は、 async_to と、このアクションの関係が、 form_tag と、それを受けるアクションの関係に似ていることに気付かれるかもしれません。

以上で、AP4Rを利用したシステムの全体像をおおまかに見てきました。

より詳しいAPIの説明や、設定ファイルの場所や全体像などは、第2回以降、アプリケーションの作成を通じて説明する予定です。

SAF(Store and Forward)機能とは

非同期処理を利用する際には、常に「失敗」した場合を考慮しておく必要があります。

ここでの失敗とは、メッセージングを行うプロセスの内部で起きるものだけでなく、ネットワークの切断によるエラー、データベースプロセスが突然死んだり、応答しなくなるといった外部の要因も含みます。

失敗しても非同期処理を保証するために、AP4Rでは、SAFという機能を提供しています。

以下、メッセージングの信頼性の一般的な話をした後、SAFが何を保証するのかを説明します。

メッセージの生成、保存、そして処理を確実に行うには何が必要?

メッセージのライフサイクルを見てみると、生成、保存、処理というステップがあることが分かります。

この流れを見ながら、確実に処理が行われるために必要なことを考えてみましょう。

図8 典型的なシーケンス その1:生成から保存まで
図8 典型的なシーケンス その1:生成から保存まで
図9 典型的なシーケンス その2:処理
図9 典型的なシーケンス その2:処理

まず「生成」について考えてみましょう。

一般に何らかの業務処理(業務データベースの更新処理)と結び付いています。

例えば、注文受付処理のなかで、注文を業務データベースに保存し、決済処理のメッセージを生成する場合を考えてみましょう。この2つのデータは、どちらか一方のみが欠けてしまうと問題が発生します。つまりアトミックである必要があるのです。

業務DBとメッセージDBが異なっていると、2つのDBでのコミットタイミングの違いにより、図8の赤い矢印のタイミングでエラーが発生すると、アトミック性が壊れる可能性があります。

注)
この文書のなかでは、以下の単語を特殊な意味で使います。
業務処理
メッセージングとは直接関係しない処理。⁠例)オンライン店舗での「注文受付処理」
業務データベース、業務DB
業務処理にもちいるデータベース
メッセージデータベース、メッセージDB
メッセージングのサービスが、メッセージを保存するために使うデータベース

次に「保存」についてです。生成はアプリケーションを処理するプロセスのなかに閉じていますが、保存はメッセージングのプロセスとの通信を伴います。

メッセージングでの保存がデータベースを利用した場合、メッセージDBのトランザクションが正常に完了した後に通信が失敗する、という隙間(図8の青い矢印)でのエラーを考慮する必要が生じます。

最後のステップである「処理」について細かく見てみると、メッセージの取得、業務 DB の更新、処理の完了通知という流れになっています。

ここでも、業務DBのトランザクションの完了から、通信の完了の隙間(図9の青い矢印)が存在します[1]⁠。

アトミック性の保証、隙間で発生するエラーを考慮し、確実な処理を行うための仕組みを利用する必要があります。

ProducerとChannelの間の「最低でも1回(at-least-once)」

メッセージングに関わる2種類の言葉を紹介します。

まず、メッセージングで登場する3つの役割です。この説明は、かなり大まかなものですので、詳しくは、Enterprise Integration Patterns等を参照してください。

Producer
メッセージを生成して、Channelに送信します。図では、同期処理を行うRailsプロセスがこれにあたります。
Channel
メッセージの保存を行います。図では、AP4RサーバがChannelを管理しています。
Consumer
メッセージを、Channelから取得し処理します。図では、非同期リクエストを受ける Rails プロセスになります。

もうひとつは、⁠サービスの質(Quality of Service、QoS⁠⁠」のなかで、信頼性のあるメッセージング(Reliable Messaging)のレベルです。

信頼性のあるメッセージングに関するQoSには以下の3種類があります。

at-most-once
最大でも1回しか処理されないことを保証します。ただしメッセージが失われ、処理が行われない可能性はあります。
at-least-once
最低でも1回は処理が行われることを保証します。
once-and-only-once
上のふたつを合わせたもので、メッセージは必ず1回配信されることを保証します。

非同期メッセージングでの QoS については、KLab Architect Blogの記事「Requirments for Software Engineers」が参考になります。

単純なat-most-onceのみを保証することは、⁠隙間」でエラーが発生した場合にメッセージを廃棄すれば良く、比較的容易です。また、once-and-only-onceをメッセージングのみで保証することは、高い実行コストや、Producer/Consumerに対する制限につながることがあります。

異常系でも安心

AP4Rでは、メッセージング層のQoSとして、at-least-onceを採用することで、非同期メッセージの処理を、⁠最低でも1回」という意味の「信頼性」で保証します。

at-least-onceを保証するための仕組みとして、AP4Rでは「Guaranteed Deliveryパターン」Enterprise Integration Patterns, 122ページ)を実装し、SAF(store-and-forward)と呼んでいます。

SAFの動作は大きく2つの部分から成っています。

store
生成されたメッセージを業務DBにいったん保管します。その際に、業務処理でのデータ更新と同一のトランザクションで行います。
forward
そのトランザクションが完了した後、保管されたメッセージを、順次AP4Rサービスへ送信します。

storeにより、メッセージの生成と、業務処理のアトミック性が保証されることが分かります。また、forwardでは、保管されたメッセージの未送信、送信済の状態を管理することで、生成されたメッセージが確実に1回はAP4Rサービスへ送信されることが保証されます。

シーケンス

具体的な流れを図で見てみましょう。エラーが発生しない場合は、前ページの図8のような流れで処理が進みます。

ひとつめのエラーの場合として、Railsプロセスが、処理の途中で止まってしまった、という状況を考えてみましょう。

具体的には、業務DB更新完了から、メッセージ送信の間に止まったとします。SAFの有無に応じて処理の流れはそれぞれ以下のようになります。

図10 SAF無しの場合の異常系
図10 SAF無しの場合の異常系
図11 SAF有りの場合の異常系
図11 SAF有りの場合の異常系

エラーの発生により、AP4Rサービスへのメッセージがすぐには送信されません。しかし、SAF有りの場合には、⁠送信するべきメッセージの内容」が業務データベースに保存されていることが分かります。

よって、後からリカバリ処理を行うことで、メッセージを正しく AP4Rサービスへ送信することが出来ます。

もうひとつ、別のタイミングでRailsプロセスが止まってしまった場合の流れを見てみましょう。

今度は、メッセージ送信完了から、保管されたメッセージの削除の間で止まったとします。

図12 SAF有りの異常系、メッセージの重複が発生する場合
図12 SAF有りの異常系、メッセージの重複が発生する場合

この場合は、メッセージが業務データベースと、AP4Rサービスの両方に存在し、リカバリにより、メッセージの重複が発生します。

このように、SAF機能によって、at-least-onceを保証することが出来ます。

ただし、at-most-onceは保証されませんので、アプリケーションでの対処が必要です。注文処理と決済処理の例では、注文のIDから、決済処理の重複を検査、防止するという形で実装することが可能かもしれません。

より一般的に、SAFメッセージ生成時に、重複チェック用のIDをメッセージに付けておき、非同期処理を行う側で重複チェックテーブルを用いて検査を行うことも出来ます。

この方法は、業務ロジックの中に検査ロジックが入らない長所がある一方で、重複チェックテーブルの古いデータを、定期的に掃除する必要が出てきます。一般に、 at-least-once のメッセージングの上で at-most-once を保証すること(つまり全体で once-and-only-onceを実現する)は、アプリケーションの作りや運用と関わってきますので、慎重に設計を行う必要があります。

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

次回からは、実際にアプリケーション作成に入っていきます。

おすすめ記事

記事・ニュース一覧