Priority Queue パターン

Azure Service Bus

サービスに送信される要求に優先順位を設定し、優先順位の高い要求から順番に受信および処理されるようにします。 このパターンは、個々のクライアントにそれぞれ異なるサービス レベルを保証するアプリケーションにおいて有用です。

コンテキストと問題

アプリケーションは、特定のタスクを他のサービスに委任して、バック グラウンド処理の実行や他のアプリケーションまたはサービスとの統合などを行うことができます。 クラウドでは通常、タスクをバック グラウンド処理に委任するためにメッセージ キューを使用しています。 多くの場合に、サービスが要求を受信する順番は重要ではありません。 しかし、場合によっては、特定の要求を優先する必要があります。 そのような要求は、アプリケーションによって先に送信された優先順位の低い要求よりも早く処理する必要があります。

解決策

キューは一般に先入れ先出し (FIFO) 構造になっているため、コンシューマーは通常、キューにポストされた順にメッセージを受信することになります。 ただし、一部のメッセージ キューでは、優先順位メッセージングをサポートします。 メッセージをポストするアプリケーションは、優先順位を割り当てることができます。 キュー内のメッセージは自動的に並べ替えられ、優先順位の高いものが優先順位の低いメッセージより先に受信されます。 この図は、このプロセスを示したものです。

メッセージの優先順位付けをサポートするキュー メカニズムを示す図。

注意

ほとんどのメッセージ キューの実装では、複数のコンシューマーがサポートされます。 (「競合コンシューマー パターン」を参照してください。) コンシューマー プロセスの数は、需要に基づいてスケールアップおよびスケールダウンできます。

優先順位に基づくメッセージ キューをサポートしないシステムでは、代替ソリューションとして、優先順位ごとに個別のキューを維持します。 メッセージを適切なキューに送信するのはアプリケーションの役割です。 各キューはそれぞれ異なるコンシューマー プールを備えることができます。 優先順位の高いキューほどコンシューマー プールのサイズは大きく、より高速のハードウェアで実行されます。 この図は、優先順位ごとに個別のメッセージ キューを使用する様子を示しています。

優先順位ごとに個別のメッセージ キューを使用する様子を示す図。

この方法の変形型として、コンシューマー プールを 1 つだけ実装し、まず優先順位の高いキューにメッセージがないか確認し、その後で優先順位の低いキューからメッセージの取得を開始するという方法があります。 単一のコンシューマー プールを使用して処理するソリューション (さまざまな優先順位のメッセージをサポートする単一キューを使用する場合、または複数のキューを使用しそれぞれが単一の優先順位を持つメッセージを処理する場合) と、複数のキューを使用しキューごとに別々のプールを備えるソリューションとの間にはセマンティックの差異があります。

単一プール アプローチでは、常に優先順位の高い順にメッセージが受信され処理されます。 理論上、優先順位が低いメッセージは、繰り返し置き換えられ、いつまでたっても処理されない可能性があります。 複数プール アプローチでは、優先順位の低いメッセージも、優先順位の高いメッセージほど早くはありませんが (プールのサイズの相対関係と、利用可能なリソースに応じて)、必ず処理されます。

優先順位キュー メカニズムを使用すると、次の利点が得られます。

  • 異なる顧客グループに異なるレベルのサービスを提供するなど、アプリケーションで、可用性またはパフォーマンスの優先順位付けを必要とするビジネス要件を満たすことができます。

  • 運用コストを最小限に抑えるのに役立ちます。 単一キュー アプローチを使用する場合、必要に応じてコンシューマーの数を減らすことができます。 優先順位の高いメッセージはやはり最初に処理されます (遅くなる可能性はありますが)。しかし、優先順位の低いメッセージは待ち時間がさらに長くなる場合があります。 キューごとに別々のコンシューマー プールを使用する複数メッセージ キュー アプローチを実装する場合は、優先順位の低いキューのコンシューマー プールを減らすことができます。 優先順位が非常に低い一部のキューに対しては、それらのキューでメッセージをリッスンするすべてのコンシューマーを停止して処理を一時停止させることもできます。

  • 複数メッセージ キュー アプローチでは、処理要件に基づいてメッセージを分割することにより、アプリケーションのパフォーマンスとスケーラビリティを最大限に高めることができます。 たとえば、重要なタスクを優先させることで、すぐに実行される受信側でそれらを処理できるようにし、重要度の低いバックグラウンド タスクは、比較的ビジーでない時間帯に実行するようスケジュールされた受信側で処理できます。

考慮事項

このパターンの実装方法を決めるときには、以下の点に注意してください。

  • ソリューションのコンテキストで、優先順位を定義します。 たとえば、"優先順位の高い" メッセージは、10 秒以内に処理する必要があるメッセージとして定義できます。 優先順位の高い項目を処理するための要件、および条件を満たすために割り当てる必要があるリソースを明らかにします。

  • 優先順位の高い項目から順番に処理していく必要があるかどうかを決定します。 単一のコンシューマー プールでメッセージを処理する場合、より優先順位の高いメッセージがキューに入ったら、優先順位の低いメッセージを処理しているタスクを置き換えて中断できるメカニズムを用意しておく必要があります。

  • 複数キュー アプローチにおいて、キューごとに専用のコンシューマー プールを使用するのでなく、すべてのキューでリッスンする単一のコンシューマー プロセス プールを使用する場合、コンシューマーは優先順位の低いキューからのメッセージよりも優先順位の高いキューからのメッセージを必ず先に処理するアルゴリズムを適用する必要があります。

  • 優先順位の高いおよび低いそれぞれのキューで処理速度を監視し、これらのキューにあるメッセージが期待どおりのレートで処理されるようにします。

  • 優先順位の低いメッセージが確実に処理されるようにする必要がある場合は、複数のコンシューマー プールを使用する複数メッセージ キュー アプローチを実装します。 または、メッセージの優先順位設定をサポートするキューでは、キューに置かれたメッセージの優先順位をその経過時間に応じて動的に上げることができます。 ただし、このアプローチは、この機能を提供するメッセージ キューによって異なります。

  • メッセージの優先順位に基づいて個別のキューを使用する方法は、明確に定義されたいくつかの優先順位を含むシステムの場合にお勧めします。

  • システムでは、メッセージの優先順位を論理的に判断できます。 たとえば、優先順位の高いおよび低いメッセージをそれぞれ明示的に指定するのではなく、メッセージを "支払いのある顧客" または "支払いのない顧客" として指定できます。それによってシステムでは、支払いのある顧客からのメッセージの処理に、より多くのリソースを割り当てることができます。

  • キューにメッセージが置かれているかどうかの確認に関連して財務および処理費用が発生する場合があります。 たとえば、商用システムの中にはメッセージのポストまたは検索が行われるたびに、またキューに対してメッセージの照会が行われるたびに少額の料金を請求するものがあります。 複数のキューをチェックする場合、このコストは増加します。

  • プールで処理するキューの長さに基づいてコンシューマー プールのサイズを動的に調整できます。 詳しくは、「自動スケール」のガイダンスをご覧ください。

このパターンを使用する状況

このパターンは、次のシナリオで役立ちます。

  • システムでは、優先順位が異なる複数のタスクを処理する必要があります。

  • 優先順位の異なるさまざまなユーザーまたはテナントに対応する必要があります。

Azure では、並べ替えによるメッセージの自動的な優先順位付けをネイティブでサポートするキュー メカニズムは提供していません。 ただし、Azure Service Bus トピック、メッセージ フィルター機能を備えたキュー メカニズムをサポートする Service Bus サブスクリプション、および Azure を大部分の優先順位キュー実装に適したものにする柔軟性の高い一連の機能がサポートされています。

Azure ソリューションでは、アプリケーションがキューにポストする場合と同様にメッセージをポストできる Service Bus トピックを実装できます。 メッセージには、アプリケーションで定義されたカスタム プロパティの形式でメタデータを含めることができます。 Service Bus サブスクリプションはトピックに関連付けることができ、サブスクリプションでは、メッセージをそのプロパティに基づいてフィルター処理できます。 アプリケーションがトピックにメッセージを送信すると、メッセージは適切なサブスクリプションに送信され、そこでコンシューマーがそれを読み取ることができます。 コンシューマー プロセスでは、メッセージ キューで使用されるものと同じセマンティクスを使用して、サブスクリプションからメッセージを取得できます。 (サブスクリプションは論理キューです。) この図は、Service Bus のトピックとサブスクリプションを使用して優先順位キューを実装する方法を示しています。

Service Bus のトピックとサブスクリプションを使用して優先順位キューを実装する方法を示す図。

前の図で、アプリケーションは複数のメッセージを作成し、各メッセージ内に Priority と呼ばれるカスタム プロパティを割り当てています。 Priority の値は、High または Low です。 アプリケーションは、これらのメッセージをトピックにポストします。 トピックには 2 つの関連するサブスクリプションがあり、これらによって Priority プロパティに基づいてメッセージがフィルター処理されます。 一方のサブスクリプションでは、Priority プロパティが High に設定されたメッセージを受け入れます。 もう一方では、Priority プロパティが Low に設定されたメッセージを受け入れます。 コンシューマー プールは、各サブスクリプションからメッセージを読み取ります。 優先順位の高いサブスクリプションには、より大きなプールが用意されます。これらのコンシューマーは、優先順位の低いプールのコンピューターより多くのリソースが使用可能な、より強力なコンピューターで実行されます。

この例では、優先順位の高いメッセージと優先順位の低いメッセージの指定に関して特筆すべき点はありません。 これらは単に、各メッセージでプロパティとして指定されるラベルです。 これらは、特定のサブスクリプションにメッセージを送信するために使用されます。 追加の優先順位が必要な場合、それらの優先順位を処理するための、追加のサブスクリプションとコンシューマー プロセス プールを比較的簡単に作成できます。

GitHub の PriorityQueue ソリューションは、このアプローチに基づいています。 このソリューションには、PriorityQueueConsumerHigh および PriorityQueueConsumerLow という名前の Azure Function プロジェクトが含まれています。 これらの Azure Functions プロジェクトは、トリガーとバインドを使用して Service Bus と統合されます。 これらは ServiceBusTrigger で定義されているさまざまなサブスクリプションに接続し、受信メッセージに対応します。

public static class PriorityQueueConsumerHighFn
{
    [FunctionName("HighPriorityQueueConsumerFunction")]
    public static void Run(
      [ServiceBusTrigger("messages", "highPriority", Connection = "ServiceBusConnection")]string highPriorityMessage,
      ILogger log)
    {
        log.LogInformation($"C# ServiceBus topic trigger function processed message: {highPriorityMessage}");
    }
}

管理者は、Azure App Service の関数でスケールアウトできるインスタンスの数を構成できます。 これを行うには、Azure portal から [スケールアウト制限を適用する] オプションを構成し、各関数の最大スケールアウト制限を設定します。 通常は、PriorityQueueConsumerLow 関数より PriorityQueueConsumerHigh 関数のインスタンスの方が多く必要です。 この構成によって、優先順位の高いメッセージが優先順位の低いメッセージよりさらに早くキューから読み取られます。

別のプロジェクト PriorityQueueSender には、30 秒ごとに実行するように構成された、時間によってトリガーされる Azure 関数が含まれています。 この関数は出力バインディングを介して Service Bus と統合され、優先順位の低い、および高いメッセージのバッチを IAsyncCollector オブジェクトに送信します。 この関数は、PriorityQueueConsumerHigh および PriorityQueueConsumerLow 関数で使用されるサブスクリプションに関連するトピックにメッセージをポストするときに、ここに示すように、Priority カスタム プロパティを使用して優先順位を指定します。

public static class PriorityQueueSenderFn
{
    [FunctionName("PriorityQueueSenderFunction")]
    public static async Task Run(
        [TimerTrigger("0,30 * * * * *")] TimerInfo myTimer,
        [ServiceBus("messages", Connection = "ServiceBusConnection")] IAsyncCollector<ServiceBusMessage> collector)
    {
        for (int i = 0; i < 10; i++)
        {
            var messageId = Guid.NewGuid().ToString();
            var lpMessage = new ServiceBusMessage() { MessageId = messageId };
            lpMessage.ApplicationProperties["Priority"] = Priority.Low;
            lpMessage.Body = BinaryData.FromString($"Low priority message with Id: {messageId}");
            await collector.AddAsync(lpMessage);

            messageId = Guid.NewGuid().ToString();
            var hpMessage = new ServiceBusMessage() { MessageId = messageId };
            hpMessage.ApplicationProperties["Priority"] = Priority.High;
            hpMessage.Body = BinaryData.FromString($"High priority message with Id: {messageId}");
            await collector.AddAsync(hpMessage);
        }
    }
}

次のステップ

このパターンを実装する場合、次のリソースが役立つ可能性があります。

  • GitHub 上の、このパターンを示すサンプル

  • 非同期メッセージングの基本。 要求を処理するコンシューマー サービスは、要求をポストしたアプリケーションのインスタンスに応答を送信することが必要な場合があります。 この記事では、要求/応答メッセージングを実装する場合に使用できる方法に関する情報を提供します。

  • 自動スケール ガイダンス。 キューの長さに基づいてキューを処理するコンシューマー プロセスのプールのサイズは、スケールを変更できる場合があります。 この方法は、特に優先順位の高いメッセージを処理するプールのパフォーマンスを向上するのに役立ちます。

このパターンを実装する場合、次のパターンが役立つ可能性があります。

  • 競合コンシューマー パターン。 キューのスループットを高める場合、同一のキューでリッスンし、タスクを並列に処理する複数のコンシューマーを実装できます。 これらのコンシューマーはメッセージに対して競合し、各メッセージを処理できるのは 1 つのコンシューマーだけです。 この記事では、このアプローチを実装するメリットとデメリットの詳細について説明します。

  • スロットル パターン。 キューを使用してスロットルを実装することができます。 優先順位メッセージングを使用すると、重要なアプリケーション (または重要な顧客によって実行されているアプリケーション) からの要求を、重要度が低いアプリケーションからの要求より優先させることができます。