Akka Projectの
Tutorial: write a scalable, fault-tolerant, persistent network chat server and client
の「Sample application」以降を読んでみます。サンプルの動作確認方法は、前回のエントリで書きました。
正確に翻訳できるほどの英語力はないので、コードやScalaの冗長な説明は省いて、重要と思われる点だけ、だいたいこんなことが書いてあるんじゃないか程度。間違ってたらごめんなさい。
システム間でやりとりするメッセージが不変(immutable)であることはとても重要。
Actorモデルは、「Actor同士で異なる状態を持たない」という単純なルール上になりたっている。そのためにはつまり、可変のメッセージを送らないことが唯一の方法。
Scalaでは case class という素晴らしいメッセージ作成方法がある。
chat ! message
などとActorにメッセージを送る。! は、応答を待たずに非同期でメッセージを送る。
応答を待つ場合は、!! を使う。
普通の関数のように即時に応答が返るわけではないが、Future(se.scalablesolutions.akka.dispatch.Future)を使って応答を待つ。Futureは別スレッドで動作し、Actorからの応答を待機する。内部ではタイムアウトやリトライなどの管理を行っている。
!!はOption型を返す(説明省略)。
RemoteClientはリモートにあるActorの参照を簡単に取得する。あたかもローカルにあるActorを扱うかのように、透過的にリモートActorを扱うことができる。
storage ! message
storage forward message
メッセージを送る点では両者は同じだが、forwardのほうは「メッセージの送り主(Client)の参照」をstorageに渡すことができる。これによってstorage側では、Clientに直接返信することができるようになる(RedisChatStorageのソースを見るとわかりやすいかも)。
Akkaでは、障害に対して "let it crash(クラッシュさせちゃいなよ)"というアプローチをとっている。これはJavaなどの並列処理に向かない言語・フレームワークが取ってる手法と違う。
(letitcrash.comというサイトが・・)
まず、並列処理を考える。
Actorがlinkしていない場合(Actorのlinkについては「NetPenguinの日記」)は、Exception発生時にそもそもハンドリングできない(ログのスタックトレースを見るくらいしかない)。
Linked ActorならException通知やハンドリングができるようになる。Linked Actorを使っていれば、障害発生時、全部復活するか全部殺すか、どちらかの対処ができる
Akkaは、非防衛的(?)プログラミングを奨励している。
障害は起こってしまうものだから、障害発生を妨げるようなことはしないで、その代わり、発生しうる障害を予期したうえで(後述の監視対象Actorのライフサイクルのことを言っていると思われる)、さっさとクラッシュさせて、別の何か(インスタンス・プロセスなど?)に対処させること。
つぎに分散Actorに焦点をあてる。
Actor supervision/linking は、リモートActorの状態監視、リモートActorがダウンしたときにどうするか(同じノードや別のノードでリスタートさせるなど)、などを実現するクリティカルな機能だ。
Supervisorとは、子Actorを開始・停止・監視するActorのこと。
Supervisorの基本的な考え方は、「必要に応じて(障害が起きたら)子Actorを再起動」して生きた状態をキープすること。これは耐障害サーバをどのように構築するか、意見の相違があるところだ。
エラーを避けるためのあらゆる手段をつくすのではなく、このアプローチでは「障害を抱擁(許容?)する」という考え方。再起動することで安定した状態にする、これが"let it crash"。
Akkaには2つの再起動方法がある。
OneForOne: クラッシュしたコンポーネントのみ再起動.
AllForOne: 監視している全てのコンポーネントを再起動
AllForOneは、1つクラッシュしたら他のコンポーネントに影響がある場合に使う。
Supervisor Actorの定義方法には2つ、declaratively(宣言的)とdynamically(動的)あるが、このサンプルではdynamicallyを使う。
まずやることは2つ
1.faultHandlerを使って、障害発生時のハンドラを定義
する(OneForOneかAllForOneを選択する)
2.再起動のトリガーとなるExceptionを選択して、
trapExitにセットする。
子Actor(このサンプルではstorage Actor)を監視する最後の手続きは、"link(actor)"を実行すること。
これにより、LinkしたActorがクラッシュした際に、Exceptionを受け、trapExitにマッチした場合に、faultHandlerに応じてActorの再起動が走る。
このサンプルのようにメッセージブローカーとしてActorを使うのはとても一般的なパターンだ。
Actorは独立したプロセス間でメッセージをやりとりするには素晴らしいが、atomicに状態を共有するのは難しい。
この問題を補完するのが、Software Transactional Memory(STM)で、AkkaにもSTMの実装が含まれている。
AkkaはActorとSTMを結合したTransactors(Transactional Actorsの略)という仕組みを提供している。これはベストなActorモデルなんじゃなかろうか。
Akkaでは現在、Map/Vector/Refの3つのトランザクションインターフェースを用意している。これらのオブジェクトは複数のActorで共有できてSTMによって管理されている。そしてトランザクション境界の外でこれらを更新しようとすればExceptionが発生する。
複数のActorが並列的に読み書きできる共有メモリを使うことができる。
トランザクション間で障害が発生したら、中止(ロールバック)するかリトライする。
STMによって、「ACID」のうち、Atomicity/Consistency/Isolationは得られたが、Durability(耐久性)が足りない。しかし、Akkaのpersistence(永続化)moduleが、これを補助してくれる。
AkkaはSTMインターフェースを拡張する形でPersistence moduleを提供している。バックエンドストレージには、Cassandra、MongoDB、Redisのモジュールが用意されている。
atomic { ... } を使ってトランザクションを実行する。
Redisのデータ構造はバイナリなので、今回は文字列をバイナリ変換(message.getBytes("UTF-8"))しているが、もっと複雑なデータでは、インスタンスをシリアライズして格納する。シリアライズの方法はserialization sectionを参照。
ChatServerはCharStorageを監視(supervise)していたわけだが、ここで「監視対象Actor」の側面をみてみよう。
まず、監視対象Actorはライフサイクルを定義する必要がある。RedisChatStorageはPermanentとしてlifeCycleに設定している。
Permanent: 常に再起動される
Temporary: 再起動はされないが、
シャットダウンフックは呼ばれる。
障害時にクラッシュして、安定状態でリスタートして処理を続行するという考え方だが、"安定状態"の定義というのはドメイン依存であり、開発者が定義することだ。
Akkaでは、2つのActor再起動時コールバック関数 preRestart / postRestart を用意しているので、ここで安定状態にセットアップしてやる。エラー原因がわかるようにThrowableを引数に持っている。
(以下割愛)