Aszinkron Request-Reply mintaAsynchronous Request-Reply pattern

Leválaszthatja a háttérbeli feldolgozást az előtérbeli gazdagépről, ha a háttérbeli feldolgozásnak aszinkronnak kell lennie, de az előtér egyértelmű választ igényel.Decouple backend processing from a frontend host, where backend processing needs to be asynchronous, but the frontend still needs a clear response.

Kontextus és problémaContext and problem

A modern alkalmazásfejlesztés során normális, hogy az ügyfélalkalmazások — gyakran futtatnak egy webes ügyfélprogramot (böngészőt), — hogy a távoli API-kra támaszkodva üzleti logikát és összeállítási funkciókat biztosítanak.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. Ezek az API-k közvetlenül kapcsolódnak az alkalmazáshoz, vagy harmadik fél által biztosított megosztott szolgáltatásokhoz is.These APIs may be directly related to the application or may be shared services provided by a third party. Ezeket az API-hívásokat általában a HTTP (S) protokollon keresztül hajtják végre, és követik a REST szemantikai utasításait.Commonly these API calls take place over the HTTP(S) protocol and follow REST semantics.

A legtöbb esetben az ügyfélalkalmazások API-jai gyors reagálásra készültek, 100 MS vagy kevesebb sorrendben.In most cases, APIs for a client application are designed to respond quickly, on the order of 100 ms or less. Számos tényező befolyásolhatja a válasz késését, beleértve a következőket:Many factors can affect the response latency, including:

  • Egy alkalmazás üzemeltetési vereme.An application's hosting stack.
  • Biztonsági összetevők.Security components.
  • A hívó és a háttér relatív földrajzi helye.The relative geographic location of the caller and the backend.
  • Hálózati infrastruktúra.Network infrastructure.
  • Aktuális terhelés.Current load.
  • A kérelem hasznos adatainak mérete.The size of the request payload.
  • Feldolgozási várólista hosszaProcessing queue length.
  • Az az idő, ameddig a háttérrendszer feldolgozza a kérést.The time for the backend to process the request.

Ezen tényezők bármelyike felveheti a válasz késleltetését.Any of these factors can add latency to the response. Néhányat a háttér horizontális felskálázásával lehet enyhíteni.Some can be mitigated by scaling out the backend. Mások, például a hálózati infrastruktúra, nagyrészt az alkalmazás fejlesztője felett vannak.Others, such as network infrastructure, are largely out of the control of the application developer. A legtöbb API elég gyorsan reagálni tud arra, hogy a válaszok ugyanarra a kapcsolatra jussanak vissza.Most APIs can respond quickly enough for responses to arrive back over the same connection. Az alkalmazás kódja a szinkron API-hívást nem blokkoló módon teheti meg, így az aszinkron feldolgozás megjelenését is lehetővé teszi, amelyet az I/O-kötésű műveletekhez ajánlott használni.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.

Bizonyos esetekben azonban a háttérrendszer által végzett munka hosszabb ideig is futhat, a másodpercek sorrendjében, vagy akár percek alatt, akár akár órákban is végrehajtva.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. Ebben az esetben nem lehet megvárni a munka befejeződését, mielőtt válaszol a kérelemre.In that case, it isn't feasible to wait for the work to complete before responding to the request. Ez a helyzet egy esetleges szinkron kérés-válasz típusú minta lehetséges problémája.This situation is a potential problem for any synchronous request-reply pattern.

Egyes architektúrák megoldják ezt a problémát úgy, hogy egy Message Broker segítségével elkülönítik a kérés és a válasz szakaszait.Some architectures solve this problem by using a message broker to separate the request and response stages. Ezt az elkülönítést gyakran a várólista-alapú terheléselosztási mintahasználatával kell megvalósítani.This separation is often achieved by use of the Queue-Based Load Leveling pattern. Ez az elkülönítés lehetővé teszi, hogy az ügyfél és a háttérrendszer API egymástól függetlenül méretezhető legyen.This separation can allow the client process and the backend API to scale independently. Az elkülönítés azonban további bonyolultságot is biztosít, ha az ügyfélnek sikeres értesítésre van szüksége, mivel ennek a lépésnek aszinkronnek kell lennie.But this separation also brings additional complexity when the client requires success notification, as this step needs to become asynchronous.

Az ügyfélalkalmazások többek között a kiszolgálók közötti REST API hívások esetében is érvényesek az elosztott rendszerekben — , például egy Service-architektúrában.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.

MegoldásSolution

A probléma egyik megoldása a HTTP-lekérdezés használata.One solution to this problem is to use HTTP polling. A lekérdezés az ügyféloldali kódok számára hasznos, mivel nehéz lehet visszahívási végpontokat biztosítani, vagy hosszú ideig futó kapcsolatokat használni.Polling is useful to client-side code, as it can be hard to provide call-back endpoints or use long running connections. Ha a visszahívások is lehetségesek, a szükséges további könyvtárak és szolgáltatások esetenként túl sok további bonyolultságot adhatnak hozzá.Even when callbacks are possible, the extra libraries and services that are required can sometimes add too much extra complexity.

  • Az ügyfélalkalmazás szinkron hívást kezdeményez az API-hoz, és egy hosszan futó műveletet indít el a háttéren.The client application makes a synchronous call to the API, triggering a long-running operation on the backend.

  • Az API a lehető leggyorsabban válaszol.The API responds synchronously as quickly as possible. Egy HTTP 202 (elfogadva) állapotkódot ad vissza, amely tudomásul veszi, hogy a kérelem feldolgozásra érkezett.It returns an HTTP 202 (Accepted) status code, acknowledging that the request has been received for processing.

    Megjegyzés

    Az API-nak ellenőriznie kell a kérelmet és a végrehajtandó műveletet a hosszú ideig futó folyamat elindítása előtt.The API should validate both the request and the action to be performed before starting the long running process. Ha a kérés érvénytelen, a válasz azonnal egy hibakódtal, például a HTTP 400-as (hibás kérelem) kóddal válaszol.If the request is invalid, reply immediately with an error code such as HTTP 400 (Bad Request).

  • A válasz egy olyan végpontra mutat, amely arra utal, hogy az ügyfél lekérdezi a hosszú ideig futó művelet eredményét.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.

  • Az API kiszervezi a feldolgozást egy másik összetevőbe, például egy üzenetsor-várólistára.The API offloads processing to another component, such as a message queue.

  • Amíg a munka továbbra is függőben van, az állapot végpontja a HTTP 202 értéket adja vissza.While the work is still pending, the status endpoint returns HTTP 202. A munka befejezése után az állapot végpontja olyan erőforrást adhat vissza, amely a befejezést jelzi, vagy átirányítja egy másik erőforrás URL-címére.Once the work is complete, the status endpoint can either return a resource that indicates completion, or redirect to another resource URL. Ha például az aszinkron művelet létrehoz egy új erőforrást, az állapot végpontja átirányítja az adott erőforrás URL-címére.For example, if the asynchronous operation creates a new resource, the status endpoint would redirect to the URL for that resource.

Az alábbi ábrán egy tipikus folyamat látható:The following diagram shows a typical flow:

Kérelmek és válaszok folyamata aszinkron HTTP-kérelmek esetén

  1. Az ügyfél kérelmet küld, és HTTP 202 (elfogadott) választ kap.The client sends a request and receives an HTTP 202 (Accepted) response.
  2. Az ügyfél HTTP GET kérelmet küld az állapot végpontjának.The client sends an HTTP GET request to the status endpoint. A munka továbbra is függőben van, így a hívás a HTTP 202-et is visszaadja.The work is still pending, so this call also returns HTTP 202.
  3. Egy ponton a munka befejeződött, és az állapot végpontja a 302 (talált) átirányítását adja vissza az erőforráshoz.At some point, the work is complete and the status endpoint returns 302 (Found) redirecting to the resource.
  4. Az ügyfél beolvassa az erőforrást a megadott URL-címen.The client fetches the resource at the specified URL.

Problémák és megfontolandó szempontokIssues and considerations

  • A minta a HTTP protokollon keresztül valósítható meg, és nem minden felsőbb rétegbeli szolgáltatás ugyanazzal a szemantikagal rendelkezik.There are a number of possible ways to implement this pattern over HTTP and not all upstream services have the same semantics. A legtöbb szolgáltatás például nem ad vissza HTTP 202-választ a GET metódusból, ha egy távoli folyamat nem fejeződött be.For example, most services won't return an HTTP 202 response back from a GET method when a remote process hasn't finished. A tiszta REST-szemantika után a HTTP 404 (nem található) értéket kell visszaadnia.Following pure REST semantics, they should return HTTP 404 (Not Found). Ez a válasz akkor logikus, ha a hívás eredményét még nem látja.This response makes sense when you consider the result of the call isn't present yet.

  • A HTTP 202-válasznak jeleznie kell az ügyfél által a válaszra vonatkozó lekérdezés helyét és gyakoriságát.An HTTP 202 response should indicate the location and frequency that the client should poll for the response. A következő további fejlécekkel kell rendelkeznie:It should have the following additional headers:

    FejlécHeader LeírásDescription JegyzetekNotes
    HelyLocation Egy URL-cím, amelyet az ügyfélnek a válasz állapotára kell lekérdezni.A URL the client should poll for a response status. Ez az URL-cím lehet egy olyan SAS-token, amelynek a inas-kulcs mintája megfelelő, ha a helynek hozzáférés-vezérlésre van szüksége.This URL could be a SAS token with the Valet Key Pattern being appropriate if this location needs access control. A inasi kulcs mintája akkor is érvényes, ha a válasz lekérdezése egy másik háttérbe való kiszervezést igényelThe valet key pattern is also valid when response polling needs offloading to another backend
    Retry-AfterRetry-After A feldolgozás befejezésének becsléseAn estimate of when processing will complete Ez a fejléc úgy van kialakítva, hogy megakadályozza az ügyfeleket abban, hogy az újrapróbálkozásokkal elárasztsák a háttér-visszaállítást.This header is designed to prevent polling clients from overwhelming the back-end with retries.
  • Előfordulhat, hogy egy feldolgozó proxyt vagy homlokzatot kell használnia a válasz fejlécének vagy a hasznos adatnak a felhasznált mögöttes szolgáltatástól függően történő kezeléséhez.You may need to use a processing proxy or facade to manipulate the response headers or payload depending on the underlying services used.

  • Ha az állapotjelző végpont átirányítja a befejezést, akkor a http 302 vagy a http 303 megfelelő visszatérési kódoknak felel meg, az Ön által támogatott szemantikai beállításoktól függően.If the status endpoint redirects on completion, either HTTP 302 or HTTP 303 are appropriate return codes, depending on the exact semantics you support.

  • A sikeres feldolgozás után a hely fejlécében megadott erőforrásnak egy megfelelő HTTP-választ kell visszaadnia, például 200 (OK), 201 (létrehozva) vagy 204 (nincs tartalom).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).

  • Ha hiba történik a feldolgozás során, a hibát megőrzi a hely fejlécében ismertetett erőforrás-URL-címen, és ideális esetben megfelelő választ ad vissza az ügyfélnek az adott erőforrásból (4xx-kód).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).

  • Nem minden megoldás fogja megvalósítani ezt a mintát, és egyes szolgáltatások további vagy másodlagos fejléceket is tartalmaznak.Not all solutions will implement this pattern in the same way and some services will include additional or alternate headers. A Azure Resource Manager például a minta módosított változatát használja.For example, Azure Resource Manager uses a modified variant of this pattern. További információ: Azure Resource Manager aszinkron művelet.For more information, see Azure Resource Manager Async Operations.

  • Előfordulhat, hogy az örökölt ügyfelek nem támogatják ezt a mintát.Legacy clients might not support this pattern. Ebben az esetben előfordulhat, hogy egy homlokzatot kell elhelyeznie az aszinkron API-ban, hogy elrejtse az aszinkron feldolgozást az eredeti ügyfélről.In that case, you might need to place a facade over the asynchronous API to hide the asynchronous processing from the original client. A Azure Logic Apps például támogatja ezt a mintázatot natív módon, amely egy aszinkron API és egy olyan ügyfél közötti integrációs rétegként használható, amely szinkron hívásokat tesz lehetővé.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. Lásd: hosszan futó feladatok végrehajtása a webhook műveleti mintával.See Perform long-running tasks with the webhook action pattern.

  • Bizonyos esetekben érdemes lehet megadnia az ügyfelek számára a hosszú ideig futó kérések megszakítását.In some scenarios, you might want to provide a way for clients to cancel a long-running request. Ebben az esetben a háttér-szolgáltatásnak támogatnia kell a lemondási utasítás valamilyen formáját.In that case, the backend service must support some form of cancellation instruction.

Mikor érdemes ezt a mintát használni?When to use this pattern

A következő minta használata:Use this pattern for:

  • Az ügyféloldali kódok, például a böngészőbeli alkalmazások, ahol nehéz a visszahívási végpontok biztosítása, vagy a hosszan futó kapcsolatok használata túl sok további bonyolultságot ad hozzá.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.

  • A szolgáltatás olyan hívásokat kezdeményez, amelyekben csak a HTTP protokoll érhető el, és a visszatérési szolgáltatás nem tud visszahívást hívni az ügyféloldali tűzfal-korlátozások miatt.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.

  • Azok a szolgáltatások, amelyeket olyan régi architektúrákkal kell integrálni, amelyek nem támogatják a modern visszahívási technológiákat, például a websocketeket vagy a webhookokat.Service calls that need to be integrated with legacy architectures that don't support modern callback technologies such as WebSockets or webhooks.

Előfordulhat, hogy ez a minta nem alkalmas a következő esetekben:This pattern might not be suitable when:

  • Használhat olyan szolgáltatást, amely aszinkron értesítésekhez készült, például Azure Event Grid.You can use a service built for asynchronous notifications instead, such as Azure Event Grid.
  • A válaszoknak valós időben kell továbbítaniuk az ügyfelet.Responses must stream in real time to the client.
  • Az ügyfélnek sok eredményt kell összegyűjtenie, és a kapott eredmények késését fontos.The client needs to collect many results, and received latency of those results is important. Használjon helyette egy Service Bus-mintát.Consider a service bus pattern instead.
  • Használhatja a kiszolgálóoldali állandó hálózati kapcsolatokat, például a websocketeket vagy a jelzőt.You can use server-side persistent network connections such as WebSockets or SignalR. Ezeknek a szolgáltatásoknak a segítségével értesítheti az eredmény hívóját.These services can be used to notify the caller of the result.
  • A hálózati kialakítás lehetővé teszi a portok megnyitását aszinkron visszahívások vagy webhookok fogadására.The network design allows you to open up ports to receive asynchronous callbacks or webhooks.

PéldaExample

A következő kód olyan alkalmazás részleteit jeleníti meg, amelyek Azure Functionst használnak a minta megvalósításához.The following code shows excerpts from an application that uses Azure Functions to implement this pattern. A megoldás három funkcióval rendelkezik:There are three functions in the solution:

  • Az aszinkron API-végpont.The asynchronous API endpoint.
  • Az állapot végpontja.The status endpoint.
  • Egy háttérbeli függvény, amely várólistán lévő munkaelemeket tart, és végrehajtja azokat.A backend function that takes queued work items and executes them.

A functions aszinkron kérelmekre vonatkozó válasz mintájának képe

GitHub-embléma Ez a minta elérhető a githubon.GitHub logo This sample is available on GitHub.

AsyncProcessingWorkAcceptor függvényAsyncProcessingWorkAcceptor function

A AsyncProcessingWorkAcceptor függvény olyan végpontot valósít meg, amely egy ügyfélalkalmazás munkáját fogadja, és egy várólistára helyezi a feldolgozáshoz.The AsyncProcessingWorkAcceptor function implements an endpoint that accepts work from a client application and puts it on a queue for processing.

  • A függvény létrehoz egy kérés-azonosítót, és metaadatokként hozzáadja az üzenetsor-üzenethez.The function generates a request ID and adds it as metadata to the queue message.
  • A HTTP-válasz egy állapot-végpontra mutató Location fejlécet tartalmaz.The HTTP response includes a location header pointing to a status endpoint. A kérelem azonosítója az URL elérési útjának része.The request ID is part of the URL path.
public static class AsyncProcessingWorkAcceptor
{
    [FunctionName("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 függvényAsyncProcessingBackgroundWorker function

A AsyncProcessingBackgroundWorker függvény a várólistáról veszi fel a műveletet, és az üzenet tartalma alapján működik, és az eredményt a sas-aláírás helyére írja.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 függvényAsyncOperationStatusChecker function

A AsyncOperationStatusChecker függvény végrehajtja az állapot végpontját.The AsyncOperationStatusChecker function implements the status endpoint. Ez a függvény először ellenőrzi, hogy befejeződött-e a kérelem.This function first checks whether the request was completed

  • Ha a kérés befejeződött, a függvény egy inas-kulcsot ad vissza a válaszhoz, vagy azonnal átirányítja a hívást a szerviz-kulcs URL-címére.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.
  • Ha a kérelem továbbra is függőben van, akkor vissza kell térnie egy önhivatkozó Location fejléctel rendelkező 202-re, amely egy ETA-t helyez el egy befejezett válaszhoz a http Retry-After fejlécben.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
}

Az alábbi információk segíthetnek a minta megvalósításakor:The following information may be relevant when implementing this pattern: