geo 冗長性を使用して高可用性アプリケーションを設計する

Azure Storage のようなクラウドベースのインフラストラクチャは、データとアプリケーションをホストするための可用性と持続性のあるプラットフォームを提供します。 クラウド ベース アプリケーションの開発者は、このプラットフォームをどのように活用してユーザーに最大の利点を提供するかを慎重に検討する必要があります。 Azure Storage では、リージョンの停止の期間でも高可用性を確保するために、geo 冗長オプションが用意されています。 geo 冗長レプリケーション用に構成されたストレージ アカウントは、プライマリ リージョンで同期的にレプリケートされた後、数百マイル離れたセカンダリ リージョンに非同期的にレプリケートされます。

Azure Storage には、geo 冗長レプリケーションのためのオプションがあります。geo 冗長ストレージ (GRS)geo ゾーン冗長ストレージ (GZRS) の 2 つです。 Azure Storage の geo 冗長オプションを使うには、ストレージ アカウントが読み取りアクセス geo 冗長ストレージ (RA-GRS) または読み取りアクセス geo ゾーン冗長ストレージ (RA-GZRS) 用に構成されていることを確認します。 そうでない場合は、ストレージ アカウントのレプリケーションの種類を変更する方法の詳細を確認してください。

この記事では、プライマリ リージョンに重大な停止が発生した場合でも、限定的な能力ながら、引き続き機能するアプリケーションを設計する方法について説明します。 プライマリ リージョンが使用できなくなった場合、アプリケーションはシームレスに切り替えることで、プライマリ リージョンが再び応答するまでセカンダリ リージョンに対して読み取り処理を行うことができます。

アプリケーション設計に関する考慮事項

プライマリ リージョンからの読み取りに支障をきたすような問題が発生した場合にセカンダリ リージョンから読み取ることで、一時的な障害や重大な停止に対処できるようにアプリケーションを設計することができます。 プライマリ リージョンが再び使用可能になったら、アプリケーションをプライマリ リージョンからの読み取りに戻すことができます。

RA-GRS か RA-GZRS を使って可用性と回復性のためにアプリケーションを設計する場合、次の重要な考慮事項に留意してください。

  • プライマリ リージョンに格納したデータの読み取り専用コピーは、セカンダリ リージョンに非同期でレプリケートされます。 この非同期レプリケーションは、セカンダリ リージョンの読み取り専用コピーがプライマリ リージョンのデータと最終的に一致することを意味します。 セカンダリ リージョンの場所はストレージ サービスによって決定されます。

  • Azure Storage クライアント ライブラリを使って、プライマリ リージョンのエンドポイントに対して読み取りと更新の要求を実行できます。 プライマリ リージョンが使用できない場合、読み取り要求をセカンダリ リージョンに自動的にリダイレクトできます。 また、プライマリ リージョンが使用できる場合でも、必要に応じて、読み取り要求をセカンダリ リージョンに直接送信するようにアプリを構成することもできます。

  • プライマリ リージョンが使用できなくなった場合、アカウントのフェールオーバーを開始できます。 セカンダリ リージョンにフェールオーバーすると、プライマリ リージョンを指す DNS エントリがセカンダリ リージョンを指すよう変更されます。 フェールオーバーが完了すると、GRS アカウントと RA-GRS アカウントの書き込みアクセスが復元されます。 詳細については、「ディザスター リカバリーとストレージ アカウントのフェールオーバー」を参照してください。

結果整合性データの操作

ここで提案するソリューションは、古くなっている可能性があるデータを呼び出し元のアプリケーションに返すことが許可されることを前提としています。 セカンダリ リージョンのデータの整合性をとるには時間がかかるため、セカンダリ リージョンに対する更新がレプリケートを完了する前にプライマリ リージョンがアクセス不能になる可能性があります。

たとえば、顧客が更新を正常に送信し、その更新がセカンダリ リージョンに反映される前にプライマリ リージョンに障害が発生したとしましょう。 顧客がデータの読み取りを要求すると、更新されたデータではなく古いデータがセカンダリ リージョンからが返されます。 アプリケーションを設計するときには、この動作が許容されるかどうかを判断する必要があります。 もしされるのであれば、ユーザーにどのように通知するかも考慮する必要があります。

この記事の後半では、結果整合性データの処理と、最終同期時刻プロパティを確認してプライマリ リージョンとセカンダリ リージョンのデータ間の不一致を評価する方法について説明します。

サービスの個別処理と一括処理

可能性は低いですが、他のサービスは完全に機能している中で、1 つのサービス (BLOB、キュー、テーブル、またはファイル) が使用できなくなることもあります。 各サービスの再試行を個別に処理することもできるし、すべてのストレージ サービスの再試行を一括でまとめて処理することもできます。

たとえば、アプリケーションでキューと BLOB を使用している場合は、サービスごとにそれぞれ別のコードで再試行可能なエラーを処理することができます。 そうすれば、BLOB サービスのエラーは、BLOB を処理するアプリケーションの部分だけに影響し、キューは通常どおり実行し続けることができます。 ただし、ストレージ サービスの再試行をすべて一括で処理することにした場合、BLOB サービスとキュー サービスどちらかのサービスが再試行可能なエラーを返すと、両方のサービスへの要求が影響を受けることになります。

最終的に、この決定はアプリケーションの複雑さによって異なります。 再試行の影響を抑えるために、サービスごとに障害を処理することもできます。 または、プライマリ リージョンのいずれかのストレージ サービスで問題があることを検出した場合、すべてのストレージ サービスの読み取り要求をセカンダリ リージョンにリダイレクトする方法もあります。

読み取り専用モードでのアプリケーションの実行

プライマリ リージョンでのサービス停止に有効に備えるには、アプリケーションが失敗した読み取り要求と更新要求の両方を処理できる必要があります。 プライマリ リージョンで障害が発生した場合、読み取り要求はセカンダリ リージョンにリダイレクトできます。 ただし、セカンダリ リージョンのレプリケートされたデータは読み取り専用であるため、更新要求をリダイレクトすることはできません。 そのため、読み取り専用モードでアプリケーションを実行できるように設計する必要があります。

たとえば、Azure Storage に更新要求を送信する前にチェックするフラグを設定できます。 更新要求が来たら、その要求をスキップして適切な応答をユーザーに返すことができます。 問題が解決するまで特定の機能を完全に無効にし、その機能が一時的に使用できないことをユーザーに通知する方法もあります。

各サービスのエラーを個別に処理する場合は、サービスごとに読み取り専用モードでアプリケーションを実行できるようにしておく必要もあります。 たとえば、サービスごとに読み取り専用フラグを設定できます。 そして、必要に応じて、コード内でそのフラグを有効または無効にします。

アプリケーションを読み取り専用モードで実行できるようにすることで、アプリケーションのメジャー アップグレード中に機能を制限することもできます。 アップグレードを行っている間は、アプリケーションを読み取り専用モードで実行するようトリガーし、セカンダリ データ センターに切り替えることで、プライマリ リージョンのデータが誰からもアクセスできないようにすることができます。

読み取り専用モードで実行中の更新の処理

読み取り専用モードで実行中に更新要求を処理する方法はたくさんあります。 このセクションでは、考慮すべきいくつかの一般的なパターンについて説明します。

  • ユーザーに応答して、更新要求が現在処理されていないことを通知できます。 たとえば、連絡先管理システムの場合、ユーザーに連絡先情報へのアクセスのみを許可し、更新はできないようにします。

  • 更新を別のリージョンにエンキューする。 この場合、保留中の更新要求を別のリージョンのキューに書き込み、プライマリ データセンターが再びオンラインになった後で、それらの要求を処理することになります。 このシナリオでは、更新要求が後で処理するためにキューに入れられたことをユーザーに知らせる必要があります。

  • 更新を別のリージョンのストレージ アカウント に書き込む。 プライマリ リージョンがオンラインに戻ったら、データの構造に応じて、それらの更新をプライマリ データにマージできます。 たとえば、名前に日付/タイムスタンプが含まれるファイルを個別に作成している場合、それらのファイルをプライマリ リージョンにコピーして戻すことができます。 このソリューションは、ログや IoT データなどのワークロードに適用できます。

再試行の処理

クラウドで実行されているサービスと通信するアプリケーションは、発生する可能性のある計画外のイベントや障害に対して鋭敏である必要があります。 これらの障害は一過性のものから持続性のものまであり、瞬間的な接続の喪失から自然災害による大規模な停止に至るまで、さまざまなものがあります。 クラウド アプリケーションの可用性を最大限に高め、アプリケーション全体の安定性を向上させるためには、適切な再試行処理を行うよう設計することが重要です。

読み取り要求

プライマリ リージョンが使用できなくなった場合、読み取り要求をセカンダリ ストレージにリダイレクトできます。 前述のように、最新でない可能性のあるデータの読み取りがアプリケーションで許容されている必要があります。 Azure Storage クライアント ライブラリには、再試行を処理するオプションと、読み取り要求をセカンダリ リージョンにリダイレクトするオプションが用意されています。

この例では、BLOB ストレージの再試行処理は BlobClientOptions クラスで構成され、これらの構成オプションを使って作成した BlobServiceClient オブジェクトに適用されます。 この構成はプライマリその次にセカンダリというアプローチで、プライマリ リージョンからの読み取り要求の再試行をセカンダリ リージョンにリダイレクトするものです。 この方法は、プライマリ リージョンでの障害が一時的なものであると予想される場合に最適です。

string accountName = "<YOURSTORAGEACCOUNTNAME>";
Uri primaryAccountUri = new Uri($"https://{accountName}.blob.core.windows.net/");
Uri secondaryAccountUri = new Uri($"https://{accountName}-secondary.blob.core.windows.net/");

// Provide the client configuration options for connecting to Azure Blob storage
BlobClientOptions blobClientOptions = new BlobClientOptions()
{
    Retry = {
        // The delay between retry attempts for a fixed approach or the delay
        // on which to base calculations for a backoff-based approach
        Delay = TimeSpan.FromSeconds(2),

        // The maximum number of retry attempts before giving up
        MaxRetries = 5,

        // The approach to use for calculating retry delays
        Mode = RetryMode.Exponential,

        // The maximum permissible delay between retry attempts
        MaxDelay = TimeSpan.FromSeconds(10)
    },

    // If the GeoRedundantSecondaryUri property is set, the secondary Uri will be used for 
    // GET or HEAD requests during retries.
    // If the status of the response from the secondary Uri is a 404, then subsequent retries
    // for the request will not use the secondary Uri again, as this indicates that the resource 
    // may not have propagated there yet.
    // Otherwise, subsequent retries will alternate back and forth between primary and secondary Uri.
    GeoRedundantSecondaryUri = secondaryAccountUri
};

// Create a BlobServiceClient object using the configuration options above
BlobServiceClient blobServiceClient = new BlobServiceClient(primaryAccountUri, new DefaultAzureCredential(), blobClientOptions);

プライマリ リージョンが長期間使用できない可能性があると判断した場合、すべての読み取り要求がセカンダリ リージョンを指すように構成することができます。 この構成は、セカンダリのみというアプローチです。 前に説明したように、この期間に更新要求を処理するための戦略と、読み取り要求だけが処理されていることをユーザーに通知する方法が必要になります。 この例では、セカンダリ リージョンのエンドポイントを使う BlobServiceClient のインスタンスを新規に作成します。

string accountName = "<YOURSTORAGEACCOUNTNAME>";
Uri primaryAccountUri = new Uri($"https://{accountName}.blob.core.windows.net/");
Uri secondaryAccountUri = new Uri($"https://{accountName}-secondary.blob.core.windows.net/");

// Create a BlobServiceClient object pointed at the secondary Uri
// Use blobServiceClientSecondary only when issuing read requests, as secondary storage is read-only
BlobServiceClient blobServiceClientSecondary = new BlobServiceClient(secondaryAccountUri, new DefaultAzureCredential(), blobClientOptions);

読み取り専用モードとセカンダリのみの要求に切り替えるタイミングを把握することは、サーキット ブレーカー パターンというアーキテクチャ設計パターンの一部です。これについては、後のセクションで説明します。

更新要求

更新要求は、読み取り専用である、セカンダリ ストレージにリダイレクトすることはできません。 前に説明したように、プライマリ リージョンが使用できないとき、アプリケーションは更新要求を処理できる必要があります。

サーキット ブレーカー パターンは更新要求にも適用されます。 更新要求のエラーを処理するために、コード内で 10 回の連続失敗といったしきい値を設定し、プライマリ リージョンへの要求に対する失敗の回数を追跡できます。 しきい値を超えたら、アプリケーションを読み取り専用モードに切り替えて、プライマリ リージョンへの更新要求が発行されないようにすることができます。

サーキット ブレーカー パターンの実装方法

回復に時間がかかる可能性のある障害の処理は、サーキット ブレーカー パターンというアーキテクチャ設計パターンの一部です。 このパターンを適切に実装することで、アプリケーションが失敗する可能性のある処理を繰り返し実行することを防ぎ、アプリケーションの安定性と回復性を向上させることができます。

サーキット ブレーカー パターンの 1 つの側面は、プライマリ エンドポイントで進行中の問題がいつ発生するかを特定することです。 この判断を行うために、クライアントが再試行可能なエラーを検出する頻度を監視できます。 シナリオはそれぞれ異なるため、セカンダリ エンドポイントに切り替えてアプリケーションを読み取り専用モードで実行することの判断に使う適切なしきい値を決める必要があります。

たとえば、プライマリ リージョンで連続 10 回の失敗があった場合に、切り替えの実行を決定できます。 これは、コード内で失敗の回数を保持することで追跡できます。 しきい値に達する前に成功した場合は、カウントを 0 に戻します。 カウントがしきい値に達した場合は、読み取り要求にセカンダリ リージョンを使うようにアプリケーションを切り替えます。

別の方法として、アプリケーションにカスタム監視コンポーネントを実装することもできます。 このコンポーネントは、プライマリ ストレージ エンドポイントに対して、簡単な読み取り要求 (小さな BLOB の読み取りなど) で継続的に ping を実行して、その正常性を判断できます。 この手法はリソースを消費しますが、それほど多くはありません。 設定したしきい値に達するような問題が見つかったら、セカンダリのみの読み取り要求と読み取り専用モードに切り替えます。 このシナリオでは、プライマリ ストレージ エンドポイントの ping が再び成功したら、プライマリ リージョンに切り替えて、引き続き更新を許可することができます。

切り替えのタイミングを決めるためのエラーのしきい値は、アプリケーション内のサービスによって異なるので、それらを構成可能なパラメーターにすることも検討してください。

もうひとつ考慮すべき点は、アプリケーションの複数のインスタンスの扱いと、各インスタンスで再試行可能なエラーが検出された場合の対処法です。 たとえば、同じアプリケーションをロードしている 20 個の VM を実行している場合、 各インスタンスを個別に処理するかどうかや、 1 つのインスタンスに問題が発生した場合、その 1 つのインスタンスの応答だけを制限するのか、 それとも、すべてのインスタンスに同じように応答させるのか、などを決める必要があります。 インスタンスを個別に処理する方が、インスタンス間で応答を調整するよりもずっと簡単ですが、その方法はアプリケーションのアーキテクチャに依存します。

結果整合性データの処理

geo 冗長ストレージは、プライマリ リージョンからセカンダリ リージョンにトランザクションをレプリケートすることによって機能します。 このレプリケーション プロセスにより、セカンダリ リージョンのデータの結果整合性が保証されます。 このことは、プライマリ リージョンにあるすべてのトランザクションが最終的にセカンダリ リージョンに現れることを意味しますが、現れるまでにタイム ラグが生じる可能性があることを意味します。 また、トランザクションがプライマリ リージョンで最初に適用されたのと同じ順序でセカンダリ リージョンに到着するという保証もありません。 トランザクションが順不同でセカンダリ リージョンに到着した場合、更新が追いつくまでは、セカンダリ リージョンのデータが不整合な状態であると見なすことができます

次の Azure テーブル ストレージの例は、ある従業員の詳細を更新して管理者ロールのメンバーにするときに起こりうる例を示しています。 この例では、従業員エンティティを更新し、さらに管理者ロール エンティティの合計管理者数を更新します。 セカンダリ リージョンで更新が順不同に適用される様子に着目してください。

Time トランザクション レプリケーション [最終同期時刻] 結果
T0 トランザクション A:
従業員エンティティを
プライマリに挿入する
トランザクション A はプライマリに挿入されていますが、
まだレプリケートされていません。
T1 トランザクション A が
セカンダリに
レプリケートされる
T1 トランザクション A がセカンダリにレプリケートされ、
最後の同期時刻が更新されます。
T2 トランザクション B:
更新
プライマリの
従業員エンティティ
T1 トランザクション B はプライマリに書き込まれていますが、
まだレプリケートされていません。
T3 トランザクション C:
更新
administrator
ロール エンティティの
更新
T1 トランザクション C はプライマリに書き込まれていますが、
まだレプリケートされていません。
T4 トランザクション C が
セカンダリに
レプリケートされる
T1 トランザクション C はセカンダリにレプリケートされています。
トランザクション B がレプリケートされていないため、
最後の同期時刻はまだ更新されていません。
T5 セカンダリからの
エンティティの読み取り
T1 トランザクション B がまだレプリケート
されていないので、従業員エンティティは
古い値になります。 トランザクション C が既にレプリケートされているため、
管理者ロール エンティティは
新しい値になります。 トランザクション B がレプリケートされていないので、
最後の同期時刻は
まだ更新されていません。 管理者ロール エンティティの日時が
最後の同期時刻よりも新しいことから、
このエンティティが不整合な状態である
ことがわかります。
T6 トランザクション B が
セカンダリに
レプリケートされる
T6 T6 – C までのすべてのトランザクションが
レプリケートされ、最後の同期時刻が
更新されます。

この例では、T5 でクライアントの読み取り先がセカンダリ リージョンに切り替わっているものとします。 クライアントは、この時点で管理者ロール エンティティを正常に読み取ることができますが、このエンティティに含まれる管理者数の値は、このときセカンダリ リージョンで管理者としてマークされている従業員エンティティの数とは一致しません。 クライアントは、情報に整合性がないというリスク付きで、この値をそのまま示すこともできますが、 更新が順不同で発生していることから管理者ロールが不整合の状態である可能性があると判断し、その事実をユーザーに通知することもできます。

ストレージ アカウントに不整合の可能性のあるデータがあるかどうかを判断するために、クライアントは最後の同期時刻プロパティの値を確認できます。 最後の同期時刻の値を見ると、セカンダリ リージョンのデータの整合性が取れていた最後の時刻と、その時点よりも前にサービスがすべてのトランザクションを適用した時刻がわかります。 上で示した例では、セカンダリ リージョンに従業員エンティティが挿入された後、最後の同期時刻が T1 に設定されています。 この値はしばらく T1 のままですが、セカンダリ リージョンの従業員エンティティが更新されると T6 に設定されます。 クライアントは T5 でエンティティを読み取ったときに最後の同期時刻を取得し、エンティティ上のタイムスタンプと比較することができます。 エンティティのタイムスタンプが最後の同期時刻よりも新しい場合、そのエンティティは不整合な状態である可能性があり、適切な対応を取ることができます。 このフィールドを使用するには、プライマリ リージョンへの最後の更新が完了した日時がわかっている必要があります。

最後の同期時刻のチェック方法については、「ストレージ アカウントの最後の同期時刻プロパティを確認する」を参照してください。

テスト

再試行可能なエラーが発生した場合にアプリケーションが予想どおりに動作するかどうかをテストすることが重要です。 たとえば、アプリケーションが問題を検出したときにセカンダリ リージョンに切り替わり、プライマリ リージョンが再び使用可能になったときに元に戻るかどうかをテストする必要があります。 この動作を適切にテストするには、再試行可能なエラーをシミュレートし、その発生頻度を制御する方法が必要です。

Fiddler を使って、スクリプトで HTTP 応答をインターセプトして変更することが 1 つの選択肢です。 このスクリプトは、プライマリ エンドポイントからの応答を識別し、HTTP 状態コードをストレージ クライアント ライブラリが再試行可能なエラーとして認識するものに変更します。 下のコード スニペットは、employeedata テーブルに対する読み取り要求への応答をインターセプトして 502 ステータスを返す、Fiddler スクリプトの簡単な例を示しています。

static function OnBeforeResponse(oSession: Session) {
    ...
    if ((oSession.hostname == "\[YOURSTORAGEACCOUNTNAME\].table.core.windows.net")
      && (oSession.PathAndQuery.StartsWith("/employeedata?$filter"))) {
        oSession.responseCode = 502;
    }
}

より広範な要求をインターセプトし、そのうちのいくつかの responseCode だけを変更するようこのサンプル コードを拡張すれば、より現実的なシナリオをシミュレートすることもできます。 Fiddler スクリプトのカスタマイズの詳細については、Fiddler のドキュメント「Modifying a Request or Response (要求または応答の変更)」を参照してください。

アプリケーションを読み取り専用に切り替えるための構成可能なしきい値を設定しておけば、非運用環境のトランザクション ボリュームを使って動作をテストすることが容易になります。


次のステップ

プライマリ エンドポイントとセカンダリ エンドポイント間の切り替え方法を示す完全なサンプルについては、「Azure Samples – Using the Circuit Breaker Pattern with RA-GRS storage (Azure サンプル - RA-GRS ストレージでのサーキット ブレーカー パターンの使用)」を参照してください。