非同期要求-応答パターンAsynchronous Request-Reply pattern

フロントエンド ホストからバックエンド処理を分離します。その場合バックエンド処理を非同期にする必要がありますが、引き続きフロントエンドには明確な応答が必要です。Decouple backend processing from a frontend host, where backend processing needs to be asynchronous, but the frontend still needs a clear response.

コンテキストと問題Context and problem

最新のアプリケーション開発では、クライアント アプリケーション (多くの場合、Web クライアント (ブラウザー) で実行されるコード) が、ビジネス ロジックと作成機能を提供するためにリモート API に依存することが通常となっています。In modern application development, it's normal for client applications — often code running in a web-client (browser) — to depend on remote APIs to provide business logic and compose functionality. これらの API は、アプリケーションに直接関連している場合もあれば、サード パーティによって提供される共有サービスである場合もあります。These APIs may be directly related to the application or may be shared services provided by a third party. 一般に、これらの API 呼び出しは HTTP(S) プロトコルを介して行われ、REST セマンティクスに従います。Commonly these API calls take place over the HTTP(S) protocol and follow REST semantics.

ほとんどの場合、クライアント アプリケーションの API は、100 ミリ秒以下の単位ですばやく応答するように設計されています。In most cases, APIs for a client application are designed to respond quickly, on the order of 100 ms or less. 次のような多くの要因によって、応答待機時間が影響を受ける可能性があります。Many factors can affect the response latency, including:

  • アプリケーションのホスト スタック。An application's hosting stack.
  • セキュリティ コンポーネント。Security components.
  • 呼び出し元とバックエンドの相対的な地理的位置。The relative geographic location of the caller and the backend.
  • ネットワーク インフラストラクチャ。Network infrastructure.
  • 現在の負荷。Current load.
  • 要求ペイロードのサイズ。The size of the request payload.
  • 処理キューの長さ。Processing queue length.
  • バックエンドが要求を処理する時間。The time for the backend to process the request.

これらのいずれかの要因によって、応答に待機時間が加わる可能性があります。Any of these factors can add latency to the response. 一部はバックエンドをスケール アウトすることで軽減できます。Some can be mitigated by scaling out the backend. ネットワーク インフラストラクチャなどの他の要因は、アプリケーション開発者がほとんど制御できません。Others, such as network infrastructure, are largely out of the control of the application developer. ほとんどの API では、同じ接続を介して応答が到着するまでに十分な速さで応答できます。Most APIs can respond quickly enough for responses to arrive back over the same connection. アプリケーション コードは、非ブロッキング方式で同期 API 呼び出しを行うことができます。これは、非同期処理の外観をしており、I/O バインド操作に推奨されます。Application code can make a synchronous API call in a non-blocking way, giving the appearance of asynchronous processing, which is recommended for I/O-bound operations.

ただし、シナリオによっては、バックエンドによって実行される作業が、数秒単位で長時間実行される場合や、数分または数時間実行されるバックグラウンド プロセスになる場合があります。In some scenarios, however, the work done by backend may be long-running, on the order of seconds, or might be a background process that is executed in minutes or even hours. その場合は、作業が完了するまで待機してから、要求に応答することはできません。In that case, it isn't feasible to wait for the work to complete before responding to the request. この状況は、すべての同期要求-応答パターンの潜在的な問題点です。This situation is a potential problem for any synchronous request-reply pattern.

一部のアーキテクチャでは、メッセージ ブローカーを使用して要求ステージと応答ステージを分離することによって、この問題を解決しています。Some architectures solve this problem by using a message broker to separate the request and response stages. この分離は、多くの場合、キュー ベースの負荷平準化パターンを使用することで実現されます。This separation is often achieved by use of the Queue-Based Load Leveling pattern. この分離により、クライアント プロセスとバックエンド API を個別にスケーリングできます。This separation can allow the client process and the backend API to scale independently. しかし、この分離により、クライアントで成功通知が必要になる場合に、この手順を非同期にする必要があるため、さらに複雑になります。But this separation also brings additional complexity when the client requires success notification, as this step needs to become asynchronous.

クライアント アプリケーションについて説明した同じ考慮事項の多くが、マイクロサービス アーキテクチャでの分散システムにおけるサーバー間の REST API 呼び出しなどにも適用されます。Many of the same considerations discussed for client applications also apply for server-to-server REST API calls in distributed systems — for example, in a microservices architecture.

解決策Solution

この問題の 1 つの解決策は、HTTP ポーリングを使用することです。One solution to this problem is to use HTTP polling. コールバック エンドポイントを提供したり、長時間実行する接続を使用したりするのは困難な場合があるため、クライアント側コードにはポーリングが役立ちます。Polling is useful to client-side code, as it can be hard to provide call-back endpoints or use long running connections. コールバックが可能な場合でも、必要な追加のライブラリとサービスによって、大幅に複雑さが加わることがあります。Even when callbacks are possible, the extra libraries and services that are required can sometimes add too much extra complexity.

  • クライアント アプリケーションは、API への同期呼び出しを行い、バックエンドで長時間実行する操作をトリガーします。The client application makes a synchronous call to the API, triggering a long-running operation on the backend.

  • API は、可能な限りすばやく同期的に応答します。The API responds synchronously as quickly as possible. それは、HTTP 202 (Accepted) 状態コードを返し、要求が処理のために受信されたことを確認します。It returns an HTTP 202 (Accepted) status code, acknowledging that the request has been received for processing.

    注意

    API では、長時間実行されるプロセスを開始する前に、要求と実行されるアクションの両方を検証する必要があります。The API should validate both the request and the action to be performed before starting the long running process. 要求が無効な場合、HTTP 400 (Bad Request) などのエラーコードですぐに応答します。If the request is invalid, reply immediately with an error code such as HTTP 400 (Bad Request).

  • 応答は、長時間実行される操作の結果を確認するために、クライアントがポーリングできるエンドポイントを指す場所参照を保持します。The response holds a location reference pointing to an endpoint that the client can poll to check for the result of the long running operation.

  • API では、メッセージ キューなどの別のコンポーネントに処理をオフロードします。The API offloads processing to another component, such as a message queue.

  • 作業がまだ保留中の間、状態エンドポイントでは HTTP 202 を返します。While the work is still pending, the status endpoint returns HTTP 202. 作業が完了したら、状態エンドポイントでは、完了を示すリソースを返すか、別のリソース URL にリダイレクトすることができます。Once the work is complete, the status endpoint can either return a resource that indicates completion, or redirect to another resource URL. たとえば、非同期操作によって新しいリソースが作成された場合、状態エンドポイントはそのリソースの URL にリダイレクトします。For example, if the asynchronous operation creates a new resource, the status endpoint would redirect to the URL for that resource.

次の図は、一般的なフローを示しています。The following diagram shows a typical flow:

非同期 HTTP 要求の要求と応答のフロー

  1. クライアントは要求を送信し、HTTP 202 (Accepted) 応答を受信します。The client sends a request and receives an HTTP 202 (Accepted) response.
  2. クライアントは、HTTP GET 要求を状態エンドポイントに送信します。The client sends an HTTP GET request to the status endpoint. 作業はまだ保留中であるため、この呼び出しも HTTP 202 を返します。The work is still pending, so this call also returns HTTP 202.
  3. ある時点で作業が完了すると、状態エンドポイントは、リソースにリダイレクトする 302 (Found) を返します。At some point, the work is complete and the status endpoint returns 302 (Found) redirecting to the resource.
  4. クライアントは、指定された URL にあるリソースをフェッチします。The client fetches the resource at the specified URL.

問題と注意事項Issues and considerations

  • HTTP 経由でこのパターンを実装できる多くの方法があり、すべてのアップストリーム サービスで同じセマンティクスを使用するとは限りません。There are a number of possible ways to implement this pattern over HTTP and not all upstream services have the same semantics. たとえば、リモート プロセスが終了していない場合、ほとんどのサービスでは GET メソッドから HTTP 202 応答を返しません。For example, most services won't return an HTTP 202 response back from a GET method when a remote process hasn't finished. 純粋な REST セマンティクスに従うと、それらは HTTP 404 (Not Found) を返す必要があります。Following pure REST semantics, they should return HTTP 404 (Not Found). この応答は、呼び出しの結果がまだ存在しないと見なす場合に意味があります。This response makes sense when you consider the result of the call isn't present yet.

  • HTTP 202 応答では、クライアントが応答をポーリングする場所と頻度を示す必要があります。An HTTP 202 response should indicate the location and frequency that the client should poll for the response. 次の追加のヘッダーが必要です。It should have the following additional headers:

    ヘッダーHeader 説明Description NotesNotes
    場所Location クライアントが応答状態をポーリングする必要がある URL。A URL the client should poll for a response status. この URL は、この場所でアクセス制御が必要な場合に適切なバレー キー パターンを持つ SAS トークンの可能性があります。This URL could be a SAS token with the Valet Key Pattern being appropriate if this location needs access control. バレー キー パターンは、応答ポーリングを別のバックエンドにオフロードする必要がある場合にも有効です。The valet key pattern is also valid when response polling needs offloading to another backend
    Retry-AfterRetry-After 処理が完了するタイミングの見積もりAn estimate of when processing will complete このヘッダーは、ポーリング クライアントが、再試行でバックエンドを過負荷にしないように設計されています。This header is designed to prevent polling clients from overwhelming the back-end with retries.
  • 使用する基になるサービスに応じて、応答ヘッダーまたはペイロードを操作するために、処理プロキシやファサードを使用する必要がある場合があります。You may need to use a processing proxy or facade to manipulate the response headers or payload depending on the underlying services used.

  • 状態エンドポイントで完了時にリダイレクトする場合、サポートする正確なセマンティクスに応じて、HTTP 302 または HTTP 303 のいずれかが適切なリターン コードになります。If the status endpoint redirects on completion, either HTTP 302 or HTTP 303 are appropriate return codes, depending on the exact semantics you support.

  • 処理が成功したら、Location ヘッダーで指定されたリソースは、200 (OK)、201 (Created)、204 (No Content) などの適切な HTTP 応答コードを返す必要があります。Upon successful processing, the resource specified by the Location header should return an appropriate HTTP response code such as 200 (OK), 201 (Created), or 204 (No Content).

  • 処理中にエラーが発生した場合、Location ヘッダーに記述されているリソース URL でエラーを保持し、そのリソースからクライアントに適切な応答コード (4xx コード) を返すことが理想的です。If an error occurs during processing, persist the error at the resource URL described in the Location header and ideally return an appropriate response code to the client from that resource (4xx code).

  • すべてのソリューションでこのパターンを同じ方法で実装するわけではなく、一部のサービスでは、追加または代替ヘッダーを含めます。Not all solutions will implement this pattern in the same way and some services will include additional or alternate headers. たとえば、Azure Resource Manager では、このパターンの変更されたバリエーションを使用します。For example, Azure Resource Manager uses a modified variant of this pattern. 詳細については、Azure Resource Manager の非同期操作に関するページを参照してください。For more information, see Azure Resource Manager Async Operations.

  • レガシ クライアントでは、このパターンをサポートしていない可能性があります。Legacy clients might not support this pattern. その場合は、元のクライアントからの非同期処理を隠すために、非同期 API にファサードを配置する必要がある場合があります。In that case, you might need to place a facade over the asynchronous API to hide the asynchronous processing from the original client. たとえば、このパターンをネイティブにサポートしている Azure Logic Apps を、非同期 API と同期呼び出しを行うクライアント間の統合レイヤーとして使用できます。For example, Azure Logic Apps supports this pattern natively can be used as an integration layer between an asynchronous API and a client that makes synchronous calls. webhook アクション パターンで長時間タスクを実行する」を参照してください。See Perform long-running tasks with the webhook action pattern.

  • 一部のシナリオでは、クライアントが長時間実行される要求をキャンセルする方法を提供する必要がある場合があります。In some scenarios, you might want to provide a way for clients to cancel a long-running request. その場合、バックエンド サービスで何らかの形式のキャンセル命令をサポートしている必要があります。In that case, the backend service must support some form of cancellation instruction.

このパターンを使用する状況When to use this pattern

このパターンは次の場合に使用します。Use this pattern for:

  • コールバック エンドポイントを提供するのが困難であったり、長時間実行接続を使用すると、著しく複雑さが増したりするブラウザー アプリケーションなどのクライアント側コード。Client-side code, such as browser applications, where it's difficult to provide call-back endpoints, or the use of long-running connections adds too much additional complexity.

  • クライアント側でのファイアウォールの制限のため、HTTP プロトコルのみが使用可能で、リターン サービスでコールバックを起動できないサービス呼び出し。Service calls where only the HTTP protocol is available and the return service can't fire callbacks because of firewall restrictions on the client-side.

  • Websocket や webhook などの最新のコールバックテクノロジをサポートしていないレガシ アーキテクチャと統合する必要があるサービス呼び出し。Service calls that need to be integrated with legacy architectures that don't support modern callback technologies such as WebSockets or webhooks.

このパターンは、次の場合に適していない可能性があります。This pattern might not be suitable when:

  • Azure Event Grid など、非同期通知用に構築されたサービスを代わりに使用できます。You can use a service built for asynchronous notifications instead, such as Azure Event Grid.
  • 応答は、クライアントにリアルタイムでストリーミングする必要があります。Responses must stream in real time to the client.
  • クライアントでは多くの結果を収集する必要があり、それらの結果の受信待機時間が重要になります。The client needs to collect many results, and received latency of those results is important. 代わりに、Service Bus パターンを検討します。Consider a service bus pattern instead.
  • WebSocket や SignalR などのサーバー側の永続的なネットワーク接続を使用できます。You can use server-side persistent network connections such as WebSockets or SignalR. これらのサービスを使用して、呼び出し元に結果を通知できます。These services can be used to notify the caller of the result.
  • ネットワーク設計により、ポートを開いて非同期コールバックや webhook を受信することができます。The network design allows you to open up ports to receive asynchronous callbacks or webhooks.

Example

次のコードは、Azure Functions を使用して、このパターンを実装するアプリケーションからの抜粋を示しています。The following code shows excerpts from an application that uses Azure Functions to implement this pattern. このソリューションには、次の 3 つの関数があります。There are three functions in the solution:

  • 非同期 API エンドポイント。The asynchronous API endpoint.
  • 状態エンドポイント。The status endpoint.
  • キューに置かれた作業項目を取得して、それらを実行するバックエンド関数。A backend function that takes queued work items and executes them.

関数内の非同期要求応答パターンの構造のイメージ

GitHub ロゴ このサンプルは GitHub で入手できます。GitHub logo This sample is available on GitHub.

<a name="asyncprocessingworkacceptor-function">AsyncProcessingWorkAcceptor 関数AsyncProcessingWorkAcceptor function

AsyncProcessingWorkAcceptor 関数は、クライアント アプリケーションから作業を受け入れ、それを処理のためにキューに配置するエンドポイントを実装します。The AsyncProcessingWorkAcceptor function implements an endpoint that accepts work from a client application and puts it on a queue for processing.

  • 関数は、要求 ID を生成し、それをメタデータとしてキュー メッセージに追加します。The function generates a request ID and adds it as metadata to the queue message.
  • HTTP 応答には、状態エンドポイントを指す場所ヘッダーが含まれます。The HTTP response includes a location header pointing to a status endpoint. 要求 ID は URL パスの一部です。The request ID is part of the URL path.
public static class AsyncProcessingWorkAcceptor
{
    [FunctionName(&quot;AsyncProcessingWorkAcceptor")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] CustomerPOCO customer,
        [ServiceBus("outqueue", Connection = "ServiceBusConnectionAppSetting")] IAsyncCollector<Message> OutMessage,
        ILogger log)
    {
        if (String.IsNullOrEmpty(customer.id) || String.IsNullOrEmpty(customer.customername))
        {
            return new BadRequestResult();
        }

        string reqid = Guid.NewGuid().ToString();

        string rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{reqid}";

        var messagePayload = JsonConvert.SerializeObject(customer);
        Message m = new Message(Encoding.UTF8.GetBytes(messagePayload));
        m.UserProperties["RequestGUID"] = reqid;
        m.UserProperties["RequestSubmittedAt"] = DateTime.Now;
        m.UserProperties["RequestStatusURL"] = rqs;

        await OutMessage.AddAsync(m);  

        return (ActionResult) new AcceptedResult(rqs, $"Request Accepted for Processing{Environment.NewLine}ProxyStatus: {rqs}");
    }
}

AsyncProcessingBackgroundWorker 関数AsyncProcessingBackgroundWorker function

AsyncProcessingBackgroundWorker 関数は、キューから操作を取得し、メッセージ ペイロードに基づいて何らかの作業を行い、その結果を SAS シグネチャの場所に書き込みます。The AsyncProcessingBackgroundWorker function picks up the operation from the queue, does some work based on the message payload, and writes the result to the SAS signature location.

public static class AsyncProcessingBackgroundWorker
{
    [FunctionName("AsyncProcessingBackgroundWorker")]
    public static void Run(
        [ServiceBusTrigger("outqueue", Connection = "ServiceBusConnectionAppSetting")]Message myQueueItem,
        [Blob("data", FileAccess.ReadWrite, Connection = "StorageConnectionAppSetting")] CloudBlobContainer inputBlob,
        ILogger log)
    {
        // Perform an actual action against the blob data source for the async readers to be able to check against.
        // This is where your actual service worker processing will be performed.

        var id = myQueueItem.UserProperties["RequestGUID"] as string;

        CloudBlockBlob cbb = inputBlob.GetBlockBlobReference($"{id}.blobdata");

        // Now write the results to blob storage.
        cbb.UploadFromByteArrayAsync(myQueueItem.Body, 0, myQueueItem.Body.Length);
    }
}

AsyncOperationStatusChecker 関数AsyncOperationStatusChecker function

AsyncOperationStatusChecker 関数は、状態エンドポイントを実装します。The AsyncOperationStatusChecker function implements the status endpoint. この関数は、まず要求が完了したかどうかをチェックしますThis function first checks whether the request was completed

  • 要求が完了している場合、関数は応答にバレー キーを返すか、またはその呼び出しをバレー キー URL に即時にリダイレクトします。If the request was completed, the function either returns a valet-key to the response, or redirects the call immediately to the valet-key URL.
  • 要求がまだ保留中の場合は、自己参照の Location ヘッダーを含めて 202 accepted を返し、http Retry After ヘッダーに完了した応答の ETA を入れる必要があります。If the request is still pending, then we should return a 202 accepted with a self-referencing Location header, putting an ETA for a completed response in the http Retry-After header.
public static class AsyncOperationStatusChecker
{
    [FunctionName("AsyncOperationStatusChecker")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "RequestStatus/{thisGUID}")] HttpRequest req,
        [Blob("data/{thisGuid}.blobdata", FileAccess.Read, Connection = "StorageConnectionAppSetting")] CloudBlockBlob inputBlob, string thisGUID,
        ILogger log)
    {

        OnCompleteEnum OnComplete = Enum.Parse<OnCompleteEnum>(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect");
        OnPendingEnum OnPending = Enum.Parse<OnPendingEnum>(req.Query["OnPending"].FirstOrDefault() ?? "Accepted");

        log.LogInformation($"C# HTTP trigger function processed a request for status on {thisGUID} - OnComplete {OnComplete} - OnPending {OnPending}");

        // Check to see if the blob is present.
        if (await inputBlob.ExistsAsync())
        {
            // If it's present, depending on the value of the optional "OnComplete" parameter choose what to do.
            return await OnCompleted(OnComplete, inputBlob, thisGUID);
        }
        else
        {
            // If it's NOT present, check the optional "OnPending" parameter.
            string rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{thisGUID}";

            switch (OnPending)
            {
                case OnPendingEnum.Accepted:
                    {
                        // Return an HTTP 202 status code.
                        return (ActionResult)new AcceptedResult() { Location = rqs };
                    }

                case OnPendingEnum.Synchronous:
                    {
                        // Back off and retry. Time out if the backoff period hits one minute
                        int backoff = 250;

                        while (!await inputBlob.ExistsAsync() && backoff < 64000)
                        {
                            log.LogInformation($"Synchronous mode {thisGUID}.blob - retrying in {backoff} ms");
                            backoff = backoff * 2;
                            await Task.Delay(backoff);
                        }

                        if (await inputBlob.ExistsAsync())
                        {
                            log.LogInformation($"Synchronous Redirect mode {thisGUID}.blob - completed after {backoff} ms");
                            return await OnCompleted(OnComplete, inputBlob, thisGUID);
                        }
                        else
                        {
                            log.LogInformation($"Synchronous mode {thisGUID}.blob - NOT FOUND after timeout {backoff} ms");
                            return (ActionResult)new NotFoundResult();
                        }
                    }

                default:
                    {
                        throw new InvalidOperationException($"Unexpected value: {OnPending}");
                    }
            }
        }
    }

    private static async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, CloudBlockBlob inputBlob, string thisGUID)
    {
        switch (OnComplete)
        {
            case OnCompleteEnum.Redirect:
                {
                    // Redirect to the SAS URI to blob storage
                    return (ActionResult)new RedirectResult(inputBlob.GenerateSASURI());
                }

            case OnCompleteEnum.Stream:
                {
                    // Download the file and return it directly to the caller.
                    // For larger files, use a stream to minimize RAM usage.
                    return (ActionResult)new OkObjectResult(await inputBlob.DownloadTextAsync());
                }

            default:
                {
                    throw new InvalidOperationException($"Unexpected value: {OnComplete}");
                }
        }
    }
}

public enum OnCompleteEnum {

    Redirect,
    Stream
}

public enum OnPendingEnum {

    Accepted,
    Synchronous
}

このパターンを実装するときは、次の情報を参考にしてください。The following information may be relevant when implementing this pattern: