BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Akkaを使って回復力のあるソフトウェアを作る

Akkaを使って回復力のあるソフトウェアを作る

原文(投稿日:2013/06/06)へのリンク

この記事で、私はあなたにAkkaの舞台裏ツアーをしてもらいますが、技術的な意味はありません。そのような好奇心は、ソースコードを見ることで満足することができます。私があなたにお見せするのは、その原則が導く、開発の背後にある原動力です。これらのアイデアは新しいものではありませんが、私はあなたにそれらがどのように接続されているかについて、新しい視点を与えることができることを願っています。

回復力とは何を意味するのか?

それと向き合いましょう: 障害は必ず起きます。あなたがどんなに一生懸命にやろうとしても、予測できずに、そしてテストが見つけなかったケースが1つ以上必ずあります。問題は「いかに障害を避ける事ができるか」ではなく、「いかにそれに対処できるか」です。回復力は Oxford 辞書 で次のように定義されています。

  1. 物質や物が元の形に戻る能力;弾力性
  2. 故障からすぐに回復する能力;タフさ具合

あなたのアプリケーションの一部を正常な形から歪めてしまうような故障の毎日起きる例には、一時的に利用できないデータベース・サーバー、素晴らしい新製品を誰もが使おうとして起きる負荷スパイク、どうにかそのサブルーチンまですり抜けて、それを窒息させる不正なユーザー入力などがあります。このような状況からの回復は、できるだけ早く正常なサービスを回復するために、理想的には自動的であるべきです。

この文脈における回復力は、障害は隔離されなければならないことを意味します。避けられるなら、障害が発生したコンポーネントは、他のコンポーネントをダウンしてはなりません。サービスレベルは下がり、恐らくある機能は一時的に利用できませんが、しかしアプリの残りの部分は尚動きます。一旦火事が広がることを阻止できたら、それを消さなくてはいけません。障害のあるコンポーネントは再スタートさせるか、ファイルオーバーによりバックアップシステムを起動する必要があります。これらのプロセスは自動で、信頼できる必要があります。なぜならそれらは計画外の全故障を捉えるからです。一度コンポーネントの機能が回復すると、他のコンポーネントとの正常なコラボが再開します。

疎結合が鍵です

示したばかりの観点から、回復力のあるアプリケーションを構成する個々のコンポーネントは、強く結合されていてはならず、密接に絡み合っていてはならないのは明白です。他のコンポーネントの内部状態に関するあらゆる知識は、障害回復の後では無効となり、あるコンポーネントを他のコンポーネントの障害に対して脆弱にしてしまいます。疎結合されたオブジェクトのアイデアは、大変古く、例えばSmalltalk-80で実現されており、そこでは、状態を保持する他に、オブジェクトはメッセージを使って他のオブジェクトと通信できるだけです。この抽象化は人間のやり取りの方法と一致している、非常に自然なものです。我々は文字通り、互いの脳を取り上げたりしません。替りに言語あるいは非言語によるコミュニケーションを介してメッセージを交換します。疎結合の重要なフィーチャは、内部状態は直接アクセスされず、あらゆるやり取りは、オブジェクトの振る舞いである保護層を介して行われます。

この原則の影は、Javaクラスの変数フィールドを外部の呼び出し者に直接公開しない、という共通のプラクティスにおいて、まだ見えます。その代わりに我々は反射的にIDEの助けを借りてgetterとsetterを生成します。しかし次に示すようにこれらは疎結合にしてくれません。

主流のオブジェクト指向言語のもう一つの特性は、オブジェクトの振る舞いを起動する(メソッドを呼ぶこと)が同期的に起きるため、メソッドコールが戻る時に、結果の値が呼び出しコードに即利用可能になり、あらゆる副作用が起きます。このことによる問題は、オブジェクトを利用不可能にできないことです。あらゆる外部の呼び出し側は、即答えに満足しなければなりません。もしそれが現在不可能であれば、唯一の選択肢は、例外を投げて、失敗をエスカレートすることです。次にこの例外は呼び出し側によって処理されなければなりません。障害回復を起動します。こうして呼び出し側は、いかにそれをするか知っている責任があります。これはあらゆるお客が自販機を修理できなければならないと、言っているようなものです。これは、非公開な状態に直接アクセス出来ない場合でさえ、オブジェクト間のかなり強い結合につながります。

従って、疎結合はコンポーネントがお互いにメッセージを非同期に送り、将来の時点でもっと先のメッセージの形式で可能な返答を受け取るように、通信する必要があります。もしあなたがこの文脈で始めて “component == object” であると考えているなら、このことは恐らく奇妙に思えるでしょう。しかし実際には、この技法は長い間、より高レベルで適用されてきました。エンタープライズアーキテクチャにおける異なるコンポーネントやサービスは、メッセージブローカーやキューを使って、一般的に分離されているので、サービスの故障と復帰は、メッセージング・インフラ内に入ってくるリクエストを保存し、サービスが戻った時に処理を再開することで、橋渡しできる。もう一つの例は、サービスの前面にあるユビキタスなRESTful HTTPインターフェースである。そこでは、HTTPリクエストメッセージは、クライアントによって送られ、サーバーはHTTPリプライメッセージを使って返答する。

あなたがあなたのアーキテクチャにおけるサービスについて考える時、それは通常数分で書かれたものでがありません。サービスは普通最大で数百のコンポーネント(このぐらい大きいと既にかなり大きなアプリケーションです)からなるもので、エンジニアリング作業のかなりの部分です。さて、あなたのサービスが内部的に同じ疎結合な方法で、より小さな規模で作られていたと、想像してください。これらの小さなオブジェクトは、非同期なメッセージパッシングによってのみ通信しています。もしそれらの一つが落ちても、他の全てが幸せに動き続けます。これらの小さなオブジェクトが Akkaのアクターであり、1つのアクターを数分で書けるのは充分納得出来ます。

非同期メッセージパッシングには監督が必要

HTTPサーバーの例から非常に明らかなのは、トラブルが発生した場合、クライアントはサーバーの機能を回復する責任を持てないことです。それは単にサービスを無効にするか、バックアップサーバーに切り替えることによって、意味のある回答が無いことに反応するだけです。こうして分離化を達成し、クライアントとサービスインスタンスの間に隔壁を挿入します。しかし、では誰がHTTPサーバーを再スタートさせ、RESTfulインターフェースをオンラインに戻す責任を持つのでしょうか?

回復力のあるアプリケーションでは、各コンポーネントは、その障害に対処する責任を持つ監督を持たなければなりません。2つのコンポーネント間のこの関係を親子関係として考えることもできます。子供が転んだら、親に言うか親が常に子供を監視してそれを検知し、親は進むべき道を決め、結局子供のコンポーネントの機能を回復するか自分自身の親に障害をエスカレートします。このアプローチの重要な特徴は、呼ばれた側の障害に対処するのはもはや呼び出し側の責任ではないことです。自販機の例では、お客以外の誰かがそれを直すかサポートを求める責任があります。障害処理のパス(子-親)とビジネスコミュニケーションパスのパスを分離したことがソフトウェア設計への恐らくアクターモデルの最も重要な貢献です。

あらゆる子供に親が必要なので、全コンポーネントが監督の階層を形成します。大組織の管理構造に似てなくもないです。そして階層の最上位に、全ての労力を象徴するもの、アプリケーションを一つに結び上げるコンポーネント、その障害がアプリケーションが全体として故障したことを意味するものです。障害が更にエスカレートすると、そのための回復行為がもっと破壊的になり、アプリケーションのより広い部分に影響を与えます。このことから2つの結論を出ます。

  • 故障の可能性の高い操作は、回復ができるだけ速く、簡単であるために、階層の低い所で実行されるべきである。
  • 重要なオブジェクトの状態は、既に述べた回復行為からそれを守るために出来るだけ高い位置に保持されるべきである。回復行為はしばしば、コンポーネントを再稼働するために、それを既知の良い状態に戻すが、これまで累積した全状態を失う。

2番目のパターンは、 Error Kernelと呼ばれ、明らかな目的は、その回復が難しく、高価である、アプリケーションの中核をできるだけ小さく、単純に保つことです。そうすれば故障は大幅に避ける事ができます。

典型的なエンタープライズアーキテクチャはこのような均一な監督階層ではありません。典型的なHTTPサーバーは、おそらくネットワーク監視ツールで監視され、自動スクリプトがそれを再起動するか、またはオペレータが手動でリカバリを実行するように警告されます。Akkaのアクターが完全に均一な方法で、必須である親の監督を説明した形に実装し、その階層はアクターがずっと上まで並ぶように構成されています。(まあ、明らかに、親 が"本物"ではないトップのアクターがいなければなりませんが、それは興味深いとはいえ技術的詳細です。)すべてのアクターは監督戦略を定義し、故障した子供のアクターを再開させるか再起動(つまり初期状態にリセット)するか、または完全に停止するか決定します。もし他に何も助けないなら、故障はエスカレートされます。すなわち監督自身が故障して、自身の親に問題を任せることになります。

疎結合オブジェクトは、分散することができる

ここまでの話を振り返ってみると、私はお互いにメッセージを送信することによって通信のみを行い、直接お互いのプライベートな内部状態にアクセスしないコンポーネントを導入しました。通信は非同期的に発生し、呼び出し側がメッセージを送信したときと呼ばれる側がそれを処理するときの間にかかるある程度の時間を許します。これらの特性のお陰で、これらのコンポーネントの動作を完全に互いに独立に、実行するのが可能になり、多分1つのスレッド上で一つづつ実行される-入れ子になったコールスタックがない場合、同期メソッドの呼び出し中に通常起きる-しかし、異なるスレッド上で並列に同様に起きる可能性があります。これらは単一サーバーのコア上で、あるいは別のネットワークホスト上に順番にスケジュールされるかもしれません。後者は、コンポーネント間で可変な状態を共有しないことで可能になり、それによって共有メモリのある環境で実行する必要がなくなります。

ちょっとこういうことを考えてみましょう:回復力のあるソフトウェア設計の欲求から始まって、私たちは、複数のネットワークノード間で配布するのに良く適した、自分自身を貸すオブジェクトを手に入れた。反対側から見て、システムの耐障害性は、冗長なハードウェアを要求します。なぜなら最も信頼性の高いサーバでも最終的に故障します。したがって、アプリケーションの分散は、ノード障害を物ともせず回復力を達成するためにも必要とされます。

回復力のもう一つの驚くべき結果は、それが監督関連の通信のために並行性の導入を必要としますが、同時に私達にそれを管理するためのツールを提供することです。安全に通信するオブジェクトは、自分のプライベートな状態を隠し、自然な境界を提示し、その中ではすべての処理は順次的で一貫しており-メッセージは1つ1つ-そして並行プログラムにおける同期を管理するための恐ろしいユーティリティプログラムは、あなた自身のコードのレベルでは全く要りません。この素晴らしい特性と引き換えに、Akkaのコアの開発は、これまでAkkaチームに非常に喜ばしい課題を提供してきました。

アクターの配置はAkkaにおいては完全に透過的で、なのでそれは展開の選択しだいです。すなわち、アクターを使ってアプリケーションを書き、それから設定によって協働しているネットワークノード上に監督階層の部品をデプロイするか、同じスレッドプール上に実行用のいくらかのアクターをまとめます。アクターは、ActorRefを使用して相互にメッセージを送信し、不変なハンドルは共有でき、自由に渡し回す事ができ、指定されたターゲットアクターが同じJVM内にいようが、別のネットワークホスト上にいようが関係なく、同じメッセージングセマンティクスを公開します。

分散した監督にはクラスタのサポートが必要

子アクターと親の間の障害関連の通信も同様にメッセージを介して行われるが、親が異なるネットワークノードにいて、その子が助けを必要としている時に、誰かがまさにその時ネットワークケーブル中を旅行していたらどうなるのか?あなたの直感は、おそらくケーブルが戻るまで障害メッセージはバッファされ、その後再送信される必要があり、同じことが親の応答にも当てはまる、というものだろう。これは、実際に起きることに非常に近いですが、実際はもう少し複雑です。なぜならAkkaは、親のマシンが二度と到達可能にならない場合(例えばその電源がついに直らないために)でもあなたのアプリケーションを動かし続ける必要があります。そのような場合、親のマシン上の全アクターは、停止したと考えられ、この場合は故障した子アクターも停止しなければならない(そして孫なども)、ということになります。結局あらゆるアクターは、親なしではいられません。複雑な部分は、他のネットワーク・ノードが現在子供のノードから到達可能できない、そのマシンと協働しているかもしれない。そしてこれらの他のノードはまだ親のマシンと通信することができる場合があります(なぜならネットワークケーブルは、結局故障します)。そのマシン上のすべてのアクターが停止したと宣言しなければならないかどうかの決定は、協働中の全ノード間で一貫して下す必要があります。

これがAkkaのクラスタサポートが登場する場面です。それは、すべての協働しているクラスタノードの一貫したメンバーシップ・リストを管理し、それを中央のマスタノードなしで行います。その代わりに、Dynamoの考え方に基づいて構築されており、ゴシッププロトコルを使って共有のメンバーシップ情報をばらまきます。各ノードは、ハートビートメッセージを使用して幾つかの仲間の生死を監視し、心拍停止が設定可能な閾値を超えると、音信不通ノードの到達不能についての知らせが他のノードに流されます。一度到達不能ノードが"ダウン"と印されることでクラスタから削除される(自動的に/プログラムで、または手動で)と、残りの全ノードは削除されたノード上のすべてのアクターは停止したと考え、その各アクター毎に、それらの両親に知らせ、再帰的にその子アクターを停止します。

親と子の間にある監督リンクは、クラスタのサービスが唯一の受益者ではない:あらゆるアクターは、他の役者が停止されたときにメッセージを受信するために、クラスタ内の他の役者のライフサイクルを監視できます。これは、終末期の手順の間にこれらのメッセージを送信するターゲットアクターによるわずかな場合に機能しますが、クラスタのお陰で、それはまた、そのノードがクラッシュするか到達不能になったために、ターゲットアクターが実際にそのような行動を取れない時に、機能します。

結論

回復力は、アプリケーションを疎結合のコンポーネントに分割することにより、そして故障をプログラミングモデルの基本的な部分として受け入れることによって達成され、また故障は監督することで明示的に表明し、対処しなければならない。これが、アクターモデル—特にAkkaに実装されているような必須の親による監督—は、回復力のあるアプリケーションを開発するための主要なツールであると、 私は強く信じている理由です。もう一つの利点は、基本的な抽象化が分散によく適しているので、アクターベースのアプリケーションは当然水平方向にも垂直方向にもスケーリングするということです。もしあなたがこれらのトピックについてもっと詳細に、特に技術的な詳細について、そして、どのようにアクター・ベースのアプリケーション・コンポーネントを書き始めるかを読みたい場合は、オンライン ドキュメントを参照してください。

著者について

高エネルギー素粒子物理学の博士号を獲得した後、宇宙事業でシステムエンジニアとして働いている間に、RolandはAkkaと関わりました。彼は2010年にオープンソースプロジェクトに貢献し始めて、2011年以来Typesafeで採用され、2012年11月以来、Akkaチームを率いています。

この記事に星をつける

おすすめ度
スタイル

BT