アクターシステム

アクターシステム

アクターは、状態と振る舞いをカプセル化するオブジェクトであり、受信者のメールボックスにメッセージを置くことでメッセージを交換し、排他的に通信します。 ある意味では、アクターはオブジェクト指向プログラミングの中で最も厳格な形式ですが、人間にとってはより理解しやすいものです。アクターを使って問題の解決策をモデル化し、人々のグループを構想し、それらにサブタスクを割り当て、その機能を組織体系に整理し、障害をエスカレートする方法を考えます (実際には人を扱わないという利点があります。つまり、感情的な状態や道徳的な問題に心配する必要はありません) 。その結果、ソフトウェア実装を構築するための精神的な足場として役立ちます。

注釈

アクターシステムは、1 ... N のスレッドを割り当てる重い構造体なので、論理的なアプリケーションごとに 1 つ作成します。

階層構造

経済組織のように、アクターは自然に階層を形成します。 プログラムの特定の機能を監督する 1 人のアクターは、その仕事をより小さく、より管理しやすいものに分割したいかもしれません。この目的のために、それが監督する子アクターを導入します。スーパービジョン (監督) の詳細は ここ で説明されていますが、このセクションでは根底にある概念に集中します。理解のための唯一の前提条件は、各アクターが、そのアクターを作り出した、ちょうど 1 人のスーパーバイザーを持つということです。

アクターシステムの典型的な特徴は、1 つの部分で扱えるほど十分に小さくなるまでタスクが分割され、委任されることです。そうすることで、タスクそのものが明確に構造化されているだけでなく、結果的に、どのメッセージを処理すべきか、どのように正常に反応すべきか、そしてどのように障害を処理すべきかということがアクターから推論することができるようになります。1 人のアクターが特定の状況に対処する手段を持っていない場合、対応する失敗メッセージをスーパーバイザーに送信して、助けを求めます。 再帰的な構造によって正しいレベルで障害が処理できるようになります。

これを、障害を漏れ無く考慮した防御的プログラミングになりやすい、階層化されたソフトウェア設計と比較してください。問題が適切な人に伝達されれば、すべてを「カーペットの下に」入れてしまうよりも優れた解決策を見出すことができます。

そのようなシステムを設計することの難しいところは、誰が何を監督すべきかということの決定方法です。もちろん最高の解決方法はありませんが、役立つガイドラインがいくつかあります:

  • 1 つのアクターがサブタスクを他のアクターに委譲したりして、そのアクターがしている仕事を管理しているとすると、マネージャは子供を監督すべきです。なぜなら管理者が、どの種類の障害が予想され、どのように対処するのかを知っているからです。

  • 1 つのアクターが非常に重要なデータを運ぶ場合 (避けられるなら状態は失われないようにすべき)、このアクターは、危険なサブタスクを監督している子供に送信し、これらの子供の失敗を適切に処理すべきです。 リクエストの性質によっては、リクエストごとに新しい子を作成することが最善であることがあり、返信を収集するための状態管理をシンプルにできます。 これは Erlang の "Error Kernel Pattern" として知られています。

  • あるアクターがその義務を果たすために、別のアクターに依存している場合、他のアクターの生死を監視し、終了の通知を受け取って行動する必要があります。 監視者がスーパーバイザー戦略に影響を与えることはないので、スーパービジョンとは異なります。機能的な依存関係だけでは、特定の子アクターを階層のどこに配置するのかを決定する基準にはなりません。

もちろん、これらのルールには常に例外がありますが、ルールを守るか破るかにかかわらず、常に理由を持つべきです。

コンテナを構成

アクターがアンサンブルするアクターシステムは、スケジューリングサービス、構成、ロギングなどの共有設備を管理するための自然な単位です。異なる構成を持つ複数のアクターシステムは Akka 自身の中でグローバルな状態共有が無ければ、同じ JVM 内で問題なく共存できるはずです。これを 1 つのノード内、またはネットワーク接続全体にわたるアクターシステム間の透過的な通信と組み合わせることで、アクターシステム自体を機能階層の構成要素として使用できます。

アクターのベストプラクティス

  1. アクターは素敵な同僚のようになるべきです。他の人の気を不必要に遣わせることなく効率的に仕事をし、リソースを奪わないようにします。 プログラミングの言葉にすると、イベント駆動型の方法でイベントを処理し、レスポンス (または更なるリクエスト) を生成することを意味します。 アクターは、やむを得ない場合を除いて、ロック、ネットワークソケットなどの外部のエンティティをブロックしてはいけません (つまり、スレッドを占有している間は受動的に待機してはいけません) 。例外は以下の後者の場合を参照してください。

  2. アクター間で可変なオブジェクトをやりとりしないでください。そのためには、メッセージが不変であることが好ましいです。 変更可能な状態を外部に公開することでアクターのカプセル化が壊れると、通常の Java の並行処理の土俵に戻ってしまい、あらゆる欠点を抱えることになります。

  3. アクターは、振る舞いと状態のコンテナであり、ふつうはメッセージで振る舞いを送信することはしません (Scala のクロージャを使う誘惑があるかもしれません)。そのリスクの 1 つは、アクター間で誤って可変の状態を共有してしまうことです。このアクターモデルの違反は、残念なことにアクタープログラミングのすばらしい体験をもたらす性質を台無しにします。

  4. トップレベルのアクターは、エラーカーネルの最も奥にあるので、それらは控えめに作成し、本当に階層的なシステムであることが好ましいです。 これは、障害のハンドリング (構成の細かさとパフォーマンスの両方を考慮する場合) において利点があります。また、ガーディアンアクターの負荷を軽減します。これを過度に使うと、競合ポイントの一つになります。

ブロッキングには慎重な管理が必要

場合によっては、ブロッキング操作、つまりスレッドが不定期にスリープするようにして外部イベントが発生するのを待つことは避けられないことです。例えば、従来の RDBMS ドライバーやメッセージング API があり、その根底にある理由は、一般的に (ネットワーク) I/O がカバーの下で発生するためです。このようなことに直面した場合、ブロッキングコールを単に class:Future の中にラップして、その代わりに使うことができますが、この戦略は単純すぎます。アプリケーションが高い負荷で実行されているときに、ボトルネックになったり、メモリやスレッドを使い果たす可能性が非常に高いです。

この "ブロッキング問題" に対する適切な解決策の非網羅的なリストには、以下のような提案があります。

  • アクター (またはルーター [Java, Scala] によって管理されているアクター) の中でブロッキングコールを行い、この目的専用にしているか、または十分な大きさのスレッドプールを設定してください。

  • ブロッキングコールを Future 内で行い、このような呼び出しの数の上限をある時点で設けます (タスクを無制限に実行すると、メモリやスレッドを使い切ってしまいます) 。

  • アプリケーションを実行するハードウェアに適したスレッド数の上限をスレッドプールに設定し、 Future 内でブロッキングコールを行います。

  • 単一のスレッドを一連のブロッキングリソース (たとえば、複数のチャネルを駆動させる NIO セレクタ) の管理専用にして、アクターメッセージとして、発生するイベントをディスパッチします。

最初の可能性としては、一度に 1 つの未処理のクエリのみを実行し、内部同期を使用してこれを保証する伝統的なデータベース処理など、自然にシングルスレッドであるリソースが特に適しています。 一般的なパターンは、N 個のアクターのためのルーターを作成することです。各アクターは、1 つのDB接続をラップし、ルーターに送信されたクエリを処理します。 スループットを最大化するために N をチューニングしなければなりません。これは、どの DBMS がどのハードウェアに配備されているかによって異なります。

注釈

スレッドプールの設定は、Akka に委譲するのが最適です。単純に application.conf で設定し、 ActorSystem [Java, Scala] でインスタンス化するだけです。

あなたが心配すべきでないこと

アクターシステムは、それ自身に含まれるアクターを実行するために、構成されたリソースを管理します。このシステムには数百万ものアクターがいるかもしれません。それらが大量にあるとみなすのが全てのマントラであり、オーバーヘッドはインスタンスごとに、たった約 300 バイトの重さです。当然ながら、大規模なシステムでメッセージが処理される正確な順序は、アプリケーション作成者が制御できるものではありませんが、これも意図したものではありません。 Akka がカバーの下で重いものを持ち上げている間、リラックスして一歩踏み出してください。

Contents