Aszinkron kérés-válasz minta

Ha a háttérfeldolgozásnak aszinkron módon kell futnia, de az előtérnek egyértelmű válaszra van szüksége, leválaszthatja a háttérbeli feldolgozást az előtérbeli gazdagépről.

Kontextus és probléma

A modern alkalmazásfejlesztés során az ügyfélalkalmazások gyakran távoli API-któl függenek, hogy üzleti logikát és írási funkciókat biztosítsanak. Számos alkalmazás futtat kódot egy webböngészőben, és más környezetek is üzemeltetnek ügyfélkódot. Az API-k közvetlenül az alkalmazáshoz kapcsolódhatnak, vagy egy külső szolgáltatás megosztott szolgáltatásaiként működhetnek. A legtöbb API-hívás HTTP-t vagy HTTPS-t használ, és a REST szemantikáját követi.

Az ügyfélalkalmazás API-jai a legtöbb esetben körülbelül 100 ezredmásodpercben (ms) vagy annál kevesebben válaszolnak. Számos tényező befolyásolhatja a válasz késését:

  • Az alkalmazás hoszting stackja
  • Biztonsági összetevők
  • A hívó relatív földrajzi helye és a háttérrendszer
  • Hálózati infrastruktúra
  • Aktuális terhelés
  • A kérés terhelésének mérete
  • Sor feldolgozási hossz
  • A kérés feldolgozásának ideje a háttérrendszerben

Ezek a tényezők késést adhatnak a válaszhoz. Egyes tényezőket enyhíthet a háttérrendszer felskálázásával. Más tényezők, például a hálózati infrastruktúra, kívül esnek az alkalmazásfejlesztői irányításon. A legtöbb API elég gyorsan válaszol ahhoz, hogy a válasz ugyanazon a kapcsolaton keresztül térjen vissza. Az alkalmazáskód nem tiltó módon képes szinkron API-hívást kezdeményezni az aszinkron feldolgozás megjelenésének érdekében. Ezt a megközelítést a bemeneti és kimeneti (I/O)-kötött műveletek esetében javasoljuk.

Bizonyos esetekben a háttérrendszer hosszú ideig futó és néhány másodpercig tartó munkát végez. Más esetekben a háttérrendszer hosszú ideig futó háttérmunkát végez percekig vagy hosszabb ideig. Ezekben az esetekben nem várhatja meg, amíg a munka befejeződik, mielőtt választ küld. Ez a helyzet problémát okozhat a szinkron kérés-válasz minták esetében. A háttérfeldolgozás tervezésével kapcsolatos útmutatásért tekintse meg a Háttérfeladatok című témakört.

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. Számos rendszer ezt az elkülönítést a Queue-Based terhelésegyensúlyozási mintával éri el. 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. Emellett további összetettséget is bevezet, ha az ügyfél sikerértesítést igényel, mert ennek a lépésnek aszinkronná kell válnia.

Az ügyfélalkalmazásokra vonatkozó számos szempont az elosztott rendszerekben, például a 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 jól működik az ügyféloldali kód esetében, ha a visszahívási végpontok nem érhetők el, vagy ha a hosszan futó kapcsolatok túl összetettek. Még ha visszahívások is lehetségesek, az általuk igényelt további kódtárak és szolgáltatások növelhetik a bonyolultságot.

A következő lépések a megoldást írják le:

  • Az ügyfélalkalmazás szinkron hívást kezdeményez az API-hoz, hogy elindítsa a 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, amely nyugtázza, hogy megkapta a feldolgozásra vonatkozó kérelmet.

    Megjegyzés:

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

  • A válasz tartalmaz egy helyhivatkozást, amely egy olyan végpontra mutat, 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égpontra irányuló minden sikeres hívás esetén a végpont HTTP 200 (OK) értéket ad vissza. Amíg a munka folyamatban van, az állapotvégpont egy olyan erőforrást ad vissza, amely az állapotot jelzi. Az állapotválasz törzsének elegendő információt kell tartalmaznia ahhoz, hogy az ügyfél megértse a művelet aktuális állapotát.

    Amikor a munka befejeződött, az állapotvégpont egy olyan erőforrást ad vissza, amely befejezést jelez, vagy átirányít 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 ábrán egy tipikus folyamat látható.

Az aszinkron HTTP-kérések kérési és válaszfolyamatát bemutató diagram.

  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. Ez a hívás HTTP 200-et ad vissza, mert a munka függőben van.

  3. Egy bizonyos ponton a munka befejeződik, és az állapotvégpont HTTP 303-at ad vissza (lásd egyéb) az erőforrásra való átirányításhoz.

  4. Az ügyfél lekéri az erőforrást a megadott URL-címen.

Problémák és szempontok

Vegye figyelembe a következő szempontokat, amikor úgy dönt, hogy hogyan valósítja meg ezt a mintát:

  • Ennek a mintának http-en keresztül történő implementálására több módszer is létezik, és a felsőbb rétegbeli szolgáltatások nem mindig ugyanazt a szemantikát használják. Egyes implementációk például nem használnak külön állapotvégpontot. Ehelyett az ügyfél közvetlenül lekérdezi a célerőforrás URL-címét, és a HTTP 404 -et (nem található) kapja meg, amíg az erőforrás létre nem jön. Ez a válasz azért jön létre, mert az erőforrás még nem létezik. Ez a megközelítés azonban nem egyértelmű, mert az érvénytelen kérelemazonosítók a HTTP 404-et is visszaadják. A különálló állapotvégpont, amely a mintában leírtak szerint HTTP 200 állapotkóddal és egy állapottesttel tér vissza, elkerüli ezt a zavart.

  • A HTTP 202-válasz azt jelzi, hogy az ügyfél hol és milyen gyakran kérdez le. A következő fejléceket kell tartalmaznia.

    Fejléc Leírás Jegyzetek
    Location Egy URL-cím, amelyet az ügyfél válaszállapotra lekérdez Ez az URL-cím lehet egy megosztott hozzáférési aláírás (SAS) token. A Valet-kulcs minta akkor működik jól, ha ehhez a helyhez hozzáférés-vezérlésre van szükség. A minta akkor is érvényes, ha a válasz lekérdezésének át kell lépnie egy másik háttérrendszerbe.
    Retry-After A feldolgozás becsült befejezési ideje Ez a fejléc segít az adatlekérdező klienseknek elkerülni, hogy túl sok kérést küldjenek a háttérrendszerbe.

    A válasz tervezésekor vegye figyelembe a várt ügyfélviselkedést. Az Ön által vezérelt ügyfél pontosan követheti ezeket a válaszértékeket. A mások által készített ügyfelek, köztük a kód nélküli vagy alacsony kódszámú eszközökkel ( például Azure Logic Apps) létrehozott ügyfelek is alkalmazhatják saját kezelésüket a HTTP 202-hez.

  • Vegye figyelembe a következő mezőket az állapotvégpont válaszában.

    szakterület Leírás Jegyzetek
    status A művelet aktuális állapota, például Függőben, Fut, Sikeres, Sikertelen vagy Megszakítva Terminál- és nemterminális értékek konzisztens, dokumentált készletét használja
    createdAt A művelet elfogadásának időpontja Segít az ügyfeleknek az elavult vagy elhagyatott műveletek észlelésében
    lastUpdatedAt Az állapot legutóbbi frissítésének időpontja Segít az ügyfeleknek különbséget tenni az elakadt és a folyamatban lévő műveletek között
    percentComplete Nem kötelező folyamatjelző Hasznos, ha a háttérrendszer meg tudja becsülni az előrehaladást
    error Strukturált hibaobjektum, ha az állapot sikertelen A konzisztencia érdekében fontolja meg az RFC 9457 formátum használatát.
  • Előfordulhat, hogy feldolgozó proxyt kell használnia a válaszfejlécek vagy hasznos adatok módosításához, a használt háttérszolgáltatásoktól függően.

  • Ha az állapotvégpont a befejezés után átirányításra kerül, használja a HTTP 303-at (lásd: Egyéb). A 303 arra utasítja az ügyfelet, hogy az eredeti kérési módszertől függetlenül küldjön GET kérést az átirányítási URL-címre. Ez a viselkedés a megfelelő szemantika ehhez a mintához, mert az ügyfél egy különálló eredményerőforrást kér le, nem pedig az eredeti műveletet. A HTTP 302 (Found) nem garantálja a metódus módosítását. Egyes ügyfelek visszajátszják az eredeti metódust átirányításkor. Ez a viselkedés nem kívánt mellékhatásokat okozhat, például ismétlődő POST-kéréseket.

  • Miután a kiszolgáló sikeresen feldolgozta a kérést, a fejléc által megadott erőforrás Location egy HTTP-állapotkódot ad vissza, például 200, 201 (Létrehozva) vagy 204 -et (Nincs tartalom).

  • Ha a feldolgozás során hiba történik, őrizze meg a hibát a fejléc által Location megadott erőforrás URL-címén, és adjon vissza egy 4xx állapotkódot az erőforrásból, amely megfelel a hibának. Használjon strukturált hibaformátumot, például RFC 9457 (HTTP API-k problémaadatai) annak érdekében, hogy az ügyfelek programozott módon elemezhetik és kezelhetik a hibákat.

  • Az állapoterőforrás és a tárolt eredmények tárolást és számítást használnak fel. Adjon meg egy adatmegőrzési szabályzatot, amely ésszerű idő elteltével törli őket. Ha tájékoztatni szeretné az ügyfeleket a megőrzési időszakról, hozzáadhat egy fejlécet Expires az állapotválaszhoz.

  • A megoldások nem minden módon implementálják ezt a mintát, és egyes szolgáltatások további vagy alternatív fejléceket is tartalmaznak. Azure Resource Manager például ennek a mintának egy módosított változatát használja. További információ: Resource Manager aszinkron műveletek.

  • 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. A Logic Apps például natív módon támogatja ezt a mintát, és használhatja integrációs rétegként egy aszinkron API és egy szinkron hívásokat végző ügyfél között. További információ: Aszinkron kérés-válasz viselkedés a Logic Appsben.

  • Ha módot szeretne biztosítani az ügyfelek számára egy hosszú ideig futó kérelem megszakítására, tegye közzé a DELETE műveletet az állapotvégpont-erőforráson. Ennek a kérésnek egy lemondási utasítást kell továbbítania a háttérfeldolgozó összetevőnek. Miután a back-end kezeli a törlést, frissítenie kell az állapoterőforrást, hogy tükrözze a törölt állapotot. Ez a folyamat segít megakadályozni, hogy a hiányos munka határozatlan ideig használja fel az erőforrásokat. Annak meghatározása, hogy a művelet támogatja-e a részleges visszaállítást, vagy kompenzáló tranzakciót igényel.

  • Megkövetelheti az ügyfelektől, hogy adjanak meg egy idempotenciakulcsot, például egy Idempotency-Key kérelemfejlécben, amikor elküldik a kezdeti kérést. Ha a háttérrendszer duplikált kulcsot kap, a második munkaelem lekérdezése helyett a meglévő állapoterőforrást kell visszaadnia. Ez a megközelítés védelmet nyújt azokkal a hálózati hibákkel szemben, amelyek miatt az ügyfél újrapróbálkozott egy POST-et, amelyet a kiszolgáló már elfogadott. Ez különösen fontos ebben a mintában, mert az ügyfél nem tud különbséget tenni az elveszett válasz és a soha nem kapott kérés között.

Megjegyzés:

Ez a minta a HTTP-lekérdezést írja le, amelyben az ügyfél rendszeresen új kéréseket ad ki az állapot ellenőrzésére. Hosszú lekérdezés esetén az ügyfél kérést küld, és a kiszolgáló nyitva tartja a kapcsolatot, amíg új adatok nem érhetők el, vagy időtúllépés történik. A hosszú lekérdezések csökkentik a válasz késését az időszakos lekérdezésekhez képest, de összetettebbé teszi a kapcsolatkezelést és az időtúllépéseket.

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

Használja ezt a mintát, ha:

  • Ügyféloldali kódokkal, például böngészőalkalmazásokkal dolgozik, és ezek a korlátozások megnehezítik a visszahívási végpontok biztosítását, vagy a hosszan futó kapcsolatok túl összetettek.

  • Olyan szolgáltatást hívhat meg, amely csak a HTTP protokollt használja, és a visszatérési szolgáltatás nem tud visszahívásokat küldeni az ügyféloldali tűzfalkorlátozások miatt.

  • Olyan számítási feladatokkal integrálható, amelyek nem támogatják a modern visszahívási mechanizmusokat, 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 Azure Event Grid.

  • A válaszoknak valós időben kell streamelnie az ügyfélnek. Fontolja meg Server-Sent események (SSE-k) használatát, amelyek egyszerű, HTTP-natív, egyirányú leküldéses csatornát biztosítanak kiszolgálóról ügyfélre anélkül, hogy az ügyfélnek le kellene kérdeznie.

  • Az ügyfélnek számos eredményt kell összegyűjtenie, és fontos ezeknek az eredményeknek a késése. Fontolja meg inkább egy üzenetközvetítő használatát.

  • Kiszolgálóoldali állandó hálózati kapcsolatok, például WebSockets vagy SignalR érhetők el. Ezekkel a kapcsolatokkal értesítheti a hívót az eredményről.

  • A hálózati kialakítás támogatja a nyitott portokat az aszinkron visszahívások vagy webhookok fogadásához.

Munkaterhelés tervezése

Az tervezőknek értékelniük kell, hogyan használhatják az Aszinkron Request-Reply mintát a számítási feladat kialakításában a Azure Well-Architected keretrendszer pilléreiben szereplő célok és alapelvek kezelésére.

Alappillé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 . A válaszképességet és a méretezhetőséget úgy javíthatja, hogy leválasztja a kérési és válaszfázisokat az olyan folyamatokról, amelyek nem igényelnek azonnali választ. Az aszinkron megközelítés növeli az egyidejűséget, és lehetővé teszi, hogy a kiszolgáló úgy ütemezze a munkát, ahogy a kapacitás elérhetővé válik.

- 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 a más pillérek céljaival való kompromisszumokat, amelyeket ez a minta bevezethet.

Example

Az alábbi kód egy olyan alkalmazás részleteit mutatja be, amelyek Azure Functions használnak a minta implementálásához. Ez a megoldás három funkcióval rendelkezik:

  • Az aszinkron API-végpont
  • Az állapotvégpont
  • Egy háttérfüggvény, amely a sorba állított munkaelemeket veszi át, és futtatja őket.

Az aszinkron válaszminta struktúrájának diagramja a Functionsben.

GitHub logo. Ez a minta a GitHub webhelyen érhető el.

Az implementáció felügyelt identitást használ a Azure Service Bus és Azure Blob Storage való hitelesítéshez, ami elkerüli a kapcsolati sztringek vagy fiókkulcsok tárolását. A függőségeket az Program.cs segítségével regisztráljuk, és az DefaultAzureCredential elsődleges konstruktorokon keresztül injektáljuk.

AsyncProcessingWorkAcceptor függvény

A AsyncProcessingWorkAcceptor függvény implementál egy végpontot, amely fogadja az ügyfélalkalmazások munkáját, és a feldolgozáshoz leküldi:

  • A függvény létrehoz egy kérésazonosítót, és metaadatként hozzáadja az üzenetsor-üzenethez.

  • A HTTP-válasz egy Location állapotvégpontra mutató fejlécet és egy Retry-After lekérdezési időközt javasoló fejlécet tartalmaz. A kérelem azonosítója megjelenik az URL-elérési úton.

public class AsyncProcessingWorkAcceptor(ServiceBusClient _serviceBusClient)
{
    [Function("AsyncProcessingWorkAcceptor")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
        [FromBody] CustomerPOCO customer)
    {
        if (string.IsNullOrEmpty(customer.id) || string.IsNullOrEmpty(customer.customername))
        {
            return new BadRequestResult();
        }

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

        string statusUrl = $"https://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{requestId}";

        var messagePayload = JsonConvert.SerializeObject(customer);
        var message = new ServiceBusMessage(messagePayload);
        message.ApplicationProperties.Add("RequestGUID", requestId);
        message.ApplicationProperties.Add("RequestSubmittedAt", DateTime.UtcNow);
        message.ApplicationProperties.Add("RequestStatusURL", statusUrl);
        var sender = _serviceBusClient.CreateSender("outqueue");

        await sender.SendMessageAsync(message);

        req.HttpContext.Response.Headers["Retry-After"] = "5";

        return new AcceptedResult(statusUrl, null);
    }
}

AsyncProcessingBackgroundWorker függvény

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

public class AsyncProcessingBackgroundWorker(BlobContainerClient _blobContainerClient)
{
    [Function("AsyncProcessingBackgroundWorker")]
    public async Task Run(
        [ServiceBusTrigger("outqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
    {
        // Perform an action against the blob data source for the async readers to check against.
        // This is where your service worker processing will be performed.

        var requestGuid = message.ApplicationProperties["RequestGUID"].ToString();
        string blobName = $"{requestGuid}.blobdata";

        var blobClient = _blobContainerClient.GetBlobClient(blobName);
        using (MemoryStream memoryStream = new MemoryStream())
        using (StreamWriter writer = new StreamWriter(memoryStream))
        {
            writer.Write(message.Body.ToString());
            writer.Flush();
            memoryStream.Position = 0;

            await blobClient.UploadAsync(memoryStream, overwrite: true);
        }
    }
}

AsyncOperationStatusChecker függvény

A AsyncOperationStatusChecker függvény implementálja az állapotvégpontot. Ez a függvény ellenőrzi a kérés állapotát:

public class AsyncOperationStatusChecker(ILogger<AsyncOperationStatusChecker> _logger)
{
    [Function("AsyncOperationStatusChecker")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "RequestStatus/{requestId}")] HttpRequest req,
        [BlobInput("data/{requestId}.blobdata", Connection = "DataStorage")] BlockBlobClient inputBlob, string requestId)
    {
        OnCompleteEnum OnComplete = Enum.Parse<OnCompleteEnum>(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect");
        OnPendingEnum OnPending = Enum.Parse<OnPendingEnum>(req.Query["OnPending"].FirstOrDefault() ?? "OK");

        _logger.LogInformation("Received status request for {RequestId} - OnComplete {OnComplete} - OnPending {OnPending}",
            requestId, OnComplete, OnPending);

        // Check whether the blob exists.
        if (await inputBlob.ExistsAsync())
        {
            // If the blob exists, the function uses the OnComplete parameter to determine the next action.
            return await OnCompleted(OnComplete, inputBlob, requestId, req);
        }
        else
        {
            // If the blob doesn't exist, the function uses the OnPending parameter to determine the next action.
            switch (OnPending)
            {
                case OnPendingEnum.OK:
                    {
                        // Return an HTTP 200 status code.
                        return new OkObjectResult(new { status = "In progress", Location = rqs });
                    }

                case OnPendingEnum.Synchronous:
                    {
                        // Long polling example: hold the connection open and check for completion
                        // using exponential backoff. Time out after approximately one minute.
                        int backoff = 250;

                        while (!await inputBlob.ExistsAsync() && backoff < 64000)
                        {
                            _logger.LogInformation("Synchronous mode {RequestId} - retrying in {Backoff} ms", requestId, backoff);
                            backoff = backoff * 2;
                            await Task.Delay(backoff);
                        }

                        if (await inputBlob.ExistsAsync())
                        {
                            _logger.LogInformation("Synchronous mode {RequestId} - completed after {Backoff} ms", requestId, backoff);
                            return await OnCompleted(OnComplete, inputBlob, requestId, req);
                        }
                        else
                        {
                            _logger.LogInformation("Synchronous mode {RequestId} - NOT FOUND after timeout {Backoff} ms", requestId, backoff);
                            return new NotFoundResult();
                        }
                    }

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

    private async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string requestId, HttpRequest req)
    {
        switch (OnComplete)
        {
            case OnCompleteEnum.Redirect:
                {
                    // Generate a user delegation SAS URI by using managed identity credentials.
                    BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient();
                    var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(7));

                    // Return 303 (See Other) to redirect the client to the result resource.
                    // GenerateUserDelegationSasUri is a custom helper. See the full implementation on GitHub.
                    req.HttpContext.Response.Headers.Location = GenerateUserDelegationSasUri(inputBlob, userDelegationKey);
                    return new StatusCodeResult(StatusCodes.Status303SeeOther);
                }

            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