Aszinkron kérés-válasz minta

Azure
Azure Logic Apps

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.

Kontextus és probléma

A modern alkalmazásfejlesztés során az ügyfélalkalmazások – gyakran webügyfelek (böngészők) által futtatott kód – esetében megszokott, hogy távoli API-któl függenek, hogy üzleti logikát és írási funkciókat biztosítsanak. Ezek az API-k közvetlenül kapcsolódnak az alkalmazáshoz, vagy egy harmadik fél által nyújtott megosztott szolgáltatások lehetnek. Ezek az API-hívások általában a HTTP(S) protokollon keresztül történnek, és a REST szemantikát követik.

Az ügyfélalkalmazások API-jait a legtöbb esetben úgy tervezték, hogy gyorsan, 100 ms vagy annál kisebb sorrendben válaszoljanak. Számos tényező befolyásolhatja a válasz késését, például:

  • Egy alkalmazás üzemelteti a vermet.
  • Biztonsági összetevők.
  • A hívó és a háttérrendszer relatív földrajzi helye.
  • Hálózati infrastruktúra.
  • Aktuális terhelés.
  • A kérelem hasznos adatainak mérete.
  • Az üzenetsor hosszának feldolgozása.
  • A háttérrendszer számára a kérés feldolgozásának ideje.

Ezen tényezők bármelyike késést adhat a válaszhoz. Néhányat a háttérrendszer horizontális felskálázásával lehet enyhíteni. Mások, például a hálózati infrastruktúra, nagyrészt nem irányítják az alkalmazásfejlesztőt. A legtöbb API elég gyorsan tud válaszolni ahhoz, hogy a válaszok ugyanazon a kapcsolaton keresztül érkezzenek vissza. Az alkalmazáskód nem blokkoló módon tud szinkron API-hívást kezdeményezni, így aszinkron feldolgozás jelenik meg, ami az I/O-kötésű műveletekhez ajánlott.

Bizonyos esetekben azonban előfordulhat, hogy a háttérrendszer által végzett munka hosszú ideig fut, másodpercek sorrendjében, vagy lehet egy háttérfolyamat, amelyet percekben vagy akár órákban hajtanak végre. Ebben az esetben nem lehet megvárni, amíg a munka befejeződik, mielőtt válaszol a kérésre. Ez a helyzet potenciális probléma bármilyen szinkron kérés-válasz mintával kapcsolatban.

Egyes architektúrák úgy oldják meg ezt a problémát, hogy egy üzenetközvetítővel választják el a kérési és válaszszakaszokat. Ezt az elkülönítést gyakran a várólista-alapú terheléselegyenlítési minta használatával lehet elérni. Ez az elkülönítés lehetővé teszi, hogy az ügyfélfolyamat és a háttér API egymástól függetlenül skálázható legyen. Ez az elkülönítés azonban további összetettséget is eredményez, ha az ügyfél sikerértesítést igényel, mivel ennek a lépésnek aszinkronnak kell lennie.

Az ügyfélalkalmazások esetében tárgyalt szempontok közül sok az elosztott rendszerekben – például mikroszolgáltatás-architektúrában – a kiszolgáló–kiszolgáló KÖZÖTTI REST API-hívásokra is vonatkozik.

Megoldás

A probléma egyik megoldása a HTTP-lekérdezés használata. A lekérdezés hasznos az ügyféloldali kódhoz, mivel nehéz lehet visszahívási végpontokat biztosítani, vagy hosszú ideig futó kapcsolatokat használni. Még ha visszahívások is lehetségesek, a szükséges további kódtárak és szolgáltatások néha túl sok extra összetettséggel járnak.

  • Az ügyfélalkalmazás szinkron hívást kezdeményez az API-hoz, és elindít egy hosszú ideig futó műveletet a háttérrendszeren.

  • Az API a lehető leggyorsabban szinkron módon válaszol. Egy HTTP 202 (elfogadott) állapotkódot ad vissza, elismerve, hogy a kérés feldolgozásra érkezett.

    Feljegyzés

    Az API-nak a hosszú ideig futó folyamat megkezdése előtt ellenőriznie kell a kérést és a végrehajtandó műveletet is. Ha a kérelem érvénytelen, azonnal válaszolja meg a következő hibakódot: HTTP 400 (Hibás kérés).

  • A válasz egy olyan végpontra mutató helyhivatkozást tartalmaz, amelyet az ügyfél lekérdezhet a hosszú ideig futó művelet eredményének ellenőrzéséhez.

  • Az API kicsomagozza a feldolgozást egy másik összetevőre, például egy üzenetsorra.

  • Az állapotvégpont minden sikeres hívásához HTTP 200-et ad vissza. Amíg a munka még függőben van, az állapotvégpont egy erőforrást ad vissza, amely jelzi, hogy a munka még folyamatban van. Ha a munka befejeződött, az állapotvégpont visszaadhat egy befejezést jelző erőforrást, vagy átirányíthatja egy másik erőforrás URL-címére. Ha például az aszinkron művelet új erőforrást hoz létre, az állapotvégpont átirányítja az adott erőforrás URL-címére.

Az alábbi diagram egy tipikus folyamatot mutat be:

Aszinkron HTTP-kérések kérése és válaszfolyamata

  1. Az ügyfél kérést küld, és HTTP 202 (Elfogadott) választ kap.
  2. Az ügyfél HTTP GET kérést küld az állapotvégpontnak. A munka még függőben van, ezért ez a hívás HTTP 200-et ad vissza.
  3. Egy ponton a munka befejeződött, és az állapotvégpont a 302-et adja vissza, amely átirányítja az erőforrást.
  4. Az ügyfél lekéri az erőforrást a megadott URL-címen.

Problémák és megfontolandó szempontok

  • Ezt a mintát többféleképpen is implementálhatja HTTP-en keresztül, és nem minden felsőbb rétegbeli szolgáltatásnak ugyanaz a szemantikája. 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 még nem fejeződött be. A tiszta REST szemantikát követve HTTP 404-et kell visszaadniuk (nem található). Ennek a válasznak akkor van értelme, ha úgy véli, hogy a hívás eredménye még nem jelenik meg.

  • A HTTP 202-válasznak azt a helyet és gyakoriságot kell jeleznie, amelyet az ügyfélnek le kell keresnie a válaszhoz. A következő további fejlécekkel kell rendelkeznie:

    Fejléc Leírás Jegyzetek
    Hely Egy URL- cím, amely alapján az ügyfélnek le kell keresnie a válasz állapotát. Ez az URL-cím lehet SAS-jogkivonat, és a valet kulcsminta megfelelő, ha ehhez a helyhez hozzáférés-vezérlésre van szükség. Az értékkulcs minta akkor is érvényes, ha a válasz-lekérdezést ki kell kapcsolni egy másik háttérrendszerbe
    Újrapróbálkozás után Becslés arra vonatkozóan, hogy mikor fejeződik be a feldolgozás Ez a fejléc célja, hogy megakadályozza, hogy a lekérdezési ügyfelek túlterhelik a háttérrendszert az újrapróbálkozással.
  • Előfordulhat, hogy a válaszfejlécek vagy a hasznos adatok kezeléséhez feldolgozási proxyt vagy homlokzatot kell használnia a használt mögöttes szolgáltatásoktól függően.

  • Ha az állapotvégpont a befejezéskor átirányít, akkor a HTTP 302 vagy a HTTP 303 megfelelő visszatérési kód, a támogatott szemantikától függően.

  • Sikeres feldolgozás esetén a Hely fejlécben megadott erőforrásnak megfelelő HTTP-válaszkódot kell visszaadnia, például 200 (OK), 201 (Létrehozva) vagy 204 -et (Nincs tartalom).

  • Ha a feldolgozás során hiba történik, őrizze meg a hibát a Hely fejlécben leírt erőforrás URL-címén, és ideális esetben adja vissza a megfelelő válaszkódot az ügyfélnek az adott erőforrásból (4xx kód).

  • Nem minden megoldás implementálja ezt a mintát ugyanúgy, és egyes szolgáltatások további vagy alternatív fejléceket is tartalmaznak. Az Azure Resource Manager például ennek a mintának egy módosított változatát használja. További információ: Azure Resource Manager Async Operations.

  • Előfordulhat, hogy a régi ügyfelek nem támogatják ezt a mintát. Ebben az esetben előfordulhat, hogy egy homlokzatot kell elhelyeznie az aszinkron API-ra, hogy elrejtse az aszinkron feldolgozást az eredeti ügyfél elől. Az Azure Logic Apps például natív módon támogatja ezt a mintát egy aszinkron API és egy szinkron hívásokat végző ügyfél integrációs rétegeként. Lásd: Hosszú ideig futó feladatok végrehajtása a webhook műveleti mintával.

  • Bizonyos esetekben érdemes lehet módot biztosítani az ügyfelek számára egy hosszú ideig futó kérelem megszakítására. Ebben az esetben a háttérszolgáltatásnak valamilyen lemondási utasítást kell támogatnia.

Mikor érdemes ezt a mintát használni?

Használja ezt a mintát a következőkhöz:

  • Az ügyféloldali kód, például a böngészőalkalmazások, ahol nehéz visszahívási végpontokat biztosítani, vagy a hosszú ideig futó kapcsolatok használata túl sok további összetettséggel jár.

  • Olyan szolgáltatáshívások, ahol csak a HTTP protokoll érhető el, és a visszatérési szolgáltatás nem tud visszahívásokat indítani az ügyféloldali tűzfalkorlátozások miatt.

  • Olyan szolgáltatáshívásokat kell integrálnunk az örökölt architektúrákkal, amelyek nem támogatják a modern visszahívási technológiákat, például a WebSocketeket vagy a webhookokat.

Ez a minta nem feltétlenül megfelelő, ha:

  • Ehelyett használhat aszinkron értesítésekhez készült szolgáltatást, például az Azure Event Gridet.
  • A válaszoknak valós időben kell streamelnie az ügyfélnek.
  • Az ügyfélnek számos eredményt kell összegyűjtenie, és fontos, hogy az eredmények késése érkezik. Fontolja meg inkább a service bus-mintát.
  • Használhat kiszolgálóoldali állandó hálózati kapcsolatokat, például WebSocketeket vagy SignalR-eket. Ezekkel a szolgáltatásokkal értesítheti a hívót az eredményről.
  • A hálózati kialakítás lehetővé teszi portok megnyitását aszinkron visszahívások vagy webhookok fogadásához.

Számítási feladatok tervezése

Az tervezőknek értékelniük kell, hogyan használható az aszinkron kérés-válasz minta a számítási feladat kialakításában az Azure Well-Architected Framework pilléreiben foglalt célok és alapelvek kezelésére. Példa:

Pillér Hogyan támogatja ez a minta a pillércélokat?
A teljesítményhatékonyság a skálázás, az adatok és a kód optimalizálásával segíti a számítási feladatok hatékony kielégítését . Az azonnali választ nem igénylő folyamatokhoz tartozó interakciók kérési és válaszfázisainak leválasztása javítja a rendszerek válaszkészségét és méretezhetőségét. Aszinkron alkalmazásproachként maximalizálhatja az egyidejűséget a kiszolgáló oldalán, és ütemezheti, hogy a kapacitás lehetővé teszi a munkát.

- PE:05 Skálázás és particionálás
- PE:07 Kód és infrastruktúra

Mint minden tervezési döntésnél, fontolja meg az ezzel a mintával bevezethető többi pillér céljaival szembeni kompromisszumokat.

Példa

Az alábbi kódrészletek egy olyan alkalmazásból származnak, amely az Azure Functions használatával implementálja ezt a mintát. A megoldásban három függvény található:

  • Az aszinkron API-végpont.
  • Az állapotvégpont.
  • Egy háttérfüggvény, amely az üzenetsorba helyezett munkaelemeket veszi át, és végrehajtja őket.

Az Async Request Reply minta struktúrájának képe a Functionsben

GitHub-emblémaEz a minta a GitHubon érhető el.

AsyncProcessingWorkAcceptor függvény

A AsyncProcessingWorkAcceptor függvény implementál egy végpontot, amely fogadja a munkát egy ügyfélalkalmazásból, és egy várólistára helyezi a feldolgozáshoz.

  • A függvény létrehoz egy kérésazonosítót, és metaadatként hozzáadja az üzenetsor-üzenethez.
  • A HTTP-válasz tartalmaz egy állapotvégpontra mutató helyfejlécet. A kérelem azonosítója az URL-elérési út része.
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<ServiceBusMessage> OutMessages,
        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);
        var message = new ServiceBusMessage(messagePayload);
        message.ApplicationProperties.Add("RequestGUID", reqid);
        message.ApplicationProperties.Add("RequestSubmittedAt", DateTime.Now);
        message.ApplicationProperties.Add("RequestStatusURL", rqs);

        await OutMessages.AddAsync(message);

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

AsyncProcessingBackgroundWorker függvény

A AsyncProcessingBackgroundWorker függvény felveszi a műveletet az üzenetsorból, elvégzi a munkát az üzenet hasznos adatai alapján, és az eredményt egy tárfiókba írja.

public static class AsyncProcessingBackgroundWorker
{
    [FunctionName("AsyncProcessingBackgroundWorker")]
    public static async Task RunAsync(
        [ServiceBusTrigger("outqueue", Connection = "ServiceBusConnectionAppSetting")] BinaryData customer,
        IDictionary<string, object> applicationProperties,
        [Blob("data", FileAccess.ReadWrite, Connection = "StorageConnectionAppSetting")] BlobContainerClient inputContainer,
        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 = applicationProperties["RequestGUID"] as string;

        BlobClient blob = inputContainer.GetBlobClient($"{id}.blobdata");

        // Now write the results to blob storage.
        await blob.UploadAsync(customer);
    }
}

AsyncOperationStatusChecker függvény

A AsyncOperationStatusChecker függvény implementálja az állapotvégpontot. Ez a függvény először ellenőrzi, hogy a kérés befejeződött-e

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")] BlockBlobClient inputBlob, string thisGUID,
        ILogger log)
    {

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

        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, then we need to back off. Depending on the value of the optional "OnPending" parameter, choose what to do.
            string rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{thisGUID}";

            switch (OnPending)
            {
                case OnPendingEnum.OK:
                    {
                        // Return an HTTP 200 status code.
                        return new OkObjectResult(new { status = "In progress", 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 new NotFoundResult();
                        }
                    }

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

    private static async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string thisGUID)
    {
        switch (OnComplete)
        {
            case OnCompleteEnum.Redirect:
                {
                    // Redirect to the SAS URI to blob storage

                    return 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 new OkObjectResult(await inputBlob.DownloadContentAsync());
                }

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

public enum OnCompleteEnum
{

    Redirect,
    Stream
}

public enum OnPendingEnum
{

    OK,
    Synchronous
}

Következő lépések

Az alábbi információk segíthetnek a minta megvalósításakor: