Freigeben über


Erstellen resilienter HTTP-Apps: Wichtige Entwicklungsmuster

Das Erstellen robuster HTTP-Apps, die bei vorübergehenden Fehlern wiederhergestellt werden können, ist eine gängige Anforderung. In diesem Artikel wird davon ausgegangen, dass Sie bereits die Einführung in die Entwicklung resilienter Apps gelesen haben, da in diesem Artikel auf die dort vermittelten Kernkonzepte aufgebaut wird. Um resiliente HTTP-Apps zu erstellen, bietet das NuGet-Paket Microsoft.Extensions.Http.Resilience Resilienzmechanismen speziell für den HttpClient. Dieses NuGet-Paket basiert auf der Microsoft.Extensions.Resilience-Bibliothek und Polly, einem beliebten Open-Source-Projekt. Weitere Informationen finden Sie unter Polly.

Erste Schritte

Um Resilienzmuster in HTTP-Apps zu verwenden, installieren Sie das NuGet-Paket Microsoft.Extensions.Http.Resilience.

dotnet add package Microsoft.Extensions.Http.Resilience --version 8.0.0

Weitere Informationen finden Sie unter dotnet add package oder Verwalten von Paketabhängigkeiten in .NET-Anwendungen.

Verbessern der Resilienz von HTTP-Clients

Um die Resilienz eines HttpClient zu verbessern, verketten Sie einen Aufruf für den Typ IHttpClientBuilder, der vom Aufruf einer der verfügbaren AddHttpClient-Methoden zurückgegeben wird. Weitere Informationen finden Sie unter IHttpClientFactory mit .NET.

Es gibt mehrere resilienzorientierte Erweiterungen. Einige orientieren sich an Standards und setzen verschiedene bewährte Methoden der Branche, während andere mehr Anpassungsmöglichkeiten bieten. Beim Verbessern der Resilienz sollten Sie nur einen Resilienzhandler hinzufügen und das Stapeln von Handlern vermeiden. Wenn Sie mehrere Resilienzhandler hinzufügen müssen, sollten Sie die Verwendung der AddResilienceHandler-Erweiterungsmethode in Betracht ziehen, mit der Sie Ihre Resilienzstrategien anpassen können.

Wichtig

Alle Beispiele in diesem Artikel basieren auf der AddHttpClient-API aus der Bibliothek Microsoft.Extensions.Http, die eine IHttpClientBuilder-Instanz zurückgibt. Die IHttpClientBuilder-Instanz wird verwendet, um den HttpClient zu konfigurieren und den Resilienzhandler hinzuzufügen.

Hinzufügen von Standardresilienzhandlern

Der Standardresilienzhandler verwendet mehrere gestapelte Resilienzstrategien mit Standardoptionen, um die Anforderungen zu senden und vorübergehende Fehler zu behandeln. Der Standardresilienzhandler wird durch Aufrufen der AddStandardResilienceHandler-Erweiterungsmethode für eine IHttpClientBuilder-Instanz hinzugefügt.

var services = new ServiceCollection();

var httpClientBuilder = services.AddHttpClient<ExampleClient>(
    configureClient: static client =>
    {
        client.BaseAddress = new("https://jsonplaceholder.typicode.com");
    });

Der vorangehende Code:

  • Erstellt eine ServiceCollection-Instanz.
  • Fügt dem Dienstcontainer einen HttpClient für den ExampleClient-Typ hinzu.
  • Konfiguriert den HttpClient für die Verwendung von "https://jsonplaceholder.typicode.com" als Basisadresse.
  • Erstellt den httpClientBuilder, der in den anderen Beispielen in diesem Artikel verwendet wird.

Ein Beispiel aus der Praxis würde auf dem Hosting basieren, z. B. das im Artikel Generischer .NET-Host beschriebene. Betrachten Sie das folgende aktualisierte Beispiel, in dem Sie das NuGet-Paket Microsoft.Extensions.Hosting verwenden:

using Http.Resilience.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

IHttpClientBuilder httpClientBuilder = builder.Services.AddHttpClient<ExampleClient>(
    configureClient: static client =>
    {
        client.BaseAddress = new("https://jsonplaceholder.typicode.com");
    });

Der vorangehende Code ähnelt dem Ansatz mit einer manuellen Erstellung einer ServiceCollection, nur dass stattdessen der Host.CreateApplicationBuilder() zum Erstellen eines Hosts verwendet wird, der die Dienste verfügbar macht.

Der ExampleClient ist wie folgt definiert:

using System.Net.Http.Json;

namespace Http.Resilience.Example;

/// <summary>
/// An example client service, that relies on the <see cref="HttpClient"/> instance.
/// </summary>
/// <param name="client">The given <see cref="HttpClient"/> instance.</param>
internal sealed class ExampleClient(HttpClient client)
{
    /// <summary>
    /// Returns an <see cref="IAsyncEnumerable{T}"/> of <see cref="Comment"/>s.
    /// </summary>
    public IAsyncEnumerable<Comment?> GetCommentsAsync()
    {
        return client.GetFromJsonAsAsyncEnumerable<Comment>("/comments");
    }
}

Der vorangehende Code:

  • Definiert einen ExampleClient-Typ mit einem Konstruktor, der einen HttpClient akzeptiert.
  • Macht eine GetCommentsAsync-Methode verfügbar, die eine GET-Anforderung an den /comments-Endpunkt sendet und die Antwort zurückgibt.

Der Comment-Typ ist wie folgt definiert:

namespace Http.Resilience.Example;

public record class Comment(
    int PostId, int Id, string Name, string Email, string Body);

Nachdem Sie ein IHttpClientBuilder (httpClientBuilder) erstellt und die Implementierung von ExampleClient und des entsprechenden Comment-Modells verstanden haben, können Sie sich das folgende Beispiel ansehen:

httpClientBuilder.AddStandardResilienceHandler();

Der obige Code fügt dem HttpClient den Standardresilienzhandler hinzu. Wie die meisten Resilienz-APIs gibt es Überladungen, mit denen Sie die Standardoptionen und angewandten Resilienzstrategien anpassen können.

Standardwerte für Standardresilienzhandler

In der Standardkonfiguration werden fünf Resilienzstrategien in der folgenden Reihenfolge verkettet (von äußerster Ebene bis zur innersten):

Auftrag Strategie Beschreibung Standardwerte
1 Ratenbegrenzung Die Ratenbegrenzungspipeline begrenzt die maximale Anzahl gleichzeitiger Anforderungen, die an die Abhängigkeit gesendet werden. Queue: 0
Zulassen: 1_000
2 Gesamttimeout Die Gesamtanforderungstimeout-Pipeline wendet ein generelles Timeout auf die Ausführung an, um sicherzustellen, dass die Anforderung (einschließlich Wiederholungsversuchen) den konfigurierten Grenzwert nicht überschreitet. Gesamttimeout: 30 s
3 Wiederholen Die Wiederholungspipeline wiederholt die Anforderung, falls die Abhängigkeit langsam ist oder einen vorübergehenden Fehler zurückgibt. Max. Wiederholungen: 3
Backoff: Exponential
Jitter verwenden: true
Verzögerung: 2 s
4 Trennschalter Die Sicherung blockiert die Ausführung, wenn zu viele direkte Fehler oder Timeouts erkannt werden. Fehlerverhältnis: 10 %
Min. Durchsatz: 100
Stichprobenentnahmedauer: 30 s
Unterbrechungsdauer: 5 s
5 Versuchstimeout Die Versuchstimeoutpipeline begrenzt die Dauer der einzelnen Anforderungen. Sie wird beim Überschreiten ausgelöst. Versuchstimeout: 10 s

Wiederholungen und Circuit Breaker

Die Strategien für Wiederholungen und Circuit Breaker verarbeiten sowohl eine Reihe bestimmter HTTP-Statuscodes als auch Ausnahmen. Betrachten Sie die folgenden HTTP-Statuscodes:

  • HTTP 500 und höher (Serverfehler)
  • HTTP 408 (Anforderungstimeout)
  • HTTP 429 (zu viele Anforderungen)

Darüber hinaus behandeln diese Strategien die folgenden Ausnahmen:

  • HttpRequestException
  • TimeoutRejectedException

Hinzufügen eines Standardhedginghandlers

Der Standardhedginghandler umschließt die Ausführung der Anforderung mit einem Standardhedgingverfahren. Das Hedging führt langsame Anforderungen parallel durch.

Rufen Sie die AddStandardHedgingHandler-Erweiterungsmethode auf, um den Standardhedginghandler zu verwenden. Im folgenden Beispiel wird der ExampleClient für die Verwendung des Standardhedginghandlers konfiguriert.

httpClientBuilder.AddStandardHedgingHandler();

Der obige Code fügt HttpClient den Standardhedginghandler hinzu.

Standardwerte für Standardhedginghandler

Beim Standardhedging wird ein Pool von Sicherungen verwendet, um sicherzustellen, dass fehlerhafte Endpunkte nicht für das Hedging genutzt werden. Standardmäßig basiert die Auswahl aus dem Pool auf der URL-Autorität (Schema + Host + Port).

Tipp

Es wird empfohlen, die Art der Strategieauswahl zu konfigurieren, indem Sie StandardHedgingHandlerBuilderExtensions.SelectPipelineByAuthority oder StandardHedgingHandlerBuilderExtensions.SelectPipelineBy für komplexere Szenarien aufrufen.

Der obige Code fügt IHttpClientBuilder den Standardhedginghandler hinzu. In der Standardkonfiguration werden fünf Resilienzstrategien in der folgenden Reihenfolge verkettet (von äußerster Ebene bis zur innersten):

Auftrag Strategie Beschreibung Standardwerte
1 Gesamtanforderungstimeout Die Gesamtanforderungstimeout-Pipeline wendet ein generelles Timeout auf die Ausführung an, um sicherzustellen, dass die Anforderung (einschließlich Hedgingversuchen) den konfigurierten Grenzwert nicht überschreitet. Gesamttimeout: 30 s
2 Hedging Die Hedgingstrategie führt die Anforderungen für mehrere Endpunkte aus, falls die Abhängigkeit langsam ist oder einen vorübergehenden Fehler zurückgibt. Routing ist optional – standardmäßig wird lediglich ein Hedging der URL durchgeführt, die in der ursprünglichen HttpRequestMessage bereitgestellt wird. Min. Versuche: 1
Max. Versuche: 10
Verzögerung: 2 s
3 Ratenbegrenzung (pro Endpunkt) Die Ratenbegrenzungspipeline begrenzt die maximale Anzahl gleichzeitiger Anforderungen, die an die Abhängigkeit gesendet werden. Queue: 0
Zulassen: 1_000
4 Sicherung (pro Endpunkt) Die Sicherung blockiert die Ausführung, wenn zu viele direkte Fehler oder Timeouts erkannt werden. Fehlerverhältnis: 10 %
Min. Durchsatz: 100
Stichprobenentnahmedauer: 30 s
Unterbrechungsdauer: 5 s
5 Versuchstimeout (pro Endpunkt) Die Versuchstimeoutpipeline begrenzt die Dauer der einzelnen Anforderungen. Sie wird beim Überschreiten ausgelöst. Timeout: 10 s

Anpassen der Routenauswahl von Hedginghandlern

Wenn Sie den Standardhedginghandler verwenden, können Sie anpassen, wie die Anforderungsendpunkte ausgewählt werden, indem Sie verschiedene Erweiterungen für den IRoutingStrategyBuilder-Typ aufrufen. Dies kann in Szenarien wie A/B-Tests nützlich sein, in denen Sie einen Prozentsatz der Anforderungen an einen anderen Endpunkt weiterleiten möchten:

httpClientBuilder.AddStandardHedgingHandler(static (IRoutingStrategyBuilder builder) =>
{
    // Hedging allows sending multiple concurrent requests
    builder.ConfigureOrderedGroups(static options =>
    {
        options.Groups.Add(new UriEndpointGroup()
        {
            Endpoints =
            {
                // Imagine a scenario where 3% of the requests are 
                // sent to the experimental endpoint.
                new() { Uri = new("https://example.net/api/experimental"), Weight = 3 },
                new() { Uri = new("https://example.net/api/stable"), Weight = 97 }
            }
        });
    });
});

Der vorangehende Code:

  • Fügt IHttpClientBuilder den Hedginghandler hinzu.
  • Konfiguriert IRoutingStrategyBuilder für die Verwendung der ConfigureOrderedGroups-Methode zum Konfigurieren der sortierten Gruppen.
  • Fügt der orderedGroup eine EndpointGroup hinzu, die 3 % der Anforderungen an den https://example.net/api/experimental-Endpunkt und 97 % der Anforderungen an den https://example.net/api/stable-Endpunkt weiterleitet.
  • Konfiguriert IRoutingStrategyBuilder für die Verwendung der ConfigureWeightedGroups-Methode zum Konfigurieren der

Um eine gewichtete Gruppe zu konfigurieren, rufen Sie die ConfigureWeightedGroups-Methode für den IRoutingStrategyBuilder-Typ auf. Im folgenden Beispiel wird IRoutingStrategyBuilder für die Verwendung der ConfigureWeightedGroups-Methode zum Konfigurieren der gewichteten Gruppen konfiguriert.

httpClientBuilder.AddStandardHedgingHandler(static (IRoutingStrategyBuilder builder) =>
{
    // Hedging allows sending multiple concurrent requests
    builder.ConfigureWeightedGroups(static options =>
    {
        options.SelectionMode = WeightedGroupSelectionMode.EveryAttempt;

        options.Groups.Add(new WeightedUriEndpointGroup()
        {
            Endpoints =
            {
                // Imagine A/B testing
                new() { Uri = new("https://example.net/api/a"), Weight = 33 },
                new() { Uri = new("https://example.net/api/b"), Weight = 33 },
                new() { Uri = new("https://example.net/api/c"), Weight = 33 }
            }
        });
    });
});

Der vorangehende Code:

  • Fügt IHttpClientBuilder den Hedginghandler hinzu.
  • Konfiguriert IRoutingStrategyBuilder für die Verwendung der ConfigureWeightedGroups-Methode zum Konfigurieren der gewichteten Gruppen.
  • Legt den Wert von SelectionMode auf WeightedGroupSelectionMode.EveryAttempt fest.
  • Fügt der weightedGroup eine WeightedEndpointGroup hinzu, die 33 % der Anforderungen an den https://example.net/api/a-Endpunkt, 33 % der Anforderungen an den https://example.net/api/b-Endpunkt und 33 % der Anforderungen an den https://example.net/api/c-Endpunkt weiterleitet.

Tipp

Die maximale Anzahl von Hedgingversuchen korreliert direkt mit der Anzahl der konfigurierten Gruppen. Bei zwei Gruppen beträgt die maximale Anzahl von Versuchen beispielsweise zwei.

Weitere Informationen finden Sie unter Polly-Dokumentation: Hedging Resilience Strategy.

Es ist üblich, entweder eine sortierte Gruppe oder eine gewichtete Gruppe zu konfigurieren, es ist aber auch möglich, beide zu konfigurieren. Die Verwendung von sortierten und gewichteten Gruppen ist in Szenarien hilfreich, in denen Sie einen Prozentsatz der Anforderungen an einen anderen Endpunkt senden möchten, z. B. bei A/B-Tests.

Hinzufügen benutzerdefinierter Resilienzhandler

Um mehr Kontrolle zu haben, können Sie die Resilienzhandler mithilfe der AddResilienceHandler-API anpassen. Diese Methode akzeptiert einen Delegaten, der die ResiliencePipelineBuilder<HttpResponseMessage>-Instanz konfiguriert, die zum Erstellen der Resilienzstrategien verwendet wird.

Rufen Sie die AddResilienceHandler-Erweiterungsmethode mit dem Namen des Handlers auf, um einen benannten Resilienzhandler zu konfigurieren. Im folgenden Beispiel wird ein benannter Resilienzhandler namens "CustomPipeline" konfiguriert.

httpClientBuilder.AddResilienceHandler(
    "CustomPipeline",
    static builder =>
{
    // See: https://www.pollydocs.org/strategies/retry.html
    builder.AddRetry(new HttpRetryStrategyOptions
    {
        // Customize and configure the retry logic.
        BackoffType = DelayBackoffType.Exponential,
        MaxRetryAttempts = 5,
        UseJitter = true
    });

    // See: https://www.pollydocs.org/strategies/circuit-breaker.html
    builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
    {
        // Customize and configure the circuit breaker logic.
        SamplingDuration = TimeSpan.FromSeconds(10),
        FailureRatio = 0.2,
        MinimumThroughput = 3,
        ShouldHandle = static args =>
        {
            return ValueTask.FromResult(args is
            {
                Outcome.Result.StatusCode:
                    HttpStatusCode.RequestTimeout or
                        HttpStatusCode.TooManyRequests
            });
        }
    });

    // See: https://www.pollydocs.org/strategies/timeout.html
    builder.AddTimeout(TimeSpan.FromSeconds(5));
});

Der vorangehende Code:

  • Fügt dem Dienstcontainer einen Resilienzhandler mit dem Namen "CustomPipeline" als pipelineName hinzu.
  • Fügt dem Resilienz-Generator eine Wiederholungsstrategie mit exponentiellem Backoff, fünf Wiederholungsversuchen und Jitter-Einstellungen hinzu.
  • Fügt eine Sicherungsstrategie mit einer Samplingdauer von 10 Sekunden, einem Fehlerverhältnis von 0,2 (20 %), einem minimalen Durchsatz von drei und einem Prädikat hinzu, das die HTTP-Statuscodes RequestTimeout und TooManyRequests für den Resilienz-Generator behandelt.
  • Fügt dem Resilienz-Generator eine Timeoutstrategie mit einem Timeout von fünf Sekunden hinzu.

Für jede der Resilienzstrategien stehen viele Optionen zur Verfügung. Weitere Informationen finden Sie unter Polly-Dokumentation: Strategies. Weitere Informationen zum Konfigurieren von ShouldHandle-Delegaten finden Sie unter Polly-Dokumentation: Fault handling in reactive strategies.

Dynamisches Neuladen

Polly unterstützt das dynamische Neuladen von konfigurierten Resilienzstrategien. Dies bedeutet, dass Sie die Konfiguration der Resilienzstrategien zur Laufzeit ändern können. Verwenden Sie zum Aktivieren des dynamischen Neuladens die entsprechende AddResilienceHandler-Überladung, die den ResilienceHandlerContextverfügbar macht. Rufen Sie je nach Kontext EnableReloads für die entsprechenden Resilienzstrategieoptionen auf:

httpClientBuilder.AddResilienceHandler(
    "AdvancedPipeline",
    static (ResiliencePipelineBuilder<HttpResponseMessage> builder,
        ResilienceHandlerContext context) =>
    {
        // Enable reloads whenever the named options change
        context.EnableReloads<HttpRetryStrategyOptions>("RetryOptions");

        // Retrieve the named options
        var retryOptions =
            context.GetOptions<HttpRetryStrategyOptions>("RetryOptions");

        // Add retries using the resolved options
        builder.AddRetry(retryOptions);
    });

Der vorangehende Code:

  • Fügt dem Dienstcontainer einen Resilienzhandler mit dem Namen "AdvancedPipeline" als pipelineName hinzu.
  • Aktiviert das erneute Laden der "AdvancedPipeline"-Pipeline, wenn sich die benannten RetryStrategyOptions-Optionen ändern.
  • Ruft die benannten Optionen aus dem IOptionsMonitor<TOptions>-Dienst ab.
  • Fügt dem Resilienz-Generator eine Wiederholungsstrategie mit den abgerufenen Optionen hinzu.

Weitere Informationen finden Sie unter Polly-Dokumentation: Advanced dependency injection.

In diesem Beispiel wird ein Optionsabschnitt verwendet, der geändert werden kann, z. B. durch eine Datei appsettings.json. Sehen Sie sich die nachfolgende Datei appsettings.json an:

{
    "RetryOptions": {
        "Retry": {
            "BackoffType": "Linear",
            "UseJitter": false,
            "MaxRetryAttempts": 7
        }
    }
}

Stellen Sie sich nun vor, dass diese Optionen an die Konfiguration der App gebunden sind, und binden Sie die HttpRetryStrategyOptions an den Abschnitt "RetryOptions":

var section = builder.Configuration.GetSection("RetryOptions");

builder.Services.Configure<HttpStandardResilienceOptions>(section);

Weitere Informationen finden Sie unter Optionsmuster in .NET.

Beispielverwendung

Ihre App erfordert eine Abhängigkeitsinjektion, um den ExampleClient und den entsprechenden HttpClient aufzulösen. Der Code erstellt den IServiceProvider und löst den ExampleClient damit auf.

IHost host = builder.Build();

ExampleClient client = host.Services.GetRequiredService<ExampleClient>();

await foreach (Comment? comment in client.GetCommentsAsync())
{
    Console.WriteLine(comment);
}

Der vorangehende Code:

Stellen Sie sich eine Situation vor, in der das Netzwerk ausfällt oder der Server nicht mehr reagiert. Das folgende Diagramm zeigt, wie die Situationen mit den Resilienzstrategien mithilfe der Methoden ExampleClient und GetCommentsAsync behandeln werden:

Beispiel für HTTP GET-Workflow mit Resilienzpipeline

Das obige Diagramm zeigt Folgendes:

  • Der ExampleClient sendet eine HTTP GET-Anforderung an den /comments-Endpunkt.
  • Die HttpResponseMessage wird ausgewertet:
    • Wenn die Antwort erfolgreich ist (HTTP 200), wird sie zurückgegeben.
    • Wenn die Antwort nicht erfolgreich ist (nicht HTTP 200), wendet die Resilienzpipeline die konfigurierten Resilienzstrategien an.

Dies ist zwar ein einfaches Beispiel, aber es zeigt, wie die Resilienzstrategien verwendet werden können, um vorübergehende Fehler zu behandeln. Weitere Informationen finden Sie unter Polly-Dokumentation: Strategies.

Bekannte Probleme

In den folgenden Abschnitten werden verschiedene bekannte Probleme beschrieben.

Kompatibilität mit dem Grpc.Net.ClientFactory-Paket

Wenn Sie die Grpc.Net.ClientFactory-Version 2.63.0 bzw. eine frühere verwenden, kann das Aktivieren der Standardresilienz- oder Hedginghandler für einen gRPC-Client zu einer Laufzeitausnahme führen. Sehen Sie sich insbesondere das folgende Codebeispiel an:

services
    .AddGrpcClient<Greeter.GreeterClient>()
    .AddStandardResilienceHandler();

Der vorangegangene Code ergibt die folgende Ausnahme:

System.InvalidOperationException: The ConfigureHttpClient method is not supported when creating gRPC clients. Unable to create client with name 'GreeterClient'.

Um dieses Problem zu beheben, empfehlen wir ein Upgrade auf die Grpc.Net.ClientFactory-Version 2.64.0 oder höher.

Es gibt eine Buildzeitprüfung, di verifiziert, ob Sie die Grpc.Net.ClientFactory-Version 2.63.0 oder eine frühere Version verwenden, und falls ja, generiert die Überprüfung eine Kompilierungswarnung. Sie können die Warnung unterdrücken, indem Sie die folgende Eigenschaft in der Projektdatei festlegen:

<PropertyGroup>
  <SuppressCheckGrpcNetClientFactoryVersion>true</SuppressCheckGrpcNetClientFactoryVersion>
</PropertyGroup>

Kompatibilität mit .NET Application Insights

Wenn Sie .NET Application Insights verwenden und in Ihrer Anwendung die Resilienzfunktion aktivieren, kann es vorkommen, dass alle Telemetriedaten von Application Insights fehlen. Das Problem tritt auf, wenn die Resilienzfunktion vor Application Insights-Diensten registriert wird. Sehen Sie sich das folgende Beispiel an, das das Problem verursacht:

// At first, we register resilience functionality.
services.AddHttpClient().AddStandardResilienceHandler();

// And then we register Application Insights. As a result, Application Insights doesn't work.
services.AddApplicationInsightsTelemetry();

Das Problem wird durch den folgenden Fehler in Application Insights verursacht und kann behoben werden, indem Application Insights-Dienste vor den Resilienzfunktionen registriert werden, wie unten dargestellt:

// We register Application Insights first, and now it will be working correctly.
services.AddApplicationInsightsTelemetry();
services.AddHttpClient().AddStandardResilienceHandler();