こんにちは、サイオステクノロジー技術部 武井です。今回はマイクロソフトが提供しているクラウドデザインパターンの1つである「Scheduler Agent Supervisorパターン」をわかりやすく紐解き、さらにAzureのテクノロジーを使って、Scheduler Agent Supervisorパターンを実装してみました。少々長くなりますが、最後までお付き合い頂けますと幸にございます。
クラウドデザインパターンとは?
マイクロソフトが提供している、クラウド上でアプリケーションを作成するためのベストプラクティスを集めたものです。
https://docs.microsoft.com/ja-jp/azure/architecture/patterns/
これらのデザインパターンは、クラウドの特徴を十分活かしている一方で、クラウド環境固有のデメリットをカバーするという、非常に秀逸なものとなっております。実装例も豊富(C#がメインなので、Javaもほしいなぁ・・・)で、すぐに実践適用可能です。
PDFでも同内容のものが提供されており、300ページ超の大作ですが、読破すればクラウドネイティブアプリケーションエンジニアになれること間違いなしです。
どうしてクラウドデザインパターンが必要なのか?
本章では、クラウド特有のメリデリを列挙し、そのメリットを活かすため、もしくはそのデメリットをカバーするために、クラウドデザインパターンがどのように使われるのかをご説明致します。オンプレミスにはなかったクラウド固有の様々な課題を、クラウドデザインパターンは一刀両断します(๑•̀ㅂ•́)و✧
リモートコールを多用する
クラウドは、複数のAPIをネットーワーク越しにコールし、それらのAPIを連携して1つのサービスを提供する形態を取ることが多くあります。今までのアプリケーションに多く見られた、アプリケーション内の関数呼び出し(インメモリコール)と比較して、ネットワーク越し(場合によってはインターネット越し)にAPIをコールするため、以下の問題が生じます。
- ネットワーク輻輳によるAPI呼び出しの失敗
- APIの認証処理によって生じる設計実装の煩雑さ
- 障害発生時の切り分けの難しさ
これらの多くは、クラウドデザインパターンで解決が可能です。
例えば、「ネットワーク輻輳によるAPI呼び出しの失敗」については、「Retryパターン」による処理の再試行によって解決が可能です。または、障害が一時的なものではなく、災害などの要因により復旧に長時間を要する場合、RetryパターンによるAPI呼び出しの再試行が、逆にリソースを消費(大量のAPIの再試行による、大量のデータストアコネクションの占有など)する場合があります。こういった場合は、障害によって再試行が必要となっているAPIを「Circuit Breakerパターン」を用いて、システムより切り離すことが有効な対策となります。
また、「APIの認証処理によって生じる設計実装の煩雑さ」については、「Gateway Offloadingパターン」により、複数のAPIの認証処理(を始めとして、SSLターミネーション、ロギングなど)を1ヶ所に集約することにより、設計や運用保守をシンプルにすることが可能です。ちなみに、これはAPI Gatewayと呼ばれることもあります。API Gatewayの詳細については、本ブログの記事「AzureのAPI Gateway(API Management)を用いてOpenID Connect Providerより発行されたJWTを検証」をご覧頂けますと幸いです。そして、いいね!やシェアをしてくれますと、もっと幸いです。この記事にて紹介しているAzureサービス「API Management」はまさに、Gateway Offloadingパターンを具現化したものといえます。
トランザクション管理が難しい
クラウドは、先程も申し上げましたとおり、複数の外部APIをコールして、1つのサービスを構成することが多くあります。例えば3つのAPI(A、B、C)があり、A→B→Cの順番に呼び出すことで、1つのサービスを構成したとします。かなり月並みな事例とはなりますが、旅行予約サイトのユースケースを思い浮かべて頂けますと、イメージがしやすいと思います。
- API A:航空券の予約をする。
- API B:ホテルの予約をする。
- API C:レンタカーの予約をする。
上記のAPIは、提供元も呼び出し先もすべて異なると仮定します。
API A、API Bの呼び出しまでは成功しましたが、API Cの呼び出しに失敗した場合、それまでに行われた「航空券の予約」「ホテルの予約」をなかったことにしなければなりません。いわゆるロールバックです。従来型のモノリシックなアプリケーションであれば、これらの処理はデータベース内で単一トランザクションとして扱っていましたので、ロールバックも容易でした。しかしながら、昨今のシステム開発では、外部のサービスベンダーから提供されている機能をネットワーク越しにAPIを用いて利用し、それらの結果を結合して1つのサービスとすることが一般的です。
この場合、各APIはステートレスのため、お互いに共有するコンテキストがなく、データベース内の単一トランザクションをロールバックするように簡単にはできません。この場合、「Compensating Transactionパターン」が適用することができます。
多種多様なストレージがある
Azureを始めとするパブリッククラウドサービスには多種多様なストレージが提供されています。Azure を例にしてあげてみますと、テキスト データやバイナリ データなどの大量の非構造化オブジェクト データのストレージを提供するAzure Blob Storage、CIFSのインターフェースを持つAzure Files、メッセージキューを格納するAzure Queue Storageなど用途に応じて様々なストレージを使い分けることができます。
ところで、こんなユースケースを考えてみます。昨今、Angularに代表されるように、クライアントサイドでビューのみならず、ルーティングまで行うフレームワークが隆盛期と言えます。Angularについての詳細は、本ブログの記事「JavaScriptフレームワーク「Angular」による掲示板システム構築 」をご覧頂きまして、シェアなんてしてくれるといいかもです。
Angularを構成するファイルはJava Scriptを始めとする静的なコンテンツになります。静的なコンテンツを配置するために、Virtual Machineを構築しApacheをインストールとか、Azure Web Appsを設定して静的コンテンツをアップロードとか、ちょっと手間だと思いませんか?(´・ω・`)
そこで、クラウドデザインパターンには、「Static Content Hosting パターン」というものがあります。このパターンでは、Azure Blob Storageが持つ静的コンテンツホスティング機能を紹介しており、Virtual MachineやAzure Web Appsよりも遥かに簡単で安価に静的コンテンツをホストできる方法が記載されております。もう、使うしかない!!o(・`д・´。)
クラウドデザインパターン利用のすゝめ
このようなデザインパターンは昔からあり、古くはJavaのデザインパターン、AWSにもAzureと同様のデザインパターンがあります(AWSの方が先ですが)。このようなデザインパターンは積極活用すべきと思います。
システム設計に限らず、物事を始める際は、先人の知恵を活用するのが最も近道ですし、最も安定した品質が確保できます。「学ぶは真似ぶ」ともいいますし、何もない真っ白なキャンパスから良質な設計を生み出せる人は、三度の飯よりコーディングが好きな生粋のエンジニアとか、数々の修羅場鉄火場をくぐり抜けた手練とか、不眠不休で働き続けるショートスリーパーとか、仕事という麻薬に侵された天性のワーカーホリックとかなのかもしれませんが、こういう逸材は稀有な存在です。
そこでデザインパターンが大活躍です。デザインパターンをそのまま適用したり、または部分的に組み合わせる、一部デフォルメする等、その案件に合わせた形で適用していく、つまり、プロジェクトマネジメントでいうところのテーラリングを行い、最適化を図っていくということが必要です。
Scheduler Agent Supervisorパターンとは?
Scheduler Agent Supervisorパターンとは、クラウドデザインパターンの1つで、リモートサービス(例えばOffice365に代表されるようなクラウド上のサービス)へのアクセスの際に発生したエラーの回復性を高めるために利用されます。以下にマイクロソフトの公開資料があります。
https://docs.microsoft.com/ja-jp/azure/architecture/patterns/scheduler-agent-supervisor
クラウド上のアプリケーションを設計するための重要な指針の1つとして、
障害は必ず発生するという前提で設計する
というものがあります。
リモートサービスへのアクセスは、ネットワークの輻輳、リモートサービス側の過負荷、スロットリング(単位時間あたりの規定アクセス数超過によるアクセス制限)の発生、地震や火事などの災害によるデータセンターの業務停止など、様々な理由で失敗します。短期間で回復するものもあれば、復旧まで長時間を要するのものあります。短期間で回復するものはRetryパターンで対応可能ですが、容易に回復できない持続的なエラーが発生した場合は、以下のいずれかの対応が必要となります。(以下はあくまで一例であり、ユーザー要件によっては、以下以外の特殊な対応が必要となる場合があります)
- リモートサービスへ処理の再試行をする。
- システムを一貫性のある状態に保つために、リモートサービスへの変更を全てロールバックする。
- 管理者にメールで通知を行い、管理者が手動で回復する。
上記の処理のオーケストレーションを行うのが、Scheduler Agent Supervisorパターンです。以下がイメージ図となります。
※下記の数字は上図の数字に対応しております
(1) まず、Remote Service(例えばOffice365に代表されるようなクラウド上のサービス)へ登録する処理の内容(ユーザー名やパスワードなど)をState Storeに登録します。State Storeの実装は、データが永続化ができれば、ファイルでもリレーショナルデータベースでもNoSQLデータベースでもなんでも構いません。その他には以下の情報が登録されます。
- タスクの実行状態(未処理、処理中、成功、失敗)
- タスクの完了予定時間
- タスクの失敗回数
(2) Schedulerが定期的にState Storeをチェックし、実行状態が「未処理」のタスクを抽出し、Agentにタスクの実行を依頼します。このときState Storeを以下のように変更します。
- タスクの実行状態:処理中
- タスクの完了予定時間:現在時刻 + タスクの処理タイムアウト時間
(3) Schedulerからの依頼を受けて、AgentがRemote Serviceにアクセスし、処理を実行します。
(4) Agentは、Remote Serviceに対する処理の結果をSchedulerに返します。
(5) Schedulerは、Agentからの実行結果を受けて、State Storeを以下のように変更します。
- タスクの実行状態:成功 or 失敗
(6) Supervisorは、定期的にState Storeをチェックし、以下の処理を行います。
- タスクの失敗回数が規定回数以内で、タスクの実行状態が失敗、もしくは、タスクの予定完了時間を過ぎていたら、タスクの実行状態を未処理にする。(つまり再びAgentに処理をさせる)
- タスクの失敗回数が規定回数を超えていたら、リトライによる復旧は不能として、メールで通知し管理者が手動で修正、各サービスをロールバックするなど要件に応じて適切な処理を行う。
Azureによるテクノロジーマッピング
先の章で説明したScheduler Agent Supervisorパターンを、Azureによるテクノロジーでどのように実現するかを表したのが、以下の図になります。(ちなみに以下は一例であり、他のテクノロジーでも実現可能です)
Office365にユーザーを登録するというユースケースを想定しております。
State Storeは、AzureのMySQLマネージドサービスであるAzure Database for MySQLで実現します。
Schedulerは、定期的にState Storeをチェックするという役割なので、Azure FunctionsによるTimer Triggerで実現します。関数は以下の2つを用意します。
pushRequestMessage | タスクの失敗回数が規定回数以内で、タスクの実行状態が失敗、もしくは、タスクの予定完了時間を過ぎていたら、タスクの実行状態を未処理にする。 |
retrieveResponseMessage | ResponseQueueからメッセージを取得し、State Storeに実行結果を登録する。 |
SchedulerとAgent間の通信は、Azure Queue Storageによるメッセージ交換によって実現します。SchedulerからAgentへ処理をリクエストするキュー(RequestQueue)と、SchedulerがAgentから処理結果を受け取るキュー(ResponseQueue)をそれぞれ別々に用意します。
Agentは、Azure App Service Web Jobとしました。ここはRemote Serviceへの複雑なビジネスロジックが入ることを想定し、5分の実行時間制限のあるAzure Functionsではなく、アプリケーション開発の自由度が高く、実行時間制限のないAzure App Service Web Jobをチョイスしました。
Supervisorは、Schedulerと同様の理由でAzure Functionsをチョイスしました。関数は以下の2つを用意します。
retryTask | State Storeから実行状態が未処理のタスクを抽出して、RequestQueue(後述)にメッセージを登録する。 |
rollbackTask | タスクの失敗回数が規定回数を超えていたら、リトライによる復旧は不能として、メールで通知し管理者が手動で修正、各サービスをロールバックするなど要件に応じて適切な処理を行う。 |
処理の流れとデータの遷移
本章では、一連の処理の流れの中で、データがどのように遷移していくかをご説明します。
まず最初にState Storeのスキーマ構造を説明します。State Storeにはt_state_storeというテーブルがあり、以下のフィールドがあります。
task_id | タスクを一意に識別するID(Auto Increment) |
user_id | タスク実行対象のユーザーのID |
task_body | Remote Serviceに処理をするために必要な情報(Office365のUPNなど) |
locked_by | 処理を行うSchedulerのインスタンスID(Schedulerが複数ある場合を想定しているが、本ブログでは1つのみなので、固定で01を登録) |
complete_by | タスクが完了する予定時間 |
process_state | タスクの実行状態(00:未処理、01:処理中、02:成功、03:失敗) |
failure_count | Agentがタスクの処理に失敗した回数 |
まず、最初にタスクの情報が、State Storeに以下のように登録されます。Schedulerとして機能するAzure Functionsの関数pushRequestMessageが、テーブルt_state_storeのフィールドprocess_stateが00(未処理)、かつlocked_byがNULLのタスクを取得します。
タスク情報を取得したら、pushRequestMessageが、テーブルt_state_storeのフィールドprocess_stateを01(処理中)、フィールドlocked_byを01、complete_byを現在時刻の10分後にします。そして、RequestQueueに以下のJSONをpushします。
AgentがRequestQueueからJSONを取得して、Remote ServiceつまりOffice365にユーザーを登録、そして、ResponseQueueに以下のJSONを登録します。ここでは、成功したらSupervisorの出番がないので、Office365への登録が失敗したと仮定して、processStateを03にしています。
retrieveResponseMessageは1分ごとにState Storeをチェックし、ResponseQueueからメッセージを取得して、JSON内のprocessStateの値を、テーブルt_state_storeのフィールドprocess_stateに03(失敗)として登録する。
retryTaskは1分ごとにState Storeをチェックし、process_stateが03(失敗)のタスクを以下のように更新します。すると、以下のレコードをSchedulerが抽出して、もう一度Agentに処理を依頼します。つまりリトライです。
locked_by | NULL |
complete_by | NULL |
process_state | 00 |
failure_count | 2 |
そして、リトライを繰り返し、ついに3回失敗したとします。これはOffice365にただならぬ障害が発生したとSupervisorのrollbackTask関数は判断して、リトライを諦め、ロールバックするなり、管理者にメールで通知するなり適切な処理をします。
さぁ実践だ
本章では、Azure FunctionsやAzure Database for MySQLなどの設定方法を記載します。
Azure Database for MySQL
Azureポータルに移動して、「リソースの作成」をクリックし、虫眼鏡のあるテキストボックスに「mysql」を入力してエンターを押すと、「Azure Database for MySQL」が表示されるので、それをクリックします。
「作成」をクリックします。
「Server name」には任意の名称、「サブスクリプション」「Resource Group」にはその環境に適したもの、「Select source」には「Blank」、「Server admin login name」にはMySQLに接続するためのユーザー名、「Password」にはMySQLに接続するためのパスワード、「Location」は自分のいるところと一番近いところ、「Version」は「5.7」を選択して、「作成」をクリックします。
リソースグループに移動して、MySQLのアイコンをクリックします。
「Connection Security」をクリックして、「Allow access to Azure services」を「ON」にして、「Save」をクリックします。これで、Azureのサービスから接続が可能になります。
MySQL WorkbenchなどのMySQLクライアントからデータベースに接続します。SSL接続のみしか受け付けませんので、以下のURLから証明書を取得して、MySQLクライアントに設定して下さい。
https://www.digicert.com/CACerts/BaltimoreCyberTrustRoot.crt.pem
以下のDDLに従って、タスク情報を登録するテーブルを作成して下さい。
CREATE TABLE `t_state_store` ( `task_id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(128) NOT NULL, `task_body` text NOT NULL, `locked_by` varchar(64) DEFAULT NULL, `complete_by` datetime DEFAULT NULL, `process_state` char(2) NOT NULL COMMENT '00:Pending\n01:Processing\n02:Processed\n03:Error', `failure_count` int(11) NOT NULL, PRIMARY KEY (`task_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1
Azure Queue Storage
Azureポータルに移動して、「リソースの作成」をクリックし、虫眼鏡のあるテキストボックスに「azure queue storage」を入力してエンターを押すと、「ストレージアカウント」が表示されるので、それをクリックします。
「作成」をクリックします。
「リソースグループ」は他のリソースが所属するのと同じリソースグループ、「ストレージアカウント名」は任意の名称を入力して、後はそのままデフォルトのままで「確認および作成」をクリックします。
「作成」をクリックします。
「リソースに移動」をクリックします。
「キュー」をクリックします。
「キュー」をクリックして、「キュー名」に「o365-request」を入力して、「OK」をクリックします。
同様の手順で「o365-response」キューも作成します。
Azure Functions
Azureポータルに移動して、「リソースの作成」をクリックし、虫眼鏡のあるテキストボックスに「functions」を入力してエンターを押すと、「function App」が表示されるので、それをクリックします。
「作成」をクリックします。
「アプリ名」は任意の名称、「サブスリプション」「リソースグループ」は環境に応じたもの、「Storage」は「新規作成」をチェック、「場所」は自分が住んでいるところに一番近いところ、他は以下のように入力して、「作成」をクリックします。
しばらくしたらリソースグループに移動して、Azure Functionsのアイコンをクリックして下さい。
関数名をクリックし、「プラットフォーム機能」→「Function Appの設定」をクリックします。
「ランタイムバージョン」を「〜1」にします。2018年10月2日時点、「〜2」にしますと、今まで使えていた便利な機能が使えなくなってしまいますので、「〜1」にします。
これから関数を作成します。この手順はこの先4回ほど繰り返すので、よく覚えておいて下さい。「関数」のとなりにある「+」をクリックします。
「カスタム関数」をクリックします。
1分ごとに起動する関数を作成するので、「Timer Trigger」をクリックします。
「名前」に「pushRequestMessage」、スケジュールに「0 * * * * *」(1分ごとに動かす設定)を入力して、「作成」をクリックします。
同様の手順で以下の名前の関数を作成して下さい。
- retryTask
- rollbackTask
retrieveResponseMessageという名前の関数を作成します。これだけ、他と違ってTimer Triggerではなく、先程作成したo365-responseキューにメッセージが登録されたときに起動される関数なので、Queue Triggerで作成します。
「カスタム関数」をクリックします。
「Queue trigger」をクリックします。
「名前」に「retrieveResponseMessage」と入力します。「ストレージアカウント接続」の「新規」をクリックして、o365-responseのキューを作成したストレージアカウントを選択します。そして「キュー名」のところに「o365-response」と入力し、「作成」をクリックします。
次にMySQLにSSL接続するために必要なルート証明書をアップロードします。「プラットフォーム機能」をクリックします。
「高度なツール」をクリックします。
「CMD」をクリックします。
コンソールに以下のように入力します。
# mkdir D:\home\site\certs # cd certs
以下で取得した証明書をmysqlcert.pmというファイル名で保存します。
https://www.digicert.com/CACerts/BaltimoreCyberTrustRoot.crt.pem
下記の画面の部分に、mysqlcert.pmをドラッグ&ドロップします。
下図のように表示されればOKです。
次に環境変数を定義します。この環境変数は、Azure FunctionsからMySQLに接続するのに利用します。「プラットフォーム機能」→「アプリケーション設定」の順にクリックします。
しばらくスクロールすると、「アプリケーション設定」の項目が見えてきて、その下の方に「+ 新しい文字列の追加」ありますので、クリックします。
以下の環境変数を追加して、最後に「保存」をクリックします。
- MYSQL_CA:D:\home\site\certs\mysqlcert.pem
- MYSQL_DB:statestore
- MYSQL_HOST:MySQLのホスト名(以下の方法で取得)
- MYSQL_USER:MySQLのユーザー名(以下の方法で取得)
- MYSQL_SECRET:MySQLを作成したときに入力したパスワード
MySQLのホスト名及びユーザー名の取得方法は以下のとおりです。
リソースグループに移動して、MySQLのアイコンをクリックします。
「Overview」をクリックします。
「Server name」は「MYSQL_HOST」、「Server admin login name」は「MYSQL_SECRET」に相当します。
次にAzure FunctionsからMySQLに接続するためのライブラリをインストールします。「プラットフォーム機能」をクリックします。
「高度なツール」をクリックします。
「CMD」をクリックします。
コンソールに以下のように入力します。
# D:\home\site\wwwroot # npm install -save mysql2
次に、関数pushRequestMessageの設定を行います。「統合」をクリックします。
「+新しい出力」をクリックして、「Azure Queue Storage」を選択して「選択」をクリックします。
「ストレージアカウント接続」はキューを作成したストレージアカウントを選択、「キュー名」は「o365-request」を入力して、後はデフォルトのままで「保存」をクリックします。
関数名をクリックします。
上図のコードを入力する部分に、以下のコードを入力して、「保存」をクリックします。
次に、関数retrieveResponseMessageの設定を行います。関数名をクリックします。
上図のコードを入力する部分に、以下のコードを入力して、「保存」をクリックします。
次に、関数retrieveResponseMessageの設定を行います。関数名をクリックします。
上図のコードを入力する部分に、以下のコードを入力して、「保存」をクリックします。
次に、関数rollbackTaskの設定を行います。関数名をクリックします。
上図のコードを入力する部分に、以下のコードを入力して、「保存」をクリックします。
Azure App Service WebJobs
Agentの設定を行います。Agentのソースは以下のGitHubリポジトリにありますので、git cloneして下さい。
https://github.com/noriyukitakei/scheduleragentsupervisor-agent
AgentはJavaで記述されております。主要なソースコードのみ以下に解説致します。基本的な実行ファイルは以下に示すO365Agent.javaのみとなり、同パッケージにある他の2つのソースファイルは、JSONのマッピング用DTOなので説明を割愛させていただきます。
AgentのソースはMavenプロジェクトとなります。以下のコマンドでビルドして下さい。
# mvn clean package
targetディレクトリ以下に「・・・-jar-with-dependencies.jar」というファイルが出来上がります。。これをo365-agent.jarというファイル名に変更します。
次に以下のファイルを作成し、run.cmdという名前で保存します。
set PATH=%PATH%;%JAVA_HOME%/bin java -jar o365-agent.jar
run.cmdとo365-agent.jarをZIPでアーカイブします。これがWeb Jobにアップロードするアプリケーション本体になりますので、大切に保存しておきます。
さて、いよいよWeb Jobを作成します。Azureポータルを起動して、「リソースの作成」をクリックし、虫眼鏡のあるテキストボックスに「app service」を入力してエンターを押します。
「Web App」が表示されますので、クリックします。
「作成」をクリックします。
「アプリ名」を任意の名称、「サブスクリプション」は環境に応じたもの、「リソースグループ」は他のリソースが入っているリソースグループを指定、「OS」は「Windows」、他はデフォルトのままで、「作成」をクリックします。
しばらくして画面上部の鈴のアイコンをクリックして以下のような画面が表示されるので、「リソースに移動」をクリックします。
環境変数を設定します。キューに接続するための情報を定義します。「アプリケーション設定」をクリックします。
「+新しい文字列の追加」をクリックして、「アプリ設定名」に「STORAGE_KEY」、「値」にAzure Storage Queueの接続情報を入力して、「保存」をクリックします。
先程のAzure Storage Queueの接続情報の取得方法は以下になります。
Azure Storage Queueが登録されているリソースグループに移動して、以下のアイコンをクリックします。※名前はご自分でつけたものになりますので、下図と違うかもしれません。
「アクセスキー」をクリックします。
「key1」の「接続文字列」が、先程の「STORAGE_KEY」の値になります。
さて、App Serviceの画面に戻って、「Webジョブ」をクリックします。
「名前」にはジョブを識別する任意の名称、「ファイルのアップロード」には、run.cmdとo365-agent.jarをZIPでアーカイブしたものをアップロード、「種類」は「継続」、「スケール」は「単一インスタンス」にして、「OK」をクリックします
以上で設定は完了です(๑•̀ㅂ•́)و✧
最後に
クラウドデザインパターンいかがでしたでしょうか?先人の知恵は、うまく有効活用して、これからもどんどんステキな設計をしていきましょう。No デザパタ、No Life!!ステキなデザパタライフをお過ごしくださいませ。