Megosztás a következőn keresztül:


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. A munka még függőben van, ezért ez a hívás HTTP 200-et ad vissza.

  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. Ennek a válasznak van értelme, mert az erőforrás még nem létezik. Ez a megközelítés azonban nem egyértelmű, ha a rendszer a 404-et is visszaadja érvénytelen kérelemazonosítók esetén. A HTTP 200-at az ebben a mintában ismertetett állapottörzsgel visszaadó dedikált állapotvégpont elkerüli ezt a kétértelműséget.

  • 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 közös hozzáférésű jogosultságkód-jogkivonat. 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 Becslés arra vonatkozóan, hogy mikor fejeződik be a feldolgozás Ez a fejléc megakadályozza, hogy a lekérdezési ügyfelek túl sok kérést küldjenek a háttérbe.

    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, Folyamatban, Sikeres, Sikertelen vagy Törölt. Használjon konzisztens, dokumentált terminál- és nem terminálértékeket.
    createdAt A művelet elfogadásának időpontja. Segít az ügyfeleknek észlelni az elavult vagy elhagyatott műveleteket.
    lastUpdatedAt Az állapot legutóbbi frissítésének időpontja. Lehetővé teszi az ügyfelek számára, hogy megkülönböztessék az elakadt műveletet az aktívan folyamatban lévő művelettől.
    percentComplete Egy választható folyamatjelző. Akkor hasznos, ha a háttérrendszer képes értelmesen megbecsü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átszhatják az eredeti metódust átirányításkor, ami nem kívánt mellékhatásokat, például ismétlődő POST-kérelmeket okozhat.

  • 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. Olyan strukturált hibaformátumot használjon, mint az RFC 9457 (HTTP API-k problémaadatai), 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. Határozzon meg egy adatmegőrzési szabályzatot, amely törli az adatokat egy ésszerű időszak után, és fontolja meg, hogy a státuszválasz fejlécében közölje az ügyfelekkel a megőrzési időszakot Expires.

  • 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 az Azure Logic Appsben.

  • 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 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 háttérrendszer kezeli a lemondást, frissítenie kell az állapoterőforrást, hogy tükrözze a megszakított állapotot. Ez a folyamat segít megakadályozni, hogy a hiányos munka határozatlan ideig használja fel az erőforrásokat. Fontolja meg, hogy a művelet támogatja-e a részleges visszagörgetést, vagy a legjobb, ha kompenzáló tranzakcióként kezelendő.

  • Fontolja meg, hogy az ügyfeleknek idempotenciakulcsot kell megadniuk (például egy Idempotency-Key kérelemfejlécben) az első kérés elküldésekor. Ha a háttérrendszer duplikált kulcsot kap, a második munkaelem 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élnek nincs módja megkülönböztetni az elveszett választ egy olyan kéréstől, amely soha nem érkezett meg.

Megjegyzés:

Ez a minta a HTTP-lekérdezést írja le, ahol az ügyfél rendszeresen új kéréseket ad ki az állapot ellenőrzésére. A hosszú lekérdezés egy kapcsolódó, de eltérő technika: 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 a Server-Sent eseményeket (SSE), amelyek egyszerű, HTTP-natív, egyirányú adatátviteli csatornát biztosítanak a kiszolgálóról az ügyfélre, anélkül hogy az ügyfélnek le kellene kérdeznie.

  • Az ügyfélnek számos eredményt kell gyűjtenie, és fontos ezeknek az eredményeknek a késése. Fontolja meg inkább az üzenetközvetítő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 az Azure Service Bus és az Azure Blob Storage hitelesítéséhez, ami elkerüli a kapcsolati sztringek vagy fiókkulcsok tárolását. A függőségek regisztrálva vannak az Program.cs-ben a DefaultAzureCredential használatával és az elsődleges konstruktorokon keresztül injektálva.

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 tartalmaz egy Location fejlécet, amely egy állapotvégpontra mutat, és egy Retry-After lekérdezési időközre utaló fejlécet. 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 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 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 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