Udostępnij za pośrednictwem


Tworzenie odpornych aplikacji HTTP: kluczowe wzorce programistyczne

Tworzenie niezawodnych aplikacji HTTP, które mogą odzyskiwać dane po błędach przejściowych, jest typowym wymaganiem. W tym artykule założono, że znasz już artykuł Introduction to resilient app development (Wprowadzenie do odpornego programowania aplikacji), ponieważ ten artykuł rozszerza podstawowe pojęcia, które zostały przekazane. Aby ułatwić tworzenie odpornych aplikacji HTTP, pakiet NuGet Microsoft.Extensions.Http.Resilience zapewnia mechanizmy odporności specjalnie dla elementu HttpClient. Ten pakiet NuGet opiera się na Microsoft.Extensions.Resilience bibliotece i polly, która jest popularnym projektem open source. Aby uzyskać więcej informacji, zobacz Polly.

Rozpocznij

Aby użyć wzorców odporności w aplikacjach HTTP, zainstaluj pakiet NuGet Microsoft.Extensions.Http.Resilience .

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

Aby uzyskać więcej informacji, zobacz dotnet add package or Manage package dependencies in .NET applications (Zarządzanie zależnościami pakietów w aplikacjach platformy .NET).

Dodawanie odporności do klienta HTTP

Aby zwiększyć odporność na element HttpClient, należy utworzyć łańcuch wywołań dla IHttpClientBuilder typu zwracanego z wywołania dowolnej z dostępnych AddHttpClient metod. Aby uzyskać więcej informacji, zobacz IHttpClientFactory with .NET (IHttpClientFactory z platformą .NET).

Dostępnych jest kilka rozszerzeń skoncentrowanych na odporności. Niektóre z nich są standardowe, dlatego stosują różne najlepsze rozwiązania branżowe, a inne są bardziej dostosowywane. Podczas dodawania odporności należy dodać tylko jedną procedurę obsługi odporności i uniknąć obsługi stosu. Jeśli musisz dodać wiele procedur obsługi odporności, rozważ użycie AddResilienceHandler metody rozszerzenia, która umożliwia dostosowanie strategii odporności.

Ważne

Wszystkie przykłady w tym artykule opierają się na interfejsie AddHttpClient API z biblioteki Microsoft.Extensions.Http , która zwraca IHttpClientBuilder wystąpienie. Wystąpienie IHttpClientBuilder służy do konfigurowania HttpClient i dodawania procedury obsługi odporności.

Dodawanie standardowej procedury obsługi odporności

Standardowy program obsługi odporności używa wielu strategii odporności skumulowanych na drugim, z domyślnymi opcjami wysyłania żądań i obsługi wszelkich błędów przejściowych. Standardowa procedura obsługi odporności jest dodawana przez wywołanie AddStandardResilienceHandler metody rozszerzenia w wystąpieniu IHttpClientBuilder .

var services = new ServiceCollection();

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

Powyższy kod ma następujące działanie:

  • Tworzy ServiceCollection wystąpienie.
  • Dodaje element HttpClient dla ExampleClient typu do kontenera usługi.
  • Konfiguruje HttpClient element do użycia "https://jsonplaceholder.typicode.com" jako adres podstawowy.
  • Tworzy element httpClientBuilder używany w innych przykładach w tym artykule.

Bardziej rzeczywisty przykład polegałby na hostingu, takim jak opisany w artykule Host ogólny platformy .NET. Korzystając z pakietu NuGet Microsoft.Extensions.Hosting, rozważmy następujący zaktualizowany przykład:

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");
    });

Powyższy kod jest podobny do podejścia do ręcznego ServiceCollection tworzenia, ale zamiast tego opiera się na Host.CreateApplicationBuilder() tworzeniu hosta, który uwidacznia usługi.

Element ExampleClient jest zdefiniowany w następujący sposób:

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");
    }
}

Powyższy kod ma następujące działanie:

  • ExampleClient Definiuje typ, który ma konstruktor, który akceptuje element HttpClient.
  • Uwidacznia metodę GetCommentsAsync , która wysyła żądanie GET do punktu końcowego /comments i zwraca odpowiedź.

Typ jest definiowany Comment w następujący sposób:

namespace Http.Resilience.Example;

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

Biorąc pod uwagę, że utworzono element (httpClientBuilder), a teraz rozumiesz implementację IHttpClientBuilder ExampleClient i odpowiedni Comment model, rozważmy następujący przykład:

httpClientBuilder.AddStandardResilienceHandler();

Powyższy kod dodaje standardową procedurę obsługi odporności do elementu HttpClient. Podobnie jak większość interfejsów API odporności, istnieją przeciążenia, które umożliwiają dostosowanie domyślnych opcji i zastosowanych strategii odporności.

Domyślne ustawienia obsługi odporności w warstwie Standardowa

Domyślna konfiguracja zawiera pięć strategii odporności w następującej kolejności (od najbardziej zewnętrznej do najbardziej wewnętrznej):

Zamówienie Strategia opis Defaults
1 Ogranicznik szybkości Potok ogranicznika szybkości ogranicza maksymalną liczbę współbieżnych żądań wysyłanych do zależności. Kolejka: 0
Pozwalać: 1_000
2 Łączny limit czasu Łączny potok limitu czasu żądania stosuje całkowity limit czasu do wykonania, zapewniając, że żądanie, w tym próby ponawiania prób, nie przekracza skonfigurowanego limitu. Łączny limit czasu: 30s
3 Ponów próbę Potok ponawia próby ponawia próbę żądania w przypadku, gdy zależność jest powolna lub zwraca błąd przejściowy. Maksymalna liczba ponownych prób: 3
Wycofywanie: Exponential
Użyj roztrzasku: true
Opóźnienie:2s
4 Wyłącznik Wyłącznik blokuje wykonywanie, jeśli wykryto zbyt wiele bezpośrednich awarii lub przekroczenia limitu czasu. Współczynnik awarii: 10%
Minimalna przepływność: 100
Czas trwania próbkowania: 30s
Czas trwania przerwy: 5s
5 Limit czasu próby Potok limitu czasu próby ogranicza czas trwania każdej próby żądania i zgłasza błąd, jeśli został przekroczony. Limit czasu próby: 10s

Ponawianie prób i wyłączniki

Strategie ponawiania i wyłącznika obsługują zarówno zestaw określonych kodów stanu HTTP, jak i wyjątków. Rozważ następujące kody stanu HTTP:

  • HTTP 500 lub nowszy (błędy serwera)
  • HTTP 408 (limit czasu żądania)
  • HTTP 429 (zbyt wiele żądań)

Ponadto te strategie obsługują następujące wyjątki:

  • HttpRequestException
  • TimeoutRejectedException

Dodawanie standardowej procedury obsługi zabezpieczania

Standardowy program obsługi zabezpieczania opakowuje wykonywanie żądania za pomocą standardowego mechanizmu hedgingowego. Zabezpieczenia ponawiają próby powolnego żądań równolegle.

Aby użyć standardowej procedury obsługi hedgingowej, wywołaj AddStandardHedgingHandler metodę rozszerzenia. W poniższym przykładzie skonfigurowaliśmy element ExampleClient tak, aby używał standardowej procedury obsługi hedgingowej.

httpClientBuilder.AddStandardHedgingHandler();

Powyższy kod dodaje standardową procedurę obsługi zabezpieczania do elementu HttpClient.

Domyślne ustawienia standardowej procedury obsługi zabezpieczania

Standardowe zabezpieczenie wykorzystuje pulę wyłączników, aby zapewnić, że punkty końcowe w złej kondycji nie są zabezpieczone. Domyślnie wybór z puli jest oparty na urzędzie adresu URL (schemat + host + port).

Napiwek

Zaleca się skonfigurowanie sposobu wybierania strategii przez wywołanie StandardHedgingHandlerBuilderExtensions.SelectPipelineByAuthority lub StandardHedgingHandlerBuilderExtensions.SelectPipelineBy w przypadku bardziej zaawansowanych scenariuszy.

Powyższy kod dodaje standardową procedurę obsługi zabezpieczania do elementu IHttpClientBuilder. Domyślna konfiguracja zawiera pięć strategii odporności w następującej kolejności (od najbardziej zewnętrznej do najbardziej wewnętrznej):

Zamówienie Strategia opis Defaults
1 Łączny limit czasu żądania Łączny potok limitu czasu żądania stosuje ogólny limit czasu do wykonania, zapewniając, że żądanie, w tym próby zabezpieczenia, nie przekracza skonfigurowanego limitu. Łączny limit czasu: 30s
2 Zabezpieczenia Strategia zabezpieczania wykonuje żądania względem wielu punktów końcowych, jeśli zależność jest powolna lub zwraca błąd przejściowy. Routing to opcje, domyślnie tylko zabezpiecza adres URL dostarczony przez oryginalny HttpRequestMessageelement . Minimalna liczba prób: 1
Maksymalna liczba prób: 10
Opóźnienie: 2s
3 Ogranicznik szybkości (na punkt końcowy) Potok ogranicznika szybkości ogranicza maksymalną liczbę współbieżnych żądań wysyłanych do zależności. Kolejka: 0
Pozwalać: 1_000
4 Wyłącznik (na punkt końcowy) Wyłącznik blokuje wykonywanie, jeśli wykryto zbyt wiele bezpośrednich awarii lub przekroczenia limitu czasu. Współczynnik awarii: 10%
Minimalna przepływność: 100
Czas trwania próbkowania: 30s
Czas trwania przerwy: 5s
5 Limit czasu próby (na punkt końcowy) Potok limitu czasu próby ogranicza czas trwania każdej próby żądania i zgłasza błąd, jeśli został przekroczony. Limit czasu: 10s

Dostosowywanie wyboru trasy procedury obsługi hedgingowej

W przypadku korzystania ze standardowej procedury obsługi zabezpieczania można dostosować sposób wybierania punktów końcowych żądań przez wywołanie różnych rozszerzeń dla IRoutingStrategyBuilder typu. Może to być przydatne w przypadku scenariuszy, takich jak testowanie A/B, w których chcesz kierować procent żądań do innego punktu końcowego:

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 }
            }
        });
    });
});

Powyższy kod ma następujące działanie:

  • Dodaje procedurę obsługi hedgingowej do elementu IHttpClientBuilder.
  • Konfiguruje metodę IRoutingStrategyBuilder ConfigureOrderedGroups , aby używać metody do konfigurowania uporządkowanych grup.
  • Dodaje element EndpointGroup do orderedGroup obiektu , który kieruje 3% żądań do https://example.net/api/experimental punktu końcowego i 97% żądań do punktu końcowego https://example.net/api/stable .
  • Konfiguruje IRoutingStrategyBuilder metodę , aby skonfigurować metodę ConfigureWeightedGroups

Aby skonfigurować grupę ważoną, wywołaj metodę ConfigureWeightedGroups dla IRoutingStrategyBuilder typu . Poniższy przykład umożliwia skonfigurowanie IRoutingStrategyBuilder grup ważonych przy ConfigureWeightedGroups użyciu metody .

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 }
            }
        });
    });
});

Powyższy kod ma następujące działanie:

  • Dodaje procedurę obsługi hedgingowej do elementu IHttpClientBuilder.
  • Konfiguruje metodę IRoutingStrategyBuilder ConfigureWeightedGroups , aby używać metody do konfigurowania grup ważonych.
  • Ustawia wartość SelectionMode na WeightedGroupSelectionMode.EveryAttempt.
  • WeightedEndpointGroup Dodaje element do weightedGroup obiektu , który kieruje 33% żądań do https://example.net/api/a punktu końcowego, 33% żądań do https://example.net/api/b punktu końcowego i 33% żądań do punktu końcowegohttps://example.net/api/c.

Napiwek

Maksymalna liczba prób zabezpieczenia bezpośrednio koreluje z liczbą skonfigurowanych grup. Jeśli na przykład masz dwie grupy, maksymalna liczba prób wynosi dwa.

Aby uzyskać więcej informacji, zobacz Polly docs: Zabezpieczanie strategii odporności.

Często konfiguruje się uporządkowaną grupę lub grupę ważoną, ale ważne jest skonfigurowanie obu tych grup. Użycie uporządkowanych i ważonych grup jest przydatne w scenariuszach, w których chcesz wysłać procent żądań do innego punktu końcowego, na przykład w przypadku testowania A/B.

Dodawanie niestandardowych procedur obsługi odporności

Aby mieć większą kontrolę, możesz dostosować procedury obsługi odporności przy użyciu interfejsu AddResilienceHandler API. Ta metoda akceptuje delegata, który konfiguruje ResiliencePipelineBuilder<HttpResponseMessage> wystąpienie używane do tworzenia strategii odporności.

Aby skonfigurować nazwaną procedurę obsługi odporności, wywołaj metodę AddResilienceHandler rozszerzenia o nazwie programu obsługi. W poniższym przykładzie skonfigurowano nazwaną procedurę obsługi odporności o nazwie "CustomPipeline".

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));
});

Powyższy kod ma następujące działanie:

  • Dodaje procedurę obsługi odporności o nazwie "CustomPipeline" jako pipelineName kontenera usługi.
  • Dodaje strategię ponawiania prób z wykładniczym wycofywaniem, pięcioma ponownymi próbami i preferencjami trząsania do konstruktora odporności.
  • Dodaje strategię wyłącznika z czasem próbkowania wynoszącym 10 sekund, współczynnik awarii wynoszący 0,2 (20%), minimalną przepływność wynoszącą trzy oraz predykat, który obsługuje RequestTimeout kody stanu HTTP i TooManyRequests konstruktorowi odporności.
  • Dodaje strategię limitu czasu z limitem czasu 5 sekund do konstruktora odporności.

Dla każdej strategii odporności jest dostępnych wiele opcji. Aby uzyskać więcej informacji, zobacz dokumentację usługi Polly: Strategies. Aby uzyskać więcej informacji na temat konfigurowania ShouldHandle delegatów, zobacz Polly docs: Fault handling in reaktywne strategie.

Dynamiczne ponowne ładowanie

Usługa Polly obsługuje dynamiczne ponowne ładowanie skonfigurowanych strategii odporności. Oznacza to, że można zmienić konfigurację strategii odporności w czasie wykonywania. Aby włączyć dynamiczne ponowne ładowanie, użyj odpowiedniego AddResilienceHandler przeciążenia, które uwidacznia element ResilienceHandlerContext. Biorąc pod uwagę kontekst, wywołaj EnableReloads odpowiednie opcje strategii odporności:

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);
    });

Powyższy kod ma następujące działanie:

  • Dodaje procedurę obsługi odporności o nazwie "AdvancedPipeline" jako pipelineName kontenera usługi.
  • Włącza ponowne ładowanie potoku "AdvancedPipeline" za każdym razem, gdy zmienią się nazwane RetryStrategyOptions opcje.
  • Pobiera nazwane opcje z IOptionsMonitor<TOptions> usługi.
  • Dodaje strategię ponawiania z pobranymi opcjami konstruktora odporności.

Aby uzyskać więcej informacji, zobacz Polly docs: Advanced dependency injection (Dokumentacja usługi Polly: zaawansowane wstrzykiwanie zależności).

Ten przykład opiera się na sekcji opcji, która może ulec zmianie, takiej jak plik appsettings.json . Rozważ następujący plik appsettings.json :

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

Teraz wyobraź sobie, że te opcje zostały powiązane z konfiguracją aplikacji, wiążąc element z HttpRetryStrategyOptions sekcją "RetryOptions" :

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

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

Aby uzyskać więcej informacji, zobacz Wzorzec opcji na platformie .NET.

Przykładowe użycie

Aplikacja opiera się na iniekcji zależności, aby rozpoznać ExampleClient element i odpowiadający mu HttpClientelement . Kod kompiluje element IServiceProvider i rozpoznaje element ExampleClient z niego.

IHost host = builder.Build();

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

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

Powyższy kod ma następujące działanie:

  • Kompiluje element IServiceProvider z pliku ServiceCollection.
  • Usuwa element ExampleClient z elementu IServiceProvider.
  • Wywołuje metodę GetCommentsAsync w obiekcie , ExampleClient aby uzyskać komentarze.
  • Zapisuje każdy komentarz do konsoli.

Wyobraź sobie sytuację, w której sieć ulegnie awarii lub serwer przestaje odpowiadać. Na poniższym diagramie pokazano, jak strategie odporności będą obsługiwać sytuację, biorąc pod uwagę metodę ExampleClient i GetCommentsAsync :

Przykładowy przepływ pracy HTTP GET z potokiem odporności.

Powyższy diagram przedstawia:

  • Obiekt ExampleClient wysyła żądanie HTTP GET do punktu końcowego /comments .
  • Wartość HttpResponseMessage jest oceniana:
    • Jeśli odpowiedź zakończy się pomyślnie (HTTP 200), zostanie zwrócona odpowiedź.
    • Jeśli odpowiedź nie powiedzie się (http inny niż 200), potok odporności stosuje skonfigurowane strategie odporności.

Chociaż jest to prosty przykład, pokazuje, jak strategie odporności mogą służyć do obsługi błędów przejściowych. Aby uzyskać więcej informacji, zobacz Polly docs: Strategies (Dokumentacja usługi Polly: strategie).

Znane problemy

W poniższych sekcjach opisano różne znane problemy.

Zgodność z pakietem Grpc.Net.ClientFactory

Jeśli używasz Grpc.Net.ClientFactory wersji 2.63.0 lub starszej, włączenie standardowej odporności lub procedur obsługi zabezpieczania dla klienta gRPC może spowodować wyjątek środowiska uruchomieniowego. W szczególności rozważmy następujący przykład kodu:

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

Powyższy kod powoduje następujący wyjątek:

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

Aby rozwiązać ten problem, zalecamy uaktualnienie do Grpc.Net.ClientFactory wersji lub nowszej 2.64.0 .

Istnieje kontrola czasu kompilacji, która sprawdza, czy używasz Grpc.Net.ClientFactory wersji 2.63.0 lub starszej wersji, a jeśli test generuje ostrzeżenie kompilacji. Ostrzeżenie można pominąć, ustawiając następującą właściwość w pliku projektu:

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

Zgodność z usługą .NET Application Insights

Jeśli używasz usługi Application Insights platformy .NET, włączenie funkcji odporności w aplikacji może spowodować brak wszystkich danych telemetrycznych usługi Application Insights. Problem występuje, gdy funkcje odporności są rejestrowane przed usługami Application Insights. Rozważmy następujący przykład powodujący problem:

// 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();

Problem jest spowodowany przez następującą usterkę w usłudze Application Insights i można rozwiązać, rejestrując usługi Application Insights przed działaniem odporności, jak pokazano poniżej:

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