Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Entkoppeln Sie die Backend-Verarbeitung von einem Frontend-Host, wenn die Backend-Verarbeitung asynchron betrieben werden muss, das Frontend jedoch eine klare Antwort benötigt.
Kontext und Problem
In der modernen Anwendungsentwicklung sind Clientanwendungen häufig von Remote-APIs abhängig, um Geschäftslogik und Verfassenfunktionen bereitzustellen. Viele Anwendungen führen Code in einem Webbrowser aus, und andere Umgebungen hosten auch Clientcode. Die APIs können sich direkt auf die Anwendung beziehen oder als gemeinsame Dienste von einem externen Dienst ausgeführt werden. Die meisten API-Aufrufe verwenden HTTP oder HTTPS und folgen der REST-Semantik.
In den meisten Fällen reagieren APIs für eine Clientanwendung in etwa 100 Millisekunden (ms) oder weniger. Viele Faktoren können sich auf die Antwortlatenz auswirken:
- Hoststapel der Anwendung
- Sicherheitskomponenten
- Der relative geografische Standort des Anrufers und des Back-Ends
- Netzwerkinfrastruktur
- Aktuelle Last
- Die Größe der Anforderungsnutzlast
- Verarbeitungswarteschlangenlänge
- Die Zeit für das Back-End zum Verarbeiten der Anforderung
Diese Faktoren können der Antwort Latenz hinzufügen. Sie können einige Faktoren mindern, indem Sie das Back-End skalieren. Andere Faktoren, z. B. die Netzwerkinfrastruktur, liegen außerhalb der Kontrolle des Anwendungsentwicklers. Die meisten APIs reagieren schnell genug, damit die Antwort über dieselbe Verbindung zurückgegeben wird. Anwendungscode kann einen synchronen API-Aufruf auf eine nicht blockierende Weise durchführen, um die Darstellung der asynchronen Verarbeitung zu ermöglichen. Wir empfehlen diesen Ansatz für Eingabe- und Ausgabevorgänge (I/O)-gebundene Vorgänge.
In einigen Szenarien erledigt das Back-End Aufgaben, die lang andauernd sind und einige Sekunden benötigen. In anderen Szenarien führt das Back-End lang laufende Hintergrundarbeit für Minuten oder sogar über längere Zeiträume hinweg durch. In diesen Fällen können Sie nicht warten, bis die Arbeit abgeschlossen ist, bevor Sie eine Antwort senden. Diese Situation kann ein Problem für synchrone Anforderungs-Antwort-Muster verursachen. Anleitungen zum Entwerfen der Back-End-Verarbeitung finden Sie unter "Hintergrundaufträge".
Einige Architekturen lösen dieses Problem mithilfe eines Nachrichtenbrokers, um Anforderungs- und Antwortphasen zu trennen. Viele Systeme erreichen diese Trennung durch das Warteschlangenbasierte Lastenausgleichsmuster. Durch diese Trennung kann der Clientprozess und die Back-End-API unabhängig voneinander skaliert werden. Es führt auch zu einer zusätzlichen Komplexität, wenn der Client eine Erfolgsbenachrichtigung erfordert, da dieser Schritt auch asynchron werden muss.
Viele der gleichen Überlegungen, die für Clientanwendungen gelten, gelten auch für Server-zu-Server-REST-API-Aufrufe in verteilten Systemen, z. B. in einer Microservices-Architektur.
Lösung
Eine Lösung für dieses Problem ist die Verwendung von HTTP-Polling. Das Abfrageverfahren eignet sich gut für clientseitigen Code, wenn Callback-Endpunkte nicht verfügbar sind oder wenn langanhaltende Verbindungen zu viel Komplexität verursachen. Auch wenn Rückrufe möglich sind, können die zusätzlichen Bibliotheken und Dienste, die sie benötigen, die Komplexität erhöhen.
Die folgenden Schritte beschreiben die Lösung:
Die Clientanwendung ruft die API synchron auf, um einen lang andauernden Vorgang auf dem Back-End auszulösen.
Die API reagiert so schnell wie möglich synchron. Er gibt einen HTTP 202 (Accepted)-Statuscode zurück, um zu bestätigen, dass er die Anforderung zur Verarbeitung empfangen hat.
Hinweis
Die API sollte die Anforderung und die auszuführende Aktion überprüfen, bevor sie den langen Prozess startet. Wenn die Anforderung ungültig ist, antworten Sie sofort mit einem Fehlercode wie HTTP 400 (ungültige Anforderung).
Die Antwort enthält einen Standortverweis, der auf einen Endpunkt verweist, den der Client abfragen kann, um das Ergebnis des lange ausgeführten Vorgangs zu überprüfen.
Die API entlädt die Verarbeitung in eine andere Komponente, z. B. eine Nachrichtenwarteschlange.
Für jeden erfolgreichen Aufruf des Statusendpunkts gibt der Endpunkt HTTP 200 (OK) zurück. Während die Arbeit ausgeführt wird, gibt der Statusendpunkt eine Ressource zurück, die diesen Zustand angibt. Der Statusantworttext sollte genügend Informationen für den Client enthalten, um den aktuellen Status des Vorgangs zu verstehen.
Wenn die Arbeit abgeschlossen ist, gibt der Statusendpunkt eine Ressource zurück, die den Abschluss angibt oder an eine andere Ressourcen-URL umleitet. Wenn der asynchrone Vorgang beispielsweise eine neue Ressource erstellt, leitet der Statusendpunkt an die URL für diese Ressource um.
Das folgende Diagramm zeigt einen typischen Fluss.
Der Client sendet eine Anforderung und empfängt die Antwort „HTTP 202 (Akzeptiert)“.
Der Client sendet eine HTTP-GET-Anforderung an den Statusendpunkt. Dieser Aufruf gibt HTTP 200 zurück, da die Arbeit aussteht.
Irgendwann wird die Arbeit abgeschlossen, und der Statusendpunkt gibt HTTP 303 (Siehe Andere) zurück, um zur Ressource umzuleiten.
Der Client ruft die Ressource an der angegebenen URL ab.
Probleme und Überlegungen
Berücksichtigen Sie die folgenden Punkte, wenn Sie sich für die Implementierung dieses Musters entscheiden:
Es gibt mehrere Möglichkeiten, dieses Muster über HTTP zu implementieren, und upstream-Dienste verwenden nicht immer dieselbe Semantik. Beispielsweise verwenden einige Implementierungen keinen separaten Statusendpunkt. Stattdessen fragt der Client die Zielressourcen-URL direkt ab und empfängt HTTP 404 (Nicht gefunden), bis die Ressource erstellt wird. Diese Antwort wird generiert, da die Ressource noch nicht vorhanden ist. Dieser Ansatz kann jedoch unklar sein, da ungültige Anforderungs-IDs auch HTTP 404 zurückgeben. Ein dedizierter Statusendpunkt, der HTTP 200 mit einem Statustext zurückgibt, wie in diesem Muster beschrieben, vermeidet diese Verwirrung.
Eine HTTP 202-Antwort gibt an, wo der Client abruft und wie oft. Sie sollte die folgenden Kopfzeilen enthalten.
Header Beschreibung Hinweise LocationEine URL, die der Client für einen Antwortstatus abruft Diese URL kann ein SAS-Token (Shared Access Signature) sein. Das Valet Key-Muster eignet sich gut, wenn für diesen Speicherort die Zugriffssteuerung erforderlich ist. Das Muster gilt auch, wenn die Antwortabfragung zu einem anderen Back-End wechseln muss. Retry-AfterGeschätzte Abschlusszeit für die Verarbeitung Dieser Header hilft Abfrage-Clients, zu viele Anfragen an das Back-End zu vermeiden. Erwägen Sie das erwartete Clientverhalten, wenn Sie diese Antwort entwerfen. Ein Client, den Sie steuern, kann diesen Antwortwerten genau folgen. Clients, die von anderen erstellt werden, einschließlich solcher, die mithilfe von No-Code- und Low-Code-Tools wie Azure Logic Apps erstellt wurden, können ihre eigene Behandlung für HTTP 202 anwenden.
Erwägen Sie die Einbeziehung der folgenden Felder in die Statusendpunktantwort.
Feld Beschreibung Hinweise statusDer aktuelle Status des Vorgangs, z. B. "Ausstehend", "Läuft", "Erfolgreich", "Fehlgeschlagen" oder "Abgebrochen" Verwendet einen konsistenten, dokumentierten Satz von Terminal- und Nichtterminalwerten. createdAtDie Zeit, zu der der Vorgang angenommen wurde Unterstützt Clients beim Erkennen veralteter oder abgebrochener Vorgänge lastUpdatedAtDie Uhrzeit, zu der der Status zuletzt aktualisiert wurde Hilft Kunden, zwischen stillstehenden und laufenden Vorgängen zu unterscheiden. percentCompleteEine optionale Statusanzeige Nützlich, wenn das Back-End den Fortschritt schätzen kann errorEin strukturiertes Fehlerobjekt, wenn der Status fehlgeschlagen ist Berücksichtigen Sie aus Gründen der Konsistenz die Verwendung des RFC 9457-Formats . Möglicherweise müssen Sie einen Verarbeitungs-Proxy verwenden, um die Antwortheader oder Nutzlast anzupassen, abhängig von den zugrunde liegenden Diensten, die Sie verwenden.
Wenn der Statusendpunkt nach Abschluss umgeleitet wird, verwenden Sie HTTP 303 (Siehe Weitere Informationen). Ein 303 weist den Client an, unabhängig von der ursprünglichen HTTP-Anforderungsmethode eine GET-Anforderung an die Umleitungs-URL zu senden. Dieses Verhalten ist die richtige Semantik für dieses Muster, da der Client eine eindeutige Ergebnisressource abruft und den ursprünglichen Vorgang nicht erneut übermittelt. HTTP 302 (Found) garantiert keine Methodenänderung. Einige Clients wiederholen die ursprüngliche Methode bei Umleitung. Dieses Verhalten kann unbeabsichtigte Nebenwirkungen verursachen, z. B. doppelte POST-Anforderungen.
Nachdem der Server die Anforderung erfolgreich verarbeitet hat, gibt die vom Header angegebene Ressource
Locationeinen HTTP-Statuscode wie 200, 201 (Erstellt) oder 204 (Kein Inhalt) zurück.Wenn während der Verarbeitung ein Fehler auftritt, speichern Sie den Fehler unter der Ressourcen-URL, die der
LocationHeader angibt, und geben einen 4xx-Statuscode von der Ressource zurück, die dem Fehler entspricht. Verwenden Sie ein strukturiertes Fehlerformat, z. B . RFC 9457 (Problemdetails für HTTP-APIs), damit Clients Fehler programmgesteuert analysieren und behandeln können.Die Statusressource und alle gespeicherten Ergebnisse verbrauchen Speicher und Berechnung. Definieren Sie eine Aufbewahrungsrichtlinie, um sie nach einem angemessenen Zeitraum zu entfernen. Um Clients über das Aufbewahrungsfenster zu informieren, können Sie der Statusantwort einen
ExpiresHeader hinzufügen.Lösungen implementieren dieses Muster nicht auf die gleiche Weise, und einige Dienste enthalten zusätzliche oder alternative Header. Beispielsweise verwendet Azure Resource Manager eine geänderte Variante dieses Musters. Weitere Informationen finden Sie unter den asynchronen Vorgängen des Resource Managers.
Dieses Muster wird von Legacy-Clients möglicherweise nicht unterstützt. In diesem Fall müssen Sie möglicherweise eine Fassade über der asynchronen API platzieren, um die asynchrone Verarbeitung vor dem ursprünglichen Client auszublenden. Logik-Apps unterstützen dieses Muster beispielsweise nativ, und Sie können es als Integrationsebene zwischen einer asynchronen API und einem Client verwenden, der synchrone Aufrufe vorgibt. Weitere Informationen finden Sie unter "Asynchrones Anforderungsantwortverhalten" in Logik-Apps.
Um Clients die Möglichkeit zu bieten, eine lange laufende Anforderung abzubrechen, stellen Sie einen DELETE-Vorgang für die Statusendpunktressource bereit. Diese Anforderung sollte eine Abbruchanweisung an die Back-End-Verarbeitungskomponente weiterleiten. Nachdem das Back-End die Stornierung verarbeitet hat, sollte die Statusressource aktualisiert werden, um den stornierten Zustand widerzuspiegeln. Dieser Vorgang verhindert, dass unvollständige Arbeiten ressourcen unbegrenzt verbrauchen. Ermitteln Sie, ob der Vorgang partielles Rollback unterstützt oder eine Ausgleichstransaktion erfordert.
Sie können festlegen, dass Clients einen idempotenten Schlüssel angeben, z. B. in einem
Idempotency-KeyAnforderungsheader, wenn sie die ursprüngliche Anforderung übermitteln. Wenn das Back-End einen doppelten Schlüssel empfängt, sollte die vorhandene Statusressource zurückgegeben werden, anstatt eine zweite Arbeitsaufgabe aufzugeben. Dieser Ansatz schützt vor Netzwerkfehlern, die dazu führen, dass der Client einen POST erneut versucht, den der Server bereits akzeptiert hat. Es ist besonders wichtig in diesem Muster, da der Client nicht zwischen einer verlorenen Antwort und einer Anforderung unterscheiden kann, die nie empfangen wurde.
Hinweis
Dieses Muster beschreibt die HTTP-Abrufung, bei der der Client regelmäßig neue Anforderungen ausgibt, um den Status zu überprüfen. Bei langen Abfragen sendet der Client eine Anforderung, und der Server hält die Verbindung geöffnet, bis neue Daten verfügbar sind oder ein Timeout auftritt. Long Polling reduziert die Antwortlatenz im Vergleich zu regelmäßigen Abfragen, führt jedoch zu einer Komplexität bei der Verbindungsverwaltung und den Timeouts.
Wann Sie dieses Muster verwenden sollten
Verwenden Sie dieses Muster in folgenden Fällen:
Sie arbeiten mit clientseitigem Code, wie z. B. Browseranwendungen, und aufgrund dieser Einschränkungen sind Callback-Endpunkte schwer bereitzustellen, oder lang andauernde Verbindungen bringen zu viel Komplexität mit sich.
Sie rufen einen Dienst auf, der nur das HTTP-Protokoll verwendet und der Rückgabedienst aufgrund von Firewalleinschränkungen auf clientseitiger Seite keine Rückrufe senden kann.
Sie integrieren sich mit Workloads, die moderne Rückrufmechanismen wie WebSockets oder Webhooks nicht unterstützen.
Dieses Muster ist möglicherweise nicht geeignet, wenn:
Sie können stattdessen einen Dienst verwenden, der für asynchrone Benachrichtigungen erstellt wurde, z. B. Azure Event Grid.
Antworten müssen in Echtzeit an den Client gestreamt werden. Erwägen Sie die Verwendung von Server-Sent Events (SSEs), die einen einfachen, http-nativen, unidirektionalen Pushkanal vom Server zum Client bereitstellen, ohne dass der Client abfragen muss.
Der Client muss viele Ergebnisse sammeln, und die Latenz dieser Ergebnisse ist wichtig. Erwägen Sie stattdessen die Verwendung eines Nachrichtenbrokers.
Serverseitige persistente Netzwerkverbindungen wie WebSockets oder SignalR sind verfügbar. Sie können diese Verbindungen verwenden, um den Anrufer über das Ergebnis zu benachrichtigen.
Das Netzwerkdesign unterstützt offene Ports zum Empfangen asynchroner Rückrufe oder Webhooks.
Workloadentwurf
Ein Architekt sollte bewerten, wie er das Muster "Asynchrones Request-Reply" im Entwurf seiner Arbeitsauslastung verwenden kann, um die Ziele und Prinzipien zu berücksichtigen, die in den Azure Well-Architected Framework-Säulen behandelt werden.
| Säule | So unterstützt dieses Muster die Säulenziele |
|---|---|
| Performance Efficiency hilft Ihrem Workload durch Optimierungen bei Skalierung, Daten und Code, die Anforderungen effizient zu erfüllen . | Sie verbessern die Reaktionsfähigkeit und Skalierbarkeit, indem Sie die Anforderungs- und Antwortphasen für Prozesse entkoppeln, die keine sofortige Antwort erfordern. Ein asynchroner Ansatz erhöht die Parallelität und ermöglicht es dem Server, die Arbeit zu planen, sobald die Kapazität verfügbar wird. - PE:05 Skalierung und Partitionierung - PE:07-Code und -Infrastruktur |
Wie bei jeder Entwurfsentscheidung sollten Sie Abwägungen im Hinblick auf die Ziele der anderen Säulen vornehmen, die dieses Muster einführen könnte.
Beispiel
Der folgende Code zeigt Auszüge aus einer Anwendung, die Azure Functions verwendet, um dieses Muster zu implementieren. Diese Lösung hat drei Funktionen:
- Der asynchrone API-Endpunkt
- Der Statusendpunkt
- Eine Back-End-Funktion, die Auftragsobjekte aus der Warteschlange verarbeitet und ausführt.
Dieses Beispiel ist auf GitHub verfügbar.
Die Implementierung verwendet verwaltete Identität zur Authentifizierung mit Azure Service Bus und Azure Blob Storage, wodurch keine Verbindungszeichenfolgen oder Kontoschlüssel gespeichert werden. Abhängigkeiten werden mit Program.cs registriert und über primäre Konstruktoren DefaultAzureCredential injiziert.
AsyncProcessingWorkAcceptor-Funktion
Die AsyncProcessingWorkAcceptor Funktion implementiert einen Endpunkt, der Arbeit von einer Clientanwendung akzeptiert und für die Verarbeitung queuesiert:
Die Funktion generiert eine Anforderungs-ID und fügt sie der Warteschlangennachricht als Metadaten hinzu.
Die HTTP-Antwort enthält einen
LocationHeader, der auf einen Statusendpunkt verweist, und einenRetry-AfterHeader, der ein Abrufintervall vorschlägt. Die Anforderungs-ID wird im URL-Pfad angezeigt.
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-Funktion
Die AsyncProcessingBackgroundWorker Funktion liest den Vorgang aus der Warteschlange, verarbeitet ihn basierend auf der Nachrichtennutzlast und schreibt das Ergebnis in ein Speicherkonto.
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-Funktion
Die AsyncOperationStatusChecker-Funktion implementiert den Statusendpunkt. Diese Funktion überprüft den Status der Anforderung:
Wenn die Anforderung abgeschlossen ist, gibt die Funktion HTTP 303 (Siehe Andere) zurück und leitet den Client an eine Valet-Schlüssel-URL für das Ergebnis um.
Wenn die Anforderung aussteht, gibt die Funktion einen HTTP 200-Code zurück, der den aktuellen Zustand enthält.
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
}