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-kra támaszkodnak, 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.

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 soralapú 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 ellenőrzi 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égpont sikeres hívásához a végpont HTTP 200 (OK) értéket ad vissza. Amíg a munka folyamatban van, a végpont egy olyan erőforrást ad vissza, amely az állapotot jelzi. Amikor a munka befejeződött, a vé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.

Egy folyamatábra, amely egy ügyfelet, egy API-végpontot, egy állapotvégpontot és egy erőforrás URI-t jelenít meg. Az ügyfél POST kérést küld az API-végpontnak, amely HTTP 202-t ad vissza. Az ügyfél ezután ismétlődő GET kéréseket küld az állapotvégpontnak. Az első válasz a HTTP 200-at, a későbbi válasz pedig a HTTP 302 -t (Found) adja vissza. Az ügyfél egy GET kéréssel követi az átirányítást az erőforrás URI-jának, amely HTTP 200-et ad vissza. A diagram egy aszinkron kérésmintát jelenít meg lekérdezéssel és a befejezett erőforrásra való végleges átirányítással.

  1. Az ügyfél kérést küld, és HTTP 202-választ kap.

  2. Az ügyfél HTTP GET kérést küld az állapotvégpontnak. A munka függőben van, ezért ez a hívás HTTP 200-et ad vissza.

  3. A munka befejeződött, és az állapotvégpont a HTTP 302 (Found) értéket adja vissza 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. A legtöbb szolgáltatás például a HTTP 404 -et (nem található) adja vissza a GET metódusból, ha egy távoli folyamat nem fejeződik be a HTTP 202 helyett. A standard REST szemantika szerint a HTTP 404 a megfelelő válasz, mert a hívás eredménye még nem létezik.

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

    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.

  • 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 a státusz végpont a befejezés után átirányít, akkor a HTTP 302 vagy a HTTP 303 (Lásd másik) érvényes visszatérési kódok a támogatott szemantikától függően.

  • Miután a kiszolgáló feldolgozta a kérést, a fejléc által megadott erőforrás 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 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.

  • 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 feldolgozási proxyt 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ó: 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, 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 régi architektúrákkal 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.

  • Az ügyfélnek számos eredményt kell gyűjtenie, és fontos ezeknek az eredményeknek a késése. Fontolja meg inkább a service bus-mintá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.

Az 1. lépésben egy ügyfél egy API-t hív meg. A 2. lépésben az API egy üzenetet helyez egy üzenetsorba. A 3. lépésben az API egy állapotvégpontot ad vissza az ügyfélnek. A 4. lépésben egy dolgozó megkapja az üzenetet az üzenetsorból. Az 5. lépésben a feldolgozó feldolgozza az üzenetet, és az eredményt a Blob Storage-ba írja. A 6. lépésben az ügyfél meghívja az állapotvégpontot. A 7. lépésben az állapotvégpont ellenőrzi az eredményt a Blob Storage-ban.

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

AsyncProcessingWorkAcceptor függvény

A 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 állapotvégpontra mutató 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> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, [FromBody] CustomerPOCO customer)
        {
            if (string.IsNullOrEmpty(customer.id) || string.IsNullOrEmpty(customer.customername))
            {
                return new BadRequestResult();
            }

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

            string scheme = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Development" ? "http" : "https";
            var rqs = $"{scheme}://{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);
            var sender = _serviceBusClient.CreateSender("outqueue");

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

AsyncProcessingBackgroundWorker függvény

A 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(nameof(AsyncProcessingBackgroundWorker))]
        public async Task Run([ServiceBusTrigger("outqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
        {
            var requestGuid = message.ApplicationProperties["RequestGUID"].ToString();
            string blobName = $"{requestGuid}.blobdata";

            await _blobContainerClient.CreateIfNotExistsAsync();

            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 függvény implementálja az állapotvégpontot. Ez a függvény ellenőrzi a kérés állapotát:

  • Ha a kérés befejeződött, a függvény egy valet kulcsot ad vissza a válaszhoz, vagy azonnal átirányítja a hívást a valet-key URL-címre.

  • Ha a kérelem függőben van, a függvény egy HTTP 200-kódot ad vissza, amely az aktuális állapotot tartalmazza.

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

            _logger.LogInformation($"C# HTTP trigger function processed a request for status on {thisGUID} - OnComplete {OnComplete} - OnPending {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, thisGUID);
            }
            else
            {
                // If the blob doesn't exist, the function uses the OnPending parameter to determine the next action.
                string scheme = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Development" ? "http" : "https";
                string rqs = $"{scheme}://{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 back-off period reaches one minute.
                            int backoff = 250;

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

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

                    default:
                        {
                            throw new InvalidOperationException($"Unexpected value: {OnPending}");
                        }
                }
            }
        }
        private async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string thisGUID)
        {
            switch (OnComplete)
            {
                case OnCompleteEnum.Redirect:

                    {
                        // The typical way to generate a shared access signature token in code requires the storage account key.
                        // If you need to use a managed identity to control access to your storage accounts in code, which is a recommended best practice, you should do so when possible.
                        // In this scenario, you don't have a storage account key, so you need to find another way to generate the shared access signatures.
                        // To generate shared access signatures, use a user delegation shared access signature. This approach lets you sign the shared access signature by using Microsoft Entra ID credentials instead of the storage account key.

                        BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient();
                        var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(7));
                        // Redirect the shared access signature uniform resource identifier (URI) to blob storage.
                        return new RedirectResult(inputBlob.GenerateSASURI(userDelegationKey));
                    }

                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
    }

Az alábbi osztály egy bővítménymetódust biztosít, amelyet az állapot-ellenőrző a felhasználói delegálás közös hozzáférésű jogosultságkódjának egységes erőforrás-azonosítójának (URI) létrehozására használ az eredményblobhoz.

    public static class CloudBlockBlobExtensions
    {
        public static string GenerateSASURI(this BlockBlobClient inputBlob, UserDelegationKey userDelegationKey)
        {
            BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient();

            BlobSasBuilder blobSasBuilder = new BlobSasBuilder()
            {
                BlobContainerName = inputBlob.BlobContainerName,
                BlobName = inputBlob.Name,
                Resource = "b",
                StartsOn = DateTimeOffset.UtcNow,
                ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(10)
            };
            blobSasBuilder.SetPermissions(BlobSasPermissions.Read);

            var blobUriBuilder = new BlobUriBuilder(inputBlob.Uri)
            {
                Sas = blobSasBuilder.ToSasQueryParameters(userDelegationKey, blobServiceClient.AccountName)
            };

            // Generate the shared access signature on the blob, which sets the constraints directly on the signature.
            Uri sasUri = blobUriBuilder.ToUri();

            // Return the URI string for the container, including the shared access signature token.
            return sasUri.ToString();
        }
    }

Következő lépések

  • A lekérdezési műveleti minta használata hosszú ideig futó feladatokhoz
  • Aszinkron HTTP API-minta
  • Webes API tervezési
  • Háttérrendszerek és előtérrendszerek minta
  • Sofőrkulcs mintázat