アクターシステム
アクターは状態と振る舞いをカプセル化されたオブジェクトのことであり、アクター同士でメッセージを相互に送り合い処理を完遂していきます。
そのアクターの集合体はアクターシステムと呼ばれています。
Akkaでのアクターシステムは設定(application.conf)やログなどの共通機能を管理する単位と位置づけられており、この単位が1JVM内での最大単位となっています。
ActorSystem.create()
メソッドによって一番最初のアクターを生成すると同時にアクターシステムも起動します。
ActorSystemは1~個のスレッドを使って処理をする非常に巨大な単位ですので、一つのコンピューター・コンテナ・JVMに一つの単位で設置することが推奨されます。
アクターシステムは階層構造
アクターシステムはアクターの階層(ファイルシステムのディレクトリ的な)を形成しており、親アクターが子アクターを生成することにより階層を作成しています。
この階層構造の真骨頂はタスクが分割され、一つの小さなアクターを高凝集にできることです。
タスク自体が明確に構造化されることにより、そのアクターはどのようなメッセージを処理するべきなのか、通常どういった動きをするべきなのか、失敗時はどう処理するべきなのかと言ったビジネスロジックを推論することができるようになります。これはある意味究極のカプセル化と言えます。
このようなシステムを設計する際に難しいのはタスクの構成をどの単位で区切って行くのかということです。
最適解は無いですが、参考になりそうなガイドラインがいくつか存在します。
- もし一つのアクターが複数の責務を保つ場合、それぞれの責務に合わせて別の子を作成しロジックと状態をシンプルにする。
- あるアクターがその責務を遂行するために他のアクターに依存している場合、そのアクターは他のアクターの生存状態を監視し、適切に子アクターを管理する(スーパーバイザ戦略)
アクターにおけるベストプラクティス
アクターシステムを安全に構成するためにアクター単体で注意しなくてはいけないことがいくつかあります。
- アクターは他のアクターのリソースを専有しないようにします。止む終えない場合を除き、ファイルやネットワークソケットなど外部のリソースをブロック(そのスレッドで占領したままにすること)してはいけません。
- アクター間ではミュータブルなオブジェクトを受け渡ししてはいけません。これは並行処理時などの他のスレッドからメッセージが書き換えられてしまう可能性があるからです。せっかくマルチスレッドプログラムが簡単になるようにAkkaさんが頑張ってくれてるのに台無しです。
- アクターはメッセージを受け入れることで、内部で定義された振る舞いを経て状態を変更していくのが基本的な動きです。メッセージに挙動(Scalaのクロージャとか、コールバック関数とか)を含めては行けません。
- アクターシステムのトップレベルのアクターにはロジックを含めてはいけません。親アクターは子アクターの管理だけに集中し、ビジネスロジックはすべて子アクターで処理するのが理想です。親はスーパーバイザ戦略により子を見守るだけの存在になりましょう
アクターシステムの終了
ユーザーガーディアンアクタを停止するか、ActorSystemのterminate()
メソッドを呼び出すことによって実行中のアクタがすべて停止します。
アクターとは
以前のアクターシステムの説明で、アクターは階層を形成しアプリケーションを構成する最小の単位であることを説明しました。今回はその参照単位であるアクターに関して詳しく解説して行こうと思います。
アクターは以下の3つの基本原則だけで成り立っています。
- 知っている送信先のアクタにN個のメッセージを送信する
- N個の新しいアクターを作成する
- メッセージが送られてきた際の振る舞いを定義する
アクターはState(状態)
・Behavir(振る舞い)
・Mailbox
・子アクター
・ スーパーバイザ
という要素で構成されています。
ここで注意していただきたいのが、一度アクターを作ったら参照されなくなったからといって自動的に破棄されない
ということです。
(ガベージコレクションされない)アクターの停止・破棄の管理は開発者のお仕事なのです。
アクターの参照(ActorRef)
context.spanw()
等でアクターは生み出しますが、その戻り値はすべてActoreRef
となっています。つまりアクターの参照を表すオブジェクトです。
アクターモデルの恩恵を受けるためには、アクターの実態を外部から完全に隠蔽する必要があるからです。
(アクターにはメッセージ経由でしかやり取りさせないメッセージ駆動なのに、直接オブジェクト操作されたら台無しだし、位置透過性が効かなくなるから)
この参照を使ってやり取りする仕組みのお陰でアクターは実体の位置に関係なくメッセージを送信することができます。
ローカルでもリモートでもクラスターでもまったく同じやり方でメッセージのやり取りを行うことができます。
またアクター参照はジェネリクスによって型指定され、指定されたタイプのメッセージしか送信できません。
状態
アクターに含まれている状態は他のアクターから完全に保護されていなければいけません。
ここで朗報です、Akkaでは概念的にそれぞれ独自の軽量スレッドを上でアクターを動かしており、それぞれのアクターは外部からの変更に保護されています。
また、一つのアクターはメッセージボックスから一つずつしか処理できない制約のお陰で、並列処理特有のリソースロックなどが不要になります。
ちなみにアクターはメモリ上に保存されているデータのため、アクターが失敗し親のスーパーバイザによって再起動されるときには状態がリセットされます。AkkaPersistence
という機能を使えばイベントソーシングの原理で状態が保持できます。
振る舞い(Behavior)
アクターにメッセージが送信されるたびに、アクターは予め登録していた振る舞いと照合しメッセージを適切に処理します。
この予め登録していた処理をBehaviorと呼びます。
MailBox
アクターの目的は送られてくるメッセージの処理でした。これらのメッセージは一度メールボックスと呼ばれるキューに送られ、アクターはそのメールボックスからメッセージを一つずつ取り出すことで振る舞いを実行します。
各アクターではこのメールボックスを一つだけ持っており、取り出し方法はデフォルトでFIFO(先入れ先出し)です。
この取り出し方法はカスタマイズすることが可能であり、メッセージの優先順番順などにすることが可能です。
子アクター
すべてのアクター子を持つことができ潜在的に親です。
サブタスクを委譲するために子を作成すると、自動的にその子を監督する対場になります。
子の生成と終了はバックグラウンドで非同期的に行われるため親をブロックすることはありません。
スーバーバイザ戦略
アクターの最後の構成要素は、予期せぬ例外つまりエラーを処理するための機構です。
子が発したエラーを親は監督し、再起動するか・停止するか・状態をそのまま復元するかなど選択することが可能となります。
アクターの監督(スーパービジョン)
子アクターで発生したエラーをうまく処理する仕組みをスーパーバイザ戦略と以前説明しました。
重要な概念なのでこの章では更に詳しく解説していこうと思います。
データの検証や予測される例外等はビジネスロジックの重要な部分なので予めアクターのメッセージ処理ロジックに追加します。
(ID無いのにユーザーの問い合わせをした、とか)
しかし予期しない障害が発生する場合が稀にあります。例えば、ネットワークリソースが追加できない、ディスクの書き込みが失敗する、開発者が予期してないアプリケーションロジックのバグなどです。
これらはアクターのメッセージ処理ロジックと混ざり合うべきではなくスーパバイザ戦略を利用し、ひとつ上の階層(親に)判断してもらうべきです。
監督する性質に応じてAkkaは3つの戦略を提供します。
- 蓄積された内部の状態を維持しながらアクターを再開する。
- アクターを再起動し、蓄積された内部の状態はクリアされる。
- アクターを完全に停止する(初期戦略)
アクターは階層構造によって構築されていると以前説明しましたが、これは障害を上方に伝播し親に処理してもらう仕組みと理にかなっています。
例えですが、直近の親ではなく2子上の階層のアクター(おじいちゃんアクター?w)に障害時の処理を委譲することができるのです。
これは通常の例外のtry-catchの仕組みに似ているのでなんとなく理解できると思います。
後述するアクターの監視通知の仕組み
を組み合わせて実現できるのですがcontext.watch(childRef)
を使用して子を監視すると子が停止した際、親も一緒にDeathPactException
を吐き停止しようとするのでこれを連続して書くとエラーバブルアウトの仕組みを構築できます。
アクターの監視(モニタリング)
Akkaのアクターライフサイクル監視は停止時しか監視できないので別名
DeathWatch
と呼ばれています。
上記では主に階層構造(親子関係)での監督の話をしてきましたが、各アクターは階層を無視した全く別のアクターを監視することができます。
ちなみに監視できる唯一の状態変化は停止
だけで再起動は停止も一瞬していますがスパーバイザの包まれてしまうため見えません。
監視すると小アクターが停止した際にTerminated
メッセージを受信します。このメッセージは適切にレシーバーで処理されない場合親アクターはDeathPathException
をスローするので注意が必要です。
先程の監督の説明でエラーは監視の仕組みと組あわせるとバブルアウトできると言いましたがこれのことです。
AcotrContext.watch(targetRef)
で監視を開始し、ActorContext.unwatch(targetRef)
で監視を終了することができます。
例外とかでアクターが停止するとどうなるの?
メッセージは?
メッセージの処理中にアクターが停止した場合は再起動したとしてもメッセージは失われます(メールボックスにも入っていない)。
したがって、メッセージの処理中に例外が発生した場合は親アクターでもう一回メッセージを送信するような処理を自分で書かなければなりません。
また、無限リトライが発生する可能性があるので試行回数に制限を設けないと積む場合があります。
メールボックスの中身は?
メールボックスはアクターと隔離されているので何も起こりません。
アクターが再起動すると同じメールボックスが存在し、メールボックス内のすべてのメッセージも再度閲覧することができます。
アクターはどうなるの?
アクター内のコードで例外をスローするとそのアクターは一時中断され監督もしくは監視プロセスが開始されます。
スーパーバイザの決定に応じアクターは再起動・停止・再開します。
アクター参照とアクターパスの違い
アクター参照
アクターの参照はActoreRefで表現されており、その主な目的はそれが指し示すアクターへメッセージ送信をサポートすることになります。
各アクターはcontext.self
フィールドにより、自分自身のActorRefにアクセスすることができます。これはよく返信を取得するために他のアクターへのメッセージへ含める場合に使用されます。
またアクター参照が提供してくれる大きな利点の一つに位置透過性があり、ローカルJVM内でもリモートJVM内でも全く同じようにメッセージのやり取りをすることが可能となります。(全く同じとか公式は言ってるけど、メッセージがシリアライズできるようにマーカーインターフェースつけたりとかは必要...)
アクターパス
アクターは階層構造で構築されるためアクターシステムのルートに向かって子と親の関係を再帰的にたどることが可能です。
このシーケンスはファイルシステム内のフォルダーと同じような構造となっており、分解されたアクター名とスラッシュで構築されています。
/user/hogehogeActor/hugahugaActor
ちなみに最上位のアクターたちのパスはこんな感じになっています
-
/user
ユーザーが作成したトップレベルアクターの更に上のガーディアンアクターです。 -
/system
システムで作成されるすべてのトップレベルアクターのガーディアンアクターです。 -
/deadLetters
停止したアクターまたは存在しないアクターに送信されたすべてのメッセージが送信されるアクターです。(ベストエフォートなので普通にメッセージが届いてないこともある) -
/temp
システムで作成される短命のアクターのガーディアンアクテーです。Pattern.ask
とかで使用されます。
位置透過性
アクター間のすべての通信はメッセージパッシングが使用され、全ては非同期です。
Akkaのやばいところは、これが単一のJVMの場合でも数百台のマシン上で組まれたクラスタ内
の場合でもすべての機能が等しく利用できてしまう点です。
このどんな環境でも等しく同じようにメッセージをやり取りできる様子を位置透過性と言います。
AkkaはP2P(ピアツーピア)
Akka Remotingはアクターシステムをピアツーピアで接続するための通信モジュールで、Akka Clusterの基礎となるものです。
これは2つの前提から成り立っています。
- システムAがシステムBに接続できる場合、システムBもシステムAに接続できる必要がある。
- 通信システムの役割が明確化されることはなく、リクエストを受け入れるだけのシステムもリクエストを投げるだけのシステムも存在しない。
早い話お互いが対等でなければなりません。P2Pですね。