Akka Actorのメッセージデリバリーの信頼性について
Akka Actor 2.3.9におけるメッセージデリバリーの信頼性のドキュメント、Message Delivery Reliabilityの意訳を公開します。
この内容は、他のメッセージパッシングのライブラリやメッセージキューを採用する際に、Akkaとどのような差があるのかを理解するということや、IDDD(実践ドメイン駆動設計)で紹介されているイベントソーシングを、Akkaでどのように実装するのか、冪等処理モデルの代わりにAkka Persisitenseがどのような処理を利用し、信頼性のあるメッセージングを獲得するのかを理解するために。重要な内容となります。
ですので意訳ではありますが訳を掲載することにしました。もし、英語訳の不備で指摘等ありましたら教えて下さい。
メッセージデリバリーの信頼性
Akkaは、一台のマシンのマルチコアプロセッサーで役立つ"スケールアップ"という概念、と、分散コンピュターネットワークで役立つ"スケールアウト"という概念、を共に実現するための信頼性のあるアプリケーションの構築を助けてくれます。
これらの働きは、コードの単位としてのアクターの間のメッセージパッシングと相互作用によって表されます。
この章では、アクター間のメッセージの正確なセマンティクスを説明していきます。
以下で意味のある議論をするために、アプリケーションは複数のネットワーク上のホストにあることを考えます。通信の基本的なメカニズムとしては、ローカルJVMのアクターへのメッセージの送信と、リモートアクターのメッセージの送信は同じものです。
しかし、デリバリーのレイテンシ(場合によっては同様に、ネットワークの帯域やメッセージのサイズにも依存する)と信頼性に関しては、明確な差があります。
リモートで送信されたメッセージの場合には、明確に多くの問題が起こりそうなステップがあります。
他の側面から見ると、ローカルの送信は同じJVMの参照にメッセージを渡しているだけです。
これに関しては、送信オブジェクトに様々な制限がありません。一方、リモート転送はメッセージサイズに制限を受けてしまいます。
常にインタラクションをするようなアクターを書く時、おそらく安全にアクターのやりとりをリモート状態にできます。これは、以下の議論で説明する性質が、常に保証されているからです。もちろん、アクターの実装においていくらかのオーバーヘッドがあります。例えば密接にコラボレーションするアクターのグループを扱うときに、ロケーション透過性を犠牲にすれば、あなたは同じJVM上にアクターを配置し、メッセージデリバリーにおいて厳密な保証をすることもできます。
以上のようなトレードオフの詳細は、以下で議論していきます。
補足として、強い信頼性を獲得するために、ビルトインされている機能をアクターの処理の上に構築する方法も紹介します。
そして最後に、この章で"Dead Letter Office"の役割について議論します。
一般的ルール
以下で、Akkaのメッセージの送信に対するルールを紹介します。(例として、tell
や!
メソッド、もちろんask
パターンも同様です)
- at-most-once delrivery つまり、デリバリーを保証しない
- メッセージは送信者と受信者のペアにおいては順番になる
最初のルールは、他のアクターの実装でも典型的に見られるものです。そして2つ目はAkka独特のものです。
議論: "at-most-once"とはなにか?
デリバリーのメカニズムのセマンティクスは、以下の3つに分類されます。
- at-most-once は、メッセージを0回または1回配信します。簡単に言うとメッセージは消失するかもしれません。
- at-least-once は、最低でも1回は届くように何度も配信します。簡単に言うと、メッセージは消失しない代わりに重複するかもしれません。
- exactly-once は、受信者に正確に1回メッセージを配信します。メッセージは、重複も消失もしません。
最初のものは最も安価です。高いパフォーマンスで実装のオーバーヘッドも最小です。この撃って忘れる(fire-and-forget)スタンスは、送信の終わりや転送のメカニズムにおいて状態を持つことはありません。二つ目のものは、転送の消失に対してリトライをします。これは、送信の終了と受信の受け取り証明書(Acknowledgement)を得るまでの状態を保持することを意味します。三番目のものが一番高コストで、最もパフォーマンスの悪いものです。なぜなら二番目のものに加えて、受信が終わるまで配信の重複をフィルタするための状態を持たねばなりません。
議論: なぜデリバリーを保証しないのか?
この疑問は、正確には以下の疑問を保証することが、問題の中心にあります。
- メッセージがネットワークに送られたか?
- メッセージが他のホストに受信されたか?
- メッセージか対象のアクターのメールボックスに入ったか?
- メッセージが対象のアクターによって処理され始めたか?
- メッセージが対象のアクターによって正しく処理されたか?
それぞれの質問に対して別々の試みとコストが求められます。これらは明らかに、メッセージパッシングのライブラリーが応えることができない状態であることがわかります。例えば3番目の疑問に関して、設定変更可能なメールボックスを利用していたことを考えてみてください。同様に5番目の疑問に関して、何を持って"正しく"とするのかを考えてみてください。
Nobody Needs Reliable Messagingにも同じ理由が述べられています。送信者がこのインタラクションが成功かを知る唯一の意味のある方法は、ビジネスレベルの成功証明書(Acknowledgement)を受け取ることです。これはAkkaがすることではありません。(我々はフレームワークで"これが何を意味するか"とか、あなたが何を私たちに求めるかを書くことはできません。)
Akkaは分散コンピューティングを行うにあたって、明確にメッセージパッシングで失敗する可能性を持ち合わせています。
つまりは、偽ろうともしないし、危うい抽象化もしません。このモデルは、Erlangで使われている成功したモデルで、ユーザー自身にアプリケーション周りの設計を求めるものです。このアプローチに関しては、Erlang documentation(セクションの10.9と10.10)に書いてあり、Akkaはそれに厳密に従っています。基本的な保証しかしないというこのことは、別な視点から言うと、これらのユースケースが、強い信頼性を求めず、
それらのコストも払うこともしないということなのです。これらにより、基本的にはこの機構の上にいつでも強い信頼性を追加することができ、逆を言うと、もっとパフォーマンスを得るためにこれ以上信頼性を取り除くということはできません。
議論: メッセージの順番
特に与えられたペアのアクターについてのルールで、1つ目から2つ目への直接の送られたメッセージは、順不同に受信されるということはありません。直接、という言葉が、tellの実行者から最終的な目的地への送信でのみこの動作の保証を適用するということが強調されています。これは、メディエーターを用意したり、メッセージの散布のような特徴(指定されてない相手でない)を利用しない時のことです。
この保証は、以下のように表現されます。
アクター
A1
がメッセージM1
,M2
,M3
を アクターA2
に送信します
アクターA3
がメッセージM4
,M5
,M6
を アクターA2
に送信しますこれが意味するところは、
M1
はM2
とM3
より前に届くM2
はM3
より前に届くM4
はM5
とM6
より前に届くM5
はM6
より前に届くA2
には、A1
とA3
のメッセージが交互に届くことがある- 配達は保証されていないので、いくらかのメッセージは捨てられるかもしれない。(例えば、
A2
に届かないなど)
Note
Akkaが順番を保証するのは、受け取り手のメッセージボックスにメッセージが順番に入る順である、ということは重要なことです。たとえばFIFO(先入れ先出し)ではない順番付けをするメールボックスの実装(PriorityMailbox
のような)の場合、アクターの処理は、キューイングを行った順番とは異なるものとなります。
また、このルールは推移(transitive)しないということに気をつけましょう。
アクター
A
がアクターC
へメッセージM1
を送った
アクターA
がアクターB
にメッセージM2
を送った
アクターB
はメッセージM2
をアクターC
に転送した
アクターC
はM1
とM2
を様々な順番で受け取る可能性がある
一般的に推移する順番では、M2
がM1
よりも先にアクターC
に受け取られることはないことを暗に意味します。(たとえこれらのどれから失われたとしても)ただこの順番は、A
とB
とC
が異なるネットワークホストにあるという場合、メッセージ配送はそれぞれ異なるレイテンシを持っているということに違反しています。
詳しくは以下を見てみましょう。
Note
メッセージを親から子へ送るようなアクターの生成メッセージに関しても、上記で議論されているセマンティクスに脅かされています。最初に子どものアクターを作成するメッセージの順番が改変されて、メッセージが送られた時に、メッセージの到達時にその子どもアクターがまだ存在していないことがあります。例えば、リモートアクターR1
が作られるメッセージがとても早く受信され、作成されて、そのアクターR1
の参照が他のリモートアクターR2
に送信されて、R2
がR1
にメッセージを送る場合は、アクターの成と即時のメッセージの送信がちゃんと順番になり役目をはたします。
通信の失敗
上記で議論された順番に対する保証は、複数のアクター間のユーザーメッセージでのみのものだという事に気をつけましょう。子のアクターの失敗は、普通のユーザーメッセージの順番とは関係なく、特別なメッセージシステムで通信をしています。
例えば:
子アクター
C
が、親アクターP
にメッセージM
を送った
子アクターが失敗して、失敗F
が生じた
親のアクターP
はM
,F
またはF
,M
のいずれかの順番でメッセージを受け取る
この理由は、内部システムのメッセージは自身で他のメールボックスを持っており、これらの順番は、ユーザーメッセージとシステムメッセージの間の順番を保証してはいないからです。
JVM内(ローカル)のメッセージの送信のルール
このセクションで気をつけるべきこと
この章で紹介される強い信頼性は、アプリケーションをローカルで動かす時のみの開発のものであり、推奨されるべきものではありません。アプリケーションをもしクラスタ上で動かす場合には、設計し直しが必要になっていまします。(ローカルのアクター間で交換されてしまっているメッセージに対して設計し直すという意味です)我々の信条として、"一度設計されたものは、様々な場所でデプロイされるべき”という考えがあり、それをする際の考えは、上記の一般的なルールに依存しています。
ローカルでのメッセージ送信の信頼性
Akkaのtest suiteは、ローカルのコンテキストにおいて、メッセージが消失しないということに依存しています。(そしてリモートのデプロイも同様に、エラーのないコンディションという想定です)これは、テストを安定に保つための最大限の努力として適用されています。ローカルでのtell
の操作は、残念ながら、普通のJVMのメソッドの呼び出しと同じような理由で失敗します。
StackOverflowError
OutOfMemoryError
- 他には、
VirtualMachineError
加えて、Akka独特のローカルの送信の失敗もあります。
- メールボックスがメッセージを受け入れない場合(例えば、BoundedMailBoxが一杯になる)
- 受信したアクターが処理に失敗したり、アクターがすでに終了していた場合
1つ目の方は明らかに設定の問題ですが、2つ目の方は考えるに値します。メッセージの送信者は、もし処理中に例外が生じてもフィードバックを得ることはありません。そのアクターのスーパーバイザーが、かわりに通知を受けとることになります。
これによって、一般的にメッセージが消失したのかどうかが、外の観察者からはわかりにくいものとなっています。
ローカルメッセージ送信の順番
厳密なFIFOのメールボックスを想定するならば、厳密なコンディションであれば、上記で言及された、メッセージの順番の保証の非推移は解消されます。ただし、注意すべきことに、これらの状況はちょっと微妙で問題があります。この事象に対する網羅的ではないリストとしては、
- トップレベルのアクターから返信を受け取る前、フェアな中間的な一時キューを守るロックが存在しています。これは、アクターが作られる間に、複数の送信者からのリクエストのエンキューが、低レベルのスレッドのスケジューリングに依存して並び替えられているという問題があることを意味しています。
- Routerを作っている時にも同様のメカニズムが利用されます。正確にはルーティングされるActorRefです。つまり同じ問題が、ルーターにアクターをデプロイするときにもおこります。
- 上記で指摘したとおり、この問題は、エンキューを取り巻くロックではどこでも生じます。たとえば、カスタムのメールボックスを適用する場合にも起こります。
このリストは注意深く書かれたものです。他の問題のシナリオに関しては私達は推測を避けています。
どうやってローカルの順番をネットワークの順番に関連付けるか
前の段落で説明したとおり、ローカルのメッセージは、厳密な環境下では、推移する因果的な(causal)順番に従います。リモートメッセージの転送は、たったひとつのネットワークリンクあれば、推移する因果的な順番に翻訳されます。2つのネットワークホストの場合は、複数のリンクが巻き込まれます。例をあげると、3つのアクターが3つの異なるノードにあるときには、上記で言及したように何の保証もありません。
現在のリモート転送においては、推移する因果的な順番はサポートされていません。(重ねて、これはロックの非FIFOの起動順に引き起こされます。この時、接続の確立がシリアライズされます)
未来への思考的観点として、複数のアクターにおいては、リモート転送レイヤーで完全に再度実装することによって、順番の保証のサポートをすることができるかもしれません。同様に、低レイヤーの転送プロトコルであるUDPやSCTPという高いスループットと低いレイテンシを利用し保証を再度取り除くことにより、可能にできるように見えます。ただし、保証とパフォーマンスのトレードをする異なる実装が求められます。
高レベルの抽象化
Akkaのコアにある一貫性のある小さなツールをベースとして、Akkaはパワフルで高レベルの抽象を提供します。
メッセージングのパターン
上記の議論の結果から、信頼のあるデリバリーへの要求に答えるための方法は、ACK-RETRYプロトコルとなります。
この処理の説明を簡単に言うと、
- 個々のメッセージに証明書と関連付けるIDを振る
- 時間内に証明書が受け取れない場合には、メッセージを再度送るというリトライメカニズムを用意する
- 受信者は重複を検出して、重複したものを除去する
この3番目は、到達を保証されない証明書の特性に引き起こされる必須なものです。ビジネスレベルでのACK-RETRYプロトコルは、Akka PersistenceモジュールのAt-Least-Once Deliveryによってサポートされています。At-Least-Once DeliveryではメッセージのIDのトラッキングにより重複の検出がされます。サードパートの他の実装としては、ビジネスロジックレベルでメッセージを冪等処理(何度実行しても同じ結果となるようにする)してしまう方法があります。
上記の3つの要求を実装した他の例は、Reliable Proxy Patternです。(これは今では、At-Least-Once Deliveryに取って代わられています。)
イベントソーシング
イベントソーシング(とシャーディング)は、数十億を超えるユーザーで利用できるように、Webサイトを大きくスケールできるようにしてくれるものです。このアイディアは、とてもシンプルです。一つのコンポーネント(アクターを考える)が、コマンドを処理し、コマンドの影響を表すイベントのリストを生成します。これらのイベントは、コンポーネントの状態として適用されて、保存されます。このスキームの良い所は、イベントはストレージに追加されるだけということです。つまり可変データではありません。これは、完全なレプリケーションとイベントストリームの消費者(consumer)をスケールさせることができます。(つまり、その他のコンポーネントは、異なる大陸にあるようなコンポーネントの状態をリプリケーションしたり、変化に対してリアクトしたりするために、イベントストリームをコンシュームするということを意味しています)
もし、コンポーネントの状態が消失した場合(キャッシュ溢れによるマシンのダウンが原因で)は、イベントストリームをリプレイすることで、簡単に状態を再生成することができます。(大抵の場合、プロセスのスピードアップのためにスナップショットが利用されますが)、Event sourcingは、
Akka Persistenceでサポートされています。
明示的な証明書を使うメールボックス
カスタムのメールボックスを実装することで、一時的な失敗を、受信側でハンドルするアクターのリトライ処理を利用することができます。このパターンは、アプリケーションの要求を満たす配信を保証するために、ローカルでとても役に立ちます。
適用されるThe Rules for In-JVM (Local) Message Sendsの警告に注意して下さい。
このパターンの例は、Mailbox with Explicit Acknowledgementで実装例をいることができます。
デッドレター
配信されなかった(と確認された)メッセージのことです。そしてこれは/deadLetters
と呼ばれる統合的なアクターに配送されます。この配信は、基本的には最大限の努力のもと行われています。とはいえ、ローカルJVMの中でこの失敗は起こったりします。(例えば、アクターが終了中だとかです。)信頼できないネットワーク転送を通じてのメッセージは、デッドレターとして生じることなく失われます。
デッドレターを何に使うのか?
主な使われ方はデバッグです。特に、一貫してアクターが送信したものが届かないような場合に使います。(デッドレターが見つかるような場合には、送信者か受信者に何かしらの悪いところがあります)この目的のために役に立つように、deadLettersというアクターにメッセージを送ることを避ける事は良い習慣です。言い換えれば、アプリケーションを起動するときに、ちゃんと状況に適合したデットレターのロガーを使い、いつもログのアウトプットはクリーアンプしておきましょうということです。この練習は、一般的な思慮深いアプリケーションが求めることですね。送信者のコードをわかりにくくさせるような、終了したアクターへのメッセージングを避けるという事は、明瞭なデバッグアウトプットを得るよりもましです。
どうやってデッドレターを受信するのか?
akka.actor.DeadLetter
をアクターは、イベントストリーム内で購読(subscribe)することができます。Event Stream (Java)かEvent Stream (Scala)にどうするのかが書いてあります。購読したアクターは、(ローカル)システムで発行されたすべてのデッドレターを受け取ることになります。デッドレターはネットワークを通じて伝搬することはありません。もし、ひとつの場所でこれらを集めたい場合には、手動でネットワーク毎にこれらのアクターを購読しなくてはいけません。同様に、これらのデッドレターは、送信操作が失敗したと決定された場所において生成されると考えられます。このデッドレターは、ローカルのシステムのもの(ネットワークが確立していない)か、リモートのもの(その時点においてActorが存在していない)になります。
デッドレターは(たいていの場合)心配事ではありません
Actoreが自身の決断によって終了されない時はいつも、自分自身に送ったメッセージが消失する可能性があるのです。複雑な終了処理シナリオの中でいつもデッドレターは簡単におこってしまいます。捨てられたakka.dispatch.Terminate
メッセージは、終了リクエストを2度と受けることを意味します。しかしこれは、一回は成功しているということなのです。同じ気質で考えると、階層構造を止めるような、子アクターたちからの親アクターへのakka.dispatch.Terminate
メッセージは、複数のデッドレターを発生させます。これは、いずれかの子どもが、親の終了しているにも関わらず、親を見続けてしまうためです。