RA-GRS を使用した高可用性アプリケーションの設計

クラウド ベースのインフラストラクチャでは、アプリケーションをホストするための可用性の高いプラットフォームが提供されます。 クラウド ベース アプリケーションの開発者は、このプラットフォームをどのように活用してユーザーに高可用性アプリケーションを届けるかを慎重に検討する必要があります。 この記事では、開発者が Azure Storage の読み取りアクセス geo 冗長ストレージ (RA-GRS) を使用してアプリケーションの可用性を高める方法に焦点を絞って説明します。

冗長性のオプションには、LRS (ローカル冗長ストレージ)、ZRS (ゾーン冗長ストレージ)、GRS (geo 冗長ストレージ)、RA-GRS (読み取りアクセス geo 冗長ストレージ) の 4 種類があります。 この記事では、GRS と RA-GRS について説明します。 GRS では、データの 3 つのコピーがプライマリ リージョン (ストレージ アカウントの設定時に選択したリージョン) に保持され、 さらに 3 つのコピーがセカンダリ リージョン (Azure によって指定されたリージョン) に非同期的に保持されます。 RA-GRS も GRS と同じですが、RA-GRS の場合はセカンダリ コピーに対する読み取りアクセスが可能です。 Azure Storage の冗長性オプションの詳細については、「Azure Storage のレプリケーション」を参照してください。 このレプリケーションに関する記事では、プライマリ リージョンとセカンダリ リージョンのペアも示されています。

この記事にはコード スニペットが含まれています。また、記事の最後には、ダウンロードして実行できる完全なサンプルへのリンクも記載されています。

RA-GRS の主な特長

RA-GRS ストレージの使用方法について説明する前に、その特長と動作について説明します。

  • Azure Storage は、プライマリ リージョンに格納したデータの読み取り専用コピーをセカンダリ リージョンに保持します。前述のように、セカンダリ リージョンの場所はストレージ サービスによって異なります。

  • セカンダリ リージョンの読み取り専用コピーは、プライマリ リージョンのデータと最終的に一致します (これを結果整合性といいます)。

  • BLOB、テーブル、キューについて、セカンダリ リージョンの "最後の同期時刻" の値を照会すれば、プライマリ リージョンからセカンダリ リージョンへのレプリケーションが最後に行われた日時がわかります (現時点では、Azure File Storage には RA-GRS 冗長性がないため非対応です)。

  • ストレージ クライアント ライブラリを使用して、プライマリ リージョンまたはセカンダリ リージョンのデータと対話することができます。 プライマリ リージョンに対する読み取り要求がタイムアウトした場合に、その要求をセカンダリ リージョンに自動的にリダイレクトすることもできます。

  • プライマリ リージョンのデータのアクセシビリティに影響する重大な問題が発生した場合は、Azure チームが geo フェールオーバーをトリガーすることがあります。このとき、プライマリ リージョンを指す DNS エントリがセカンダリ リージョンを指すよう変更されます。

  • geo フェールオーバーが行われると、Azure によって新しいセカンダリ リージョンの場所が選択され、その場所にデータがレプリケートされます。そして、セカンダリ DNS エントリがセカンダリ リージョンを指すよう変更されます。 セカンダリ エンドポイントは、ストレージ アカウントがレプリケートを完了するまで使用できなくなります。 詳細については、「Azure Storage の停止が発生した場合の対処方法」を参照してください。

RA-GRS を使用する場合のアプリケーション設計に関する考慮事項

この記事の主な目的は、プライマリ データ センターで重大な障害が発生しても (制限付きであっても) 機能し続けられるアプリケーションを設計する方法を示すことです。 アプリケーションが機能し続けるためには、一時的なエラーや長時間にわたる問題に対処できなければなりません。それには、問題が発生したときにセカンダリ リージョンからの読み取りに切り替え、プライマリ リージョンが再び使用可能になったときに元に戻します。

結果整合性データの使用

ここで提案するソリューションは、古くなっている可能性があるデータを呼び出し元のアプリケーションに返すことに問題がないことを前提としています。 セカンダリ リージョンのデータは常に整合性が保たれているわけではありません。プライマリ リージョンがアクセス不能になったとき、プライマリ リージョンに書き込まれていたデータがまだセカンダリ リージョンにレプリケートされていない可能性もあります。

たとえば、顧客が更新を送信した後、その更新がセカンダリ リージョンに反映される前に、プライマリ リージョンが停止する場合があります。 このときデータの読み取りを要求した顧客には、更新されたデータではなく古いデータが返されます。 こうしたケースが許容されるかどうかを判断し、許容される場合は顧客に通知する方法を決める必要があります。 セカンダリ リージョンのデータが最後に同期された時刻をチェックして、それが最新かどうかを確認する方法については、この記事の後半で説明します。

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

まれなケースですが、他のサービスは完全に機能している中で、1 つのサービスだけが使用できなくなる場合もあります。 個々のサービス (BLOB、キュー、テーブル) の再試行や読み取り専用モードを個別に処理することも、すべてのストレージ サービスの再試行をまとめて処理することもできます。

たとえば、アプリケーションでキューと BLOB を使用している場合は、サービスごとにそれぞれ別のコードで再試行可能なエラーを処理することができます。 この場合、BLOB サービスから再試行を受け取ったときに キュー サービスが機能していれば、アプリケーションで影響を受けるのは BLOB を処理する部分だけで済みます。 すべてのストレージ サービスの再試行をまとめて処理する場合、BLOB サービス への呼び出しから再試行可能なエラーが返されると、BLOB サービスとキュー サービスのどちらに対する要求にも影響が及びます。

最終的に、アプリケーションの複雑さによって取るべき手法は異なります。 プライマリ リージョンのいずれかのストレージ サービスで問題が検出されたときに、サービスごとにエラーを処理するのではなく、すべてのストレージ サービスに対する読み取り要求をセカンダリ リージョンにリダイレクトして、読み取り専用モードでアプリケーションを実行する方法もあります。

その他の考慮事項

この記事の残りの部分では、以下の考慮事項について説明します。

  • サーキット ブレーカー パターンを使用した読み取り要求の再試行の処理

  • 結果整合性データと最後の同期時刻

  • テスト

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

RA-GRS ストレージを使用するには、失敗した読み取り要求と更新要求の両方を処理できる必要があります (ここでの更新とは、挿入、更新、および削除を意味します)。 プライマリ データ センターに障害が発生した場合、読み取り要求はセカンダリ データ センターにリダイレクトできますが、更新要求はリダイレクトできません。これは、セカンダリ データ センターが読み取り専用だからです。 そのため、読み取り専用モードでアプリケーションを実行するには何らかの対処法が必要です。

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

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

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

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

読み取り専用モードで実行中に更新要求を処理する方法はたくさんあります。 ここですべてを網羅することはできませんが、一般的なパターンをいくつか示します。

  1. 現在更新を受け付けていないことをユーザーに通知する。 たとえば、連絡先管理システムの場合、顧客に連絡先情報へのアクセスのみを許可し、更新はできないようにします。

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

  3. 更新を別のリージョンのストレージ アカウント に書き込む。 プライマリ データ センターがオンラインに戻ったら、データの構造に応じて、これらの更新をプライマリ データにマージします。 たとえば、名前に日付/時刻のタイムスタンプを付けて個別にファイルを作成していれば、それらのファイルをプライマリ リージョンにコピーして戻すことができます。 この方法は、ログ記録や IoT データなどの一部のワークロードに使用できます。

再試行の処理

エラーが再試行可能かどうかを決めるのは、 ストレージ クライアント ライブラリです。 たとえば 404 エラー (リソースが見つかりません) は、再試行が成功する可能性が低いので再試行可能ではありません。 一方、500 エラーはサーバー エラーであり、単に一時的な問題である可能性があるので再試行可能です。 詳細については、.NET ストレージ クライアント ライブラリの ExponentialRetry クラスのオープン ソース コードを参照してください (ShouldRetry メソッドを検索)。

読み取り要求

読み取り要求は、プライマリ ストレージに問題がある場合にセカンダリ ストレージにリダイレクトすることができます。 ただし、上の「結果整合性データの使用」で説明したとおり、古くなっている可能性のあるデータを読み取ることがアプリケーションで許容されている必要があります。 ストレージ クライアント ライブラリを使用して RA-GRS のデータにアクセスする場合は、LocationMode プロパティの値を次のいずれかに設定することで読み取り要求の再試行の動作を指定できます。

  • PrimaryOnly (既定値)

  • PrimaryThenSecondary

  • SecondaryOnly

  • SecondaryThenPrimary

LocationModePrimaryThenSecondary に設定した場合、再試行可能なエラーによってプライマリ エンドポイントへの最初の読み取り要求が失敗すると、クライアントが自動的にセカンダリ エンドポイントへの読み取り要求を行います。 エラーがサーバーのタイムアウトの場合、クライアントはタイムアウトの期限が切れた後にサービスから再試行可能なエラーを受け取ります。

再試行可能なエラーへの対応を決める際に考慮すべきシナリオは、基本的に次の 2 つです。

  • 問題は単発的なもので、プライマリ エンドポイントに対する後続の要求では再試行可能なエラーは返されない。 たとえば、一時的なネットワーク エラーが発生した場合がこれに該当します。

    このような問題は頻度に起こることではないので、このシナリオでは LocationModePrimaryThenSecondary に設定しても著しいパフォーマンスの低下はありません。

  • プライマリ リージョンのストレージ サービスの少なくとも 1 つに問題があり、そのサービスに対する後続のすべての要求でしばらくの間再試行可能なエラーが返される可能性が高い。 たとえば、プライマリ リージョンが完全にアクセス不能になった場合がこれに該当します。

    このシナリオでは、すべての読み取り要求が最初にプライマリ エンドポイントを試し、タイムアウトになるまで待機してからセカンダリ エンドポイントに切り替えるため、パフォーマンスの低下が起こります。

これらのシナリオについては、LocationMode プロパティを SecondaryOnly に設定し、プライマリ エンドポイントで継続的な問題が起こっていることを確認したら、すべての読み取り要求を直接セカンダリ エンドポイントに送信する必要があります。 また、この時点でアプリケーションを読み取り専用モードに変更する必要もあります。 このアプローチはサーキット ブレーカー パターンと呼ばれます。

更新要求

サーキット ブレーカー パターンは更新要求にも適用されます。 ただし、更新要求はセカンダリ ストレージにはリダイレクトできません (セカンダリ ストレージは読み取り専用のため)。 そのため、更新要求については LocationMode プロパティを既定値の PrimaryOnly のままにしておく必要があります。 エラーに対処するには、更新要求にメトリック (10 回連続の失敗など) を適用し、そのしきい値が満たされたときに読み取り専用モードにアプリケーションを切り替えます。 更新モードに戻すときにも同じ方法を使用できます (下のサーキット ブレーカー パターンに関するセクションを参照してください)。

サーキット ブレーカー パターン

アプリケーションでサーキット ブレーカー パターンを使用すると、繰り返し失敗する可能性がある操作の再試行を防ぐことができます。 膨大な回数の再試行に時間を取られることなく、アプリケーションを実行し続けることができます。 また、問題が修正されたことを検出するので、その時点でアプリケーションが操作を再試行することができます。

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

プライマリ エンドポイントで継続的な問題が発生しているかどうかを判断するには、クライアントで再試行可能なエラーが発生する頻度を監視します。 セカンダリ エンドポイントに切り替えて読み取り専用モードで実行すべきタイミングは、アプリケーションによってさまざまです。必要に応じて適切なしきい値を設定してください。 たとえば、「一度も成功することなく 10 回連続で失敗したとき」や、 「2 分間に要求の 90% が失敗したとき」などが考えられます。

最初のシナリオの場合は、単に失敗の回数を数えて、最大値に達する前に成功すればカウントをゼロに戻します。 2 番目のシナリオの場合は、MemoryCache オブジェクト (.NET) を使用して実装するのも 1 つの方法です。 要求ごとに CacheItem をキャッシュに追加し、値を成功 (1) または失敗 (0) に設定して、有効期限を現在から 2 分後 (または任意の時間) に設定します。 エントリの有効期限に達すると、そのエントリは自動的に削除されます。 これにより、随時 2 分間の猶予が与えられます。 ストレージ サービスに要求を行うたびに、まず MemoryCache オブジェクト全体に対して Linq クエリを使用して成功率を計算します (値を合計して要求回数で除算)。 成功率がしきい値 (10% など) を下回ったときは、読み取り要求の LocationMode プロパティを SecondaryOnly に設定し、続行する前にアプリケーションを読み取り専用モードに切り替えます。

切り替えのタイミングを決めるエラーのしきい値は、アプリケーションのサービスによって異なります。しきい値を構成可能なパラメーターにすることも検討してください。 また、前述した試行可能なエラーの処理方法 (サービスごとに個別に処理するか、まとめて処理するか) もここで決定します。

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

エラーの頻度を監視する方法

プライマリ リージョンの再試行の頻度を監視する方法は主に 3 つあります。これで、セカンダリ リージョンに切り替えてアプリケーションを読み取り専用モードにするタイミングを判断します。

  • ストレージ要求に渡す OperationContext オブジェクトの Retrying イベントにハンドラーを追加します。この方法はこの記事で紹介しているほか、付属のサンプルでも使用されています。 これらのイベントはクライアントが要求を再試行するたびに呼び出されるので、プライマリ エンドポイントで再試行可能なエラーが発生した頻度を追跡できます。

    operationContext.Retrying += (sender, arguments) =>
    {
        // Retrying in the primary region
        if (arguments.Request.Host == primaryhostname)
            ...
    };
    
  • カスタム再試行ポリシーの Evaluate メソッドで、再試行が行われるたびにカスタム コードを実行することができます。 これは、再試行の発生を記録するだけでなく、再試行の動作を見直す機会にもなります。

    public RetryInfo Evaluate(RetryContext retryContext,
    OperationContext operationContext)
    {
        var statusCode = retryContext.LastRequestResult.HttpStatusCode;
        if (retryContext.CurrentRetryCount >= this.maximumAttempts
        || ((statusCode >= 300 && statusCode < 500 && statusCode != 408)
        || statusCode == 501 // Not Implemented
        || statusCode == 505 // Version Not Supported
            ))
        {
        // Do not retry
            return null;
        }
    
        // Monitor retries in the primary location
        ...
    
        // Determine RetryInterval and TargetLocation
        RetryInfo info =
            CreateRetryInfo(retryContext.CurrentRetryCount);
    
        return info;
    }
    
  • 3 番目のアプローチは、アプリケーションにカスタム監視コンポーネントを実装し、プライマリ ストレージ エンドポイントをダミーの読み取り要求 (小さな BLOB の読み取りなど) で継続的に ping し、その状態を確認する方法です。 この方法はリソースを消費しますが、それほど多くはありません。 設定したしきい値に達するような問題が見つかったら、SecondaryOnly と読み取り専用モードへの切り替えを実施します。

切り替え後しばらくすると、プライマリ エンドポイントに戻して更新を許可することが必要になる場合もあります。 上で説明した最初の 2 つの方法のいずれかを使用している場合は、指定した時間が過ぎた後、または指定した回数だけ操作が実行された後に、プライマリ エンドポイントに戻して更新モードを有効にします。 そしてまた再試行ロジックに任せるだけで、他には何もする必要はありません。 問題が解決している場合、アプリケーションはそのままプライマリ エンドポイントを使用して更新を許可します。 問題が解決していない場合は、設定した条件を満たせなかったときに、もう一度セカンダリ エンドポイントと読み取り専用モードに戻ります。

3 番目のシナリオを使用している場合は、プライマリ ストレージ エンドポイントに対する ping が再び成功したら、PrimaryOnly への切り替えをトリガーして、引き続き更新を許可することができます。

結果整合性データの処理

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

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

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

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

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

テスト

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

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

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

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

アプリケーションを読み取り専用モードに切り替えるためのしきい値を構成可能にしている場合は、運用環境以外のトランザクション ボリュームを使って動作をテストしやすくなります。

次のステップ