SeamFramework.orgCommunity Documentation
この章は現在レビュー中ですのでご注意ください。
この章は (web) クラスタリングと EJB パッシベーションという二つの異なるトピックが Seam において偶然にも共通の解を共有するということを説明します。したがって、これらはこの参照マニュアルでは一緒に説明されることになります。パフォーマンスもこのカテゴリに分類される傾向がありますが、それは別々に扱われます。なぜなら、この章はプログラミングモデルとそれが上記の機能によってどのように影響を受けるかという点に集中しているからです。
この章では、Seam マネージャが Seam コンポーネントとエンティティインスタンスのパッシベーションをどのように管理するか、この機能を有効にする方法、そして、この機能がいかにクラスタリングに関連しているかを学びます。また、Seam アプリケーションをクラスタにデプロイし、HTTP セッションレプリケーションが適切に動作していることを検証する方法について学びます。クラスタリングに関する背景から始めて、Seam アプリケーションを JBoss AS クラスタにデプロイする方法の例を見ていきましょう。
クラスタリング (より正式には web クラスタリング) は、クライアントに対してアプリケーションの統一的な見方を提供する一方で、アプリケーションが複数の並列したサーバー (ノード) 上で動作できるようにします。負荷はサーバー間で分散します。一つまたは複数のサーバーに障害が発生しても、アプリケーションは生き残ったノードのどこかの上でアクセス可能です。このトポロジは、パフォーマンスと可用性がノードを追加するだけで改善できるので、スケーラブルなエンタプライズアプリケーションを開発する上で重要です。しかし、それは 「障害が発生したサーバー上の状態はどうなるのか?」 という重要な疑問を提起します。
Seam は最初からクラスタ内で動作するステートレスアプリケーションをサポートしています。ここまでで、Seam がスコープを追加したり、ステートフル(なスコープを持った)コンポーネントのライフサイクルを制御することによってステート管理を提供していることがお分かりになったでしょう。でも Seam のステート管理はインスタンスの生成、格納、破棄を越えた機能を提供しています。Seam は JavaBean コンポーネントへの変更を追跡し、要求の処理中のある戦略的な点での変更を格納します。これは、その変更が要求がクラスタのセカンダリノードへ移ったときに復元できるようにするためです。幸い、ステートフル EJB コンポーネントのモニタリングとレプリケーションは EJB サーバーによって処理されますので、Seamのこの機能はステートフルJavaBeansをEJBと同等にすることを意図しています。
ちょっと待ってください。それ以上の機能があるのです。Seam はクラスタ化したアプリケーションのためのとてもユニークな機能も提供するのです。 JavaBean コンポーネントのモニタリングに加え、Seam はレプリケーションの間に管理エンティティインスタンス (例えば、JPAやHibernateエンティティ) が分離状態にならないことを保証します。Seam はロード済のエンティティが自動的にセカンダリノードでロードされるように記録します。 しかし、この機能を使うにはSeam 管理永続コンテキストを使わなければなりません。この機能の詳細については、この章の後半に説明があります。
Seam がクラスタ環境に対して提供する機能を理解したところで、クラスタリングのためのプログラミングについて調べましょう。
クラスタ環境で使用されるセッションまたは対話のスコープのmutableなJavaBeanコンポーネントは、Seam APIの org.jboss.seam.core.Mutable
インタフェースを実装しなければなりません。契約 (contract) の一部としてコンポーネントはダーティフラグを管理しなければなりません。コンポーネントはその値をレポートしたり、clearDirty()
メソッドによってリセットします。Seam はこのメソッドをコンポーネントを複製するかどうかを決めるのに使います。これはオブジェクトの変更のたびにセッションに追加や削除をするために奇妙な Servlet API を使わなければならない事態を避けます。
また、すべてのセッションまたは対話のスコープの JavaBean コンポーネントはシリアライズ可能であることを保証する必要があります。それに、ステートフルコンポーネント (EJB または JavaBean) のすべてのフィールドはフィールドがtransientのマークがついているか、あるいは @PrePassivate
メソッドでnullがセットされてていない限りシリアライズ可能でなければなりません。@PostActivate
メソッドで transientや null にされたフィールドを元に戻すことも可能です。
人々がよくハマるのはリストを作るのに List.subList
を使うところです。その結果のリストはシリアライズ可能ではないのです。そのような状況では注意してください。 java.io.NotSerializableException
を叩いてみて、一目でculpritが置けないのであれば、その例外にブレークポイントを設定し、アプリケーションサーバーをデバッグモードで実行し、(Eclipseなどの) デバッガをアタッチしてデシリアライズのどこで滞っているかを調べましょう。
クラスタリングはホットデプロイ可能なコンポーネントとは一緒に使えないので注意してください。しかし、いつも説明しているように、そもそも開発環境ではないところでホットデプロイ可能なコンポーネントを使うべきではありません。
このチュートリアルに述べられている手続きは seam-gen アプリケーションと Seam booking サンプルで動作確認しました。
このチュートリアルでは、マスターおよびスレーブサーバーのIPアドレスはそれぞれ192.168.1.2と192.168.1.3と仮定します。ここでは mod_jk ロードバランサはあえて使っていません。両方のノードが要求に応えていることやセッションを共有していることを簡単に確認できるようにするためです。
私はこの手順において farm デプロイの方法を使っています。でも、アプリケーションを通常の方法でデプロイして、二つのサーバーが起動順に応じてマスター/スレーブ間でネゴシエートをするようにすることも可能です。
JBoss AS クラスタリングは jGroups によって提供される UDP マルチキャストに依存しています。RHEL / Fedora の出荷時のSELinux の設定はこれらのパケットをデフォルトでブロックします。これらを通すようにするには iptables のルールを (rootになって) 修正します。次のコマンドは IP アドレスが 192.168.1.x にマッチするように適用します。
/sbin/iptables -I RH-Firewall-1-INPUT 5 -p udp -d 224.0.0.0/4 -j ACCEPT /sbin/iptables -I RH-Firewall-1-INPUT 9 -p udp -s 192.168.1.0/24 -j ACCEPT /sbin/iptables -I RH-Firewall-1-INPUT 10 -p tcp -s 192.168.1.0/24 -j ACCEPT /etc/init.d/iptables save
詳細情報は JBoss Wiki の このページ にあります。
JBoss ASの二つのインスタンスを生成します(zipを2回展開するだけです)
HSQLDBを使っていないなら、JDBC ドライバを両方のインスタンスの server/all/lib/ へデプロイします。
WEB-INF/web.xml の最初の子要素として <distributable/>
を追加します。
org.jboss.seam.core.init
の distributable
プロパティを true に設定し ManagedEntityInterceptor (i.e., <core:init distributable="true"/>
)を有効にします。
二つのIPアドレスが利用可能であることを確認します(2台のコンピュータ、二つのネットワークカード、または同じインタフェース上の二つのIPアドレス)。二つのIPアドレスとして 192.168.1.2 と 192.168.1.3 を仮定します。
最初の IP アドレス上のマスタとなる JBoss AS インスタンスを起動します。
./bin/run.sh -c all -b 192.168.1.2
そのログは1個のクラスタメンバと0個の他のメンバがあることをレポートすべきです。
スレーブとなる JBoss AS インスタンスにおいて server/all/farm ディレクトリが空になっていることを確認します。
2番目のIPアドレス上でスレーブになる JBoss AS インスタンスを起動します
./bin/run.sh -c all -b 192.168.1.3
ログは二つのクラスタメンバーと一つの他のメンバーが存在することを報告すべきです。また、その状態がマスターから取得されていることも示すべきです。
マスターインスタンスの server/all/farm へ -ds.xml をデプロイします
マスターのログでは、そのデプロイを確認できるべきです。スレーブのログでは、そのスレーブのデプロイを確認するメッセージを確認できるべきです。
アプリケーションを server/all/farm ディレクトリへデプロイします
マスターのログではそのデプロイを確認できるべきです。スレーブのログではスレーブへのデプロイを確認するメッセージが見えるべきです。デプロイされたアーカイブが転送されるまで3分まで待たなければならないかもしれません。
こうしてあなたのアプリケーションは HTTP セッションレプリケーションを行うクラスタ内で実行しています。しかし、もちろん、クラスタが実際に動作していることを検証したいことでしょう。
アプリケーションが二つの異なる JBoss AS サーバー上での起動の成功は確認できたのは良いですが、百聞は一見にしかずとも言います。マスターが停止したときにスレーブが引き継ぐために二つのインスタンスが HTTP セッションが交換することを検証してみたいことでしょう。
まず手始めにブラウザでマスターインスタンス上で実行中のアプリケーションにアクセスしてみましょう。これで最初の HTTP セッションが生成されます。ここで、そのインスタンス上で JBoss AS JMX コンソールを開いて次の MBean へナビゲートしてください:
Category: jboss.cache
Entry: service=TomcatClusteringCache
Method: printDetails()
printDetails() メソッドを呼び出します。アクティブな HTTP セッションのツリーが見えることでしょう。ブラウザのセッションがこのツリー内のセッションの一つに対応していることを確認しましょう。
ここでスレーブインスタンスに切り替えて JMX コンソールで同じメソッドを呼び出します。そこでは同じリストが見えるべきです(少なくともアプリケーションコンテキストパスの下で)。
こうして少なくとも両方のサーバーが同一のセッション群を持っていることが確認できています。さあ、そのデータが適切にシリアライズ、アンシリアライズできていることをテストする時が来ました。
マスターインスタンスのURLを使ってログインします。次に、2番目のインスタンスのURLを作ります。サーブレットパスの直後に ;jsessionid=XXXX を付けて、IPアドレスの部分を変更します。そのセッションが別のインスタンスに引き継がれていることが見えるはずです。ここでマスターインスタンスを kill してスレーブインスタンスからそのアプリケーションを継続して利用できることを確認してみましょう。server/all/farm ディレクトリからデプロイされたアーカイブを削除し、インスタンスを再起動します。URL内のIPをマスターインスタンスのものに戻し、URLのページを表示します。元々のセッションがまだ利用可能なことが確認できるでしょう。
オブジェクトがパッシベートとアクティベートすることを監視する一つの方法は、session または conversation スコープの Seam コンポーネントを作成し、適切なライフサイクルメソッドを実装することです。HttpSessionActivationListener インタフェースからの任意のメソッドを実行することが可能です (Seam は 非 EJB コンポーネントに関してもこのインタフェースを自動的に登録します)。
public void sessionWillPassivate(HttpSessionEvent e);
public void sessionDidActivate(HttpSessionEvent e);
または、二つの引数なしの public void メソッドに対して、それぞれ @PrePassivate
と @PostActivate
をマークするだけです。パッシベーションステップは各要求の最後に発生し、アクティベーションステップはそのノードが呼び出されたときに発生することに注意してください。
これで Seam がクラスタ内で実行する全体像を理解できたことでしょう。ここで Seam のもっとも神秘的で、すばらしい仲介者である ManagedEntityInterceptor について述べるときが来ました。
ManagedEntityInterceptor (MEI) は Seam におけるオプション扱いのインタセプタで、それが有効になると conversation スコープへ適用されます。それを有効にするのは簡単です。ただ org.jboss.seam.init.core component の distributable プロパティを true に設定するだけです。もっと簡単に言うと、コンポーネントディスクリプタ (components.xml) で次のコンポーネント宣言を追加 (または更新) します。
<core:init distributable="true"/>
これは HTTP セッションのレプリケーションを有効にはしませんが、Seam が EJB コンポーネントや HTTP セッション内のコンポーネントのパッシベーションを扱えるようにするための準備をします。
MEI は、全体としては同じ目標を達成する、二つの異なるシナリオ (EJB パッシベーション と HTTP セッションパッシベーション) に対応します。それは少なくとも一つの拡張永続コンテキストを使った conversation の寿命を通して、その永続コンテキストによってロードされたエンティティインスタンスが管理され続けることを確かにします(パッシベーションによって早まって分離状態になりません)。要するに、それは拡張永続コンテキストの完全性 (integrity) を守ります(したがって、保証します)。
前の文章はこの契約をおびやかすようなチャレンジが存在することを暗示しています。つまり、二つ考えられます。一つのケースはe拡張永続コンテキストを持つステートフルセッションBean (SFSB) がパッシベートされた場合 (メモリを節約するか、クラスタ内の別ノードにそれを移行するため)、そして二つめのケースが HTTP セッションがパッシベートされた場合 (クラスタ内の別ノードへ移行する準備をするため)です。
最初にパッシベーションの一般的な問題を議論し、次に前述の二つのチャレンジについて別々に見ていきたいと思います。
永続コンテキストは永続マネージャ (JPA EntityManager や Hibernate Session) がデータベースから (オブジェクト-リレーショナルマッピングによって) ロードしたエンティティインスタンス (オブジェクト)を格納する場所です。永続コンテキスト内では、ユニークなデータベースレコードごとに一つのオブジェクト以上は存在しません。永続コンテキストは、よくファーストレベルキャッシュとして参照されます。なぜなら、アプリケーションがユニークな識別子で要求したレコードが永続コンテキスト内にすでにロード済みであるならば、データベースの呼び出しは回避されるからです。しかし、それは単なるキャッシュということだけにとどまりません。
永続コンテキスト内のオブジェクトは修正可能で、永続マネージャはその修正を追跡します。オブジェクトが修正されたとき、それは "dirty" になったとみなされさます。永続マネージャはこれらの変更を write-behind (つまり、必要なときだけという意味です) として知られるテクニックを使ってデータベースに反映させます。このように、永続コンテキストは保留中のデータベースへの変更のセットを保持します。
データベース指向アプリケーションはデータベースから読み書きするだけよりもっとたくさんのことを行います。それらはデータベースに (一度で) 反映されるべきトランザクション情報を獲得します。この情報は一つの画面で一度に得られるとは限りません。さらに、ユーザーは保留された変更を承認するか、拒否するかについて判断する呼び出しを行う必要があるかもしれません。
ここで私たちが到達したことは、ユーザーの視点からトランザクションの概念は拡張される必要があるということです。そして、それこそが拡張永続コンテキストがこの要求に完全に適合する理由になっています。それは、変更をアプリケーションがオープンしている間保持することができるし、永続マネージャの組み込み機能を使って保留中の変更をデータベースに書き込むことができます。そのとき、アプリケーション開発者は低レベルの詳細について悩む必要はありません (EntityManager#flush()
への単純な呼び出しがこのトリックを実行します)。
永続マネージャとエンティティインスタンス間のリンクはオブジェクトの参照を使って維持されます。エンティティインスタンスはシリアライズ可能ですが、永続マネージャ (とその永続コンテキスト) はそうではありません。したがって、シリアライズのプロセスはこの設計に反して動作します。シリアライズは SFSB または HTTP セッション のいずれかがパッシベートするときに発生し得るものです。アプリケーション内の活動を維持するためには、永続マネージャとそれが管理するエンティティインスタンスはそれらの関係を失うことなしにシリアライズを行わなければなりません。それが MEI が提供する目的となるものです。
Conversation はステートフルセッションBean (SFSB) を念頭に最初は設計されたもので、主な理由としては EJB 3 仕様は SFSB を拡張永続コンテキストを保持するものとして指定しているということがあります。Seam は拡張永続コンテキストを保管する、Seam管理永続コンテキストと呼ばれるものを導入します。それは仕様書内の多くの制限 (複雑な伝播の規則と手動によるフラッシュ機能の不足) を回避します。両方とも SFSB と一緒に使うことが可能です。
SFSB はそれがアクティブであり続けるためにはクライアントがその参照を保持してくれることに頼っています。Seamはこの参照の理想的な場所として conversation コンテキストを提供しました。それゆえ、conversation コンテキストがアクティブである限り、SFSB はアクティブです。もし EntityManager が @PersistenceContext(EXTENDED) アノテーションを使ってSFSBにインジェクトされるなら、EntityManager はその SFSB にバインドされ、その寿命、すなわち conversationの寿命、を通してオープンでありつづけます。EntityManager が @In を使ってインジェクトされるなら、EntityManager は Seam によって維持され、conversation コンテキストに直接格納されます。こうして SFSB の寿命に独立して conversationの寿命の間、存続し続けます。
すでに説明してきたように、Java EE コンテナは SFSBをパッシベートします。それは JVM の外部のストレージの場所にオブジェクトをシリアライズすることを意味します。これは個々の SFSB の設定に応じて発生します。このプロセスは無効にすることすら可能です。しかし、永続コンテキストはシリアライズ可能ではありません (これは SMPC のみ正しい?)。つまり、Java EE コンテナの大きく依存して発生することです。仕様書はこの状況についてあまり明確ではありません。多くのベンダーは、もし拡張永続コンテキストの保証が必要ならばそのような状況が発生しないように言っています。Seamのアプローチはより保守的です。Seam は基本的に永続コンテキストを持つ SFSB を信頼していません。SFSB の各呼び出しの後、Seam は SFSB によって保持されるエンティティインスタンスの参照を現在の conversation へ移動し (それゆえ、HTTP セッションへ移動されます)、SFSB 上のこれらのフィールドに null を詰めます。そして次の呼び出し時の最初にこの参照を復元するのです。もちろん、Seam はすでに conversation 内に永続マネージャを格納しています。こうして、SFSB がパッシベートの後にアクティベートするとき、それはアプリケーションに絶対に悪影響がないようにするのです。
拡張永続コンテキストへの参照を保持するアプリケーション内で SFSB を使っていて、それら SFSB がパッシベート可能であるならば、MEI を使わなければなりません。この要求は (クラスタではなく) シングルインスタンスを使っているとしても有効です。しかし、クラスタ化された SFSB を使っているならば、この要求も適用されます。
SFSB のパッシベーションを無効にすることが可能です。詳しくは、JBoss Wiki の Ejb3DisableSfsbPassivation のページをご覧下さい。
SFSB のパッシベーションはHTTP セッションを活用して動作します。しかし、HTTP セッションがパッシベートしたらどうなるのでしょうか。これはセッションレプリケーションが終了したときのクラスタ環境で発生します。このケースは巧妙しすぎて扱うのが難しく、まさにMEI インフラストラクチャが活躍するところです。このケースでは永続マネージャはシリアライズできないので破壊されるでしょう。Seam はこの破壊を扱います (HTTP セッションが適切にシリアライズすることを保証します)。しかし、反対のケースではどうなるでしょう。MEI が エンティティインスタンスを conversation へ関連づけようとするとき、それはインスタンスをラッパー内に埋め込み、そのラッパーがシリアライズ後に永続マネージャをインスタンスに再関連付けできるような情報を提供します。アプリケーションがクラスタ内の別ノードにジャンプするとき (おそらくターゲットノードがダウンしたため)、MEI インフラストラクチャは基本的に永続コンテキストを再構築します。ここで大きく後退しているのは、永続コンテキストが (データベースから) 再構築され、保留中の変更を取りこぼしてしまうことです。しかし、Seam が成し遂げていることは、エンティティインスタンスがバージョンで管理されているなら、楽観的ロックの保証が守られるということを確かにします (なぜ dirty 状態は転送されないのでしょうか )。
クラスタにアプリケーションをデプロイし、HTTP セッションリプリケーションを使うなら、MEI を使わなければなりません。