Rails から AP4R サーバへの接続の可用性とか、負荷分散とか...

AP4R について書くのはずいぶん久しぶりな気がしますが... (^^;


RailsAP4R を使っている場合、以下の流れでメッセージを介して非同期が実現されています。
# ユーザーからのリクエストで、1,2 が同期的に実行され、3 以降が非同期で実行されます。

0. AP4RRails プラグインの導入
1. アクションのなかで、ap4r.async_to(message, ...) すると、
2. Rails から AP4R サーバ上のキューにメッセージを PUT (dRuby プロトコル)
3. AP4R から指定された宛先に送信 (HTTP など)
4. 宛先でメッセージを処理 (宛先が Rails であれば、通常のリクエストと同様に処理可能)


可用性の高いシステムを考えるとき、この流れのなかで 1->2 の部分が SPoF (Single Point of Failure) になってしまっていました。


リバースプロキシ構成であれば、バックエンドの Rails インスタンスのどれかがこけても、フロントエンドでうまいこと振りわけることで、縮退しつつもシステムは動きつづけます。しかしながら、メッセージを PUT する先の AP4R サーバが落ちていると、アクションはエラーとなってしまいます。

# 確実に非同期処理が実行される必要があれば、業務データベースへのコミットと AP4R へのメッセージの PUT はアトミックにしないと駄目ですよね?


個々の Rails インスタンスごとに AP4R を個別に用意すれば、すべての処理がとまることはないでしょうが、やたらとサーバが増えてしまいそうで、管理を考えるとそれも悩ましいです。



というわけで、Rails から AP4R サーバへの接続に失敗したときに、別の AP4R サーバにフェイルオーバーするようにしてみました。(まだ、リリースはしてませんが...。)
最低 2つの AP4R サーバを起動して設定をしておけば、一方への接続に失敗したとき、他方に接続を切り替えます。両方とも落ちてるときはどうしようもないですが、そうでなければ生きている AP4R サーバにメッセージを PUT できます。



environment.rb のなかで、以下のように設定します。

uris = %w(6438 6439 6440).map {|port| "druby://localhost:#{port}"}
::Ap4r::AsyncHelper::Base.druby_uris(uris, :fail_over => true)


最初は、druby://localhost:6438 に接続しにいきますが、そことの接続ができなければ、druby://localhost:6439 との接続を試みます。そして、そこも駄目だと druby://localhost:6440 に接続しにいきます。カスケード フェイルオーバーっていうみたいですね。

# environment.rb のなかで、druby URI の配列の並び順を Rails インスタンスごとに変えるようにしておけば (ポートでわけたり、ランダムにしたり?)、Active-Standby のたすき掛け構成みたいになりますね。



ちなみに、オプションはもう 2つ、用意しています。

uris = %w(6438 6439 6440).map {|port| "druby://localhost:#{port}"}
::Ap4r::AsyncHelper::Base.druby_uris(uris, :rotate => true)


この設定では、接続先の AP4R が落ちてなくても、メッセージを PUT するごとに、接続先の AP4R サーバを切り替えていきます。ロードバランサのラウンドロビン的な動きです。:fail_over オプションを設定してなければ、フェイルオーバーはしません。逆に、:fail_over オプションと組み合わせれば、正常時でも接続先の AP4R サーバを切り替え、異常時でもフェイルオーバーして AP4R サーバを切り替えることになります。


接続できなかった AP4R サーバは、接続先 URI のリストから削られます。なので、いったん接続できなかった AP4R サーバは、Rails インスタンスを再起動するまで利用されません。これが嫌な場合もありそうなので...


uris = %w(6438 6439 6440).map {|port| "druby://localhost:#{port}"}
::Ap4r::AsyncHelper::Base.druby_uris(uris, :fail_over => true, :fail_reuse => true)


としておくと、いったん接続できなかった AP4R サーバにも、(ローテーションやフェイルオーバーを繰り返していくなかで) 接続しにいきます。ほんとは keep alive 的な仕組みも入れて、生き返ってたら接続先の URI リストに戻すほうがよいのかもしれませんが、今回はこんな風にしてみました。

# 落ちたままのサーバに接続を何度も繰り返すのはまずいので、URI リストすべてに接続を試みて駄目であればエラーとなります。



いろいろ考えてはみたんですが、このあたりの可用性や負荷分散の動きってのは、ケースバイケースなんだろうなぁと思い至りました。その個別の動きを DSL ライクに設定 (実装) できるようにしようかとも思ったのですが、なんだか余計に複雑になってしまいそうだったので、シンプルな動きのみを提供するだけにとどめてみました。