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
doorderedGroup
obiektu , który kieruje 3% żądań dohttps://example.net/api/experimental
punktu końcowego i 97% żądań do punktu końcowegohttps://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
naWeightedGroupSelectionMode.EveryAttempt
. WeightedEndpointGroup
Dodaje element doweightedGroup
obiektu , który kieruje 33% żądań dohttps://example.net/api/a
punktu końcowego, 33% żądań dohttps://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"
jakopipelineName
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 iTooManyRequests
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"
jakopipelineName
kontenera usługi. - Włącza ponowne ładowanie potoku
"AdvancedPipeline"
za każdym razem, gdy zmienią się nazwaneRetryStrategyOptions
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
:
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();