构建可复原的 HTTP 应用:关键开发模式
一个常见的要求是构建可从暂时性故障错误中恢复的可靠 HTTP 应用。 本文假设你已经阅读可复原的应用开发简介,因为本文延伸讲解了其中介绍的核心概念。 为了帮助构建可复原的 HTTP 应用,Microsoft.Extensions.Http.Resilience NuGet 包专门为 HttpClient 提供了复原机制。 此 NuGet 包依赖于 Microsoft.Extensions.Resilience
库和 Polly,后者是一个常用的开源项目。 有关详细信息,请参阅 Polly。
开始使用
若要在 HTTP 应用中使用复原模式,请安装 Microsoft.Extensions.Http.Resilience NuGet 包。
dotnet add package Microsoft.Extensions.Http.Resilience --version 8.0.0
有关详细信息,请参阅 dotnet add package 或管理 .NET 应用程序中的包依赖项。
向 HTTP 客户端添加复原能力
若要向 HttpClient 添加复原能力,可对 IHttpClientBuilder 类型的调用进行链接,其中该类型是从调用任何可用的 AddHttpClient 方法而返回的。 有关详细信息,请参阅 .NET 的 IHttpClientFactory。
有几种以复原能力为中心的扩展可用。 一些是标准扩展,因此采用各种行业最佳做法,而另一些则更可自定义。 添加复原能力时,应只添加一个复原处理程序并避免堆叠处理程序。 如果需要添加多个复原处理程序,应考虑使用 AddResilienceHandler
扩展方法,它支持自定义复原策略。
重要
本文中的所有示例都依赖于来自 Microsoft.Extensions.Http 库的 AddHttpClient API,它返回 IHttpClientBuilder 实例。 IHttpClientBuilder 实例用于配置 HttpClient 和添加复原处理程序。
添加标准复原处理程序
标准复原处理程序使用堆叠在一起的多个复原策略,默认选项用于发送请求并处理任何暂时性错误。 通过对 IHttpClientBuilder 实例调用 AddStandardResilienceHandler
扩展方法来添加标准复原处理程序。
var services = new ServiceCollection();
var httpClientBuilder = services.AddHttpClient<ExampleClient>(
configureClient: static client =>
{
client.BaseAddress = new("https://jsonplaceholder.typicode.com");
});
前面的代码:
- 创建一个 ServiceCollection 实例。
- 向服务容器添加
ExampleClient
类型的 HttpClient。 - 配置 HttpClient 来将
"https://jsonplaceholder.typicode.com"
用作基址。 - 创建
httpClientBuilder
,我们将在本文的其他示例中使用它。
更真实的示例将依赖于托管,如 .NET 通用主机一文中所述。 使用 Microsoft.Extensions.Hosting NuGet 包,请考虑下列已更新的示例:
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");
});
上述代码类似于使用 ServiceCollection
手动创建的方法,但它依赖于 Host.CreateApplicationBuilder() 来构建公开服务的主机。
ExampleClient
定义如下:
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");
}
}
前面的代码:
- 定义一个
ExampleClient
类型,它具有接受 HttpClient 的构造函数。 - 公开
GetCommentsAsync
方法,它向/comments
终结点发送 GET 请求并返回响应。
Comment
类型定义如下:
namespace Http.Resilience.Example;
public record class Comment(
int PostId, int Id, string Name, string Email, string Body);
假设你已经创建了一个 IHttpClientBuilder (httpClientBuilder
),并且现在要了解 ExampleClient
实现和相应的 Comment
模型,那么请考虑以下示例:
httpClientBuilder.AddStandardResilienceHandler();
上述代码将标准复原处理程序添加到 HttpClient。 与大多数复原 API 一样,有一些重载可用于自定义默认选项并应用复原策略。
标准复原处理程序默认值
默认配置按以下顺序链接 5 个复原策略(从最外层到最内层):
订单 | 策略 | 说明 | Defaults |
---|---|---|---|
1 | 速率限制器 | 速率限制器管道限制发送到依赖项的最大并发请求数。 | 队列:0 许可: 1_000 |
2 | 超时总计 | 总请求超时管道将总超时应用于执行,确保请求(包括重试尝试)不会超过配置的限制。 | 超时总计:30 秒 |
3 | 重试 | 如果依赖项速度缓慢或返回暂时性错误,重试管道会重试请求。 | 最多重试次数:3 回退: Exponential 使用抖动: true 延迟:2 秒 |
4 | 断路器 | 如果检测到过多的直接故障或超时,断路器会阻止执行。 | 故障率:10% 最小吞吐量: 100 采样持续时间:30 秒 中断持续时间:5 秒 |
5 | 尝试超时 | 尝试超时管道限制每个请求尝试持续时间,并在超出此持续时间时引发。 | 尝试超时:10 秒 |
重试和断路器
重试和断路器策略都会处理一组特定的 HTTP 状态代码和异常。 请考虑以下 HTTP 状态代码:
- HTTP 500 及更高版本(服务器错误)
- HTTP 408(请求超时)
- HTTP 429(请求过多)
此外,这些策略还会处理以下异常:
HttpRequestException
TimeoutRejectedException
添加标准对冲 (hedging) 处理程序
标准对冲处理程序使用标准对冲机制包装请求的执行。 对冲会并行重试速度缓慢的请求。
若要使用标准对冲处理程序,请调用 AddStandardHedgingHandler
扩展方法。 以下示例将 ExampleClient
配置为使用标准对冲处理程序。
httpClientBuilder.AddStandardHedgingHandler();
上述代码将标准对冲处理程序添加到 HttpClient。
标准对冲处理程序默认值
标准对冲使用一组断路器来确保运行不正常的终结点不被对冲。 默认情况下,根据 URL 机构(架构 + 主机 + 端口)从这组断路器中进行选择。
提示
对于更高级的场景,建议通过调用 StandardHedgingHandlerBuilderExtensions.SelectPipelineByAuthority
或 StandardHedgingHandlerBuilderExtensions.SelectPipelineBy
来配置选择策略的方式。
上述代码将标准对冲处理程序添加到 IHttpClientBuilder。 默认配置按以下顺序链接 5 个复原策略(从最外层到最内层):
订单 | 策略 | 说明 | Defaults |
---|---|---|---|
1 | 总请求超时 | 总请求超时管道将总超时应用于执行,确保请求(包括对冲尝试)不会超过配置的限制。 | 超时总计:30 秒 |
2 | Hedging | 当依赖项速度缓慢或返回暂时性错误时,对冲策略针对多个终结点执行请求。 路由是选项,默认情况,它只对原始 HttpRequestMessage 提供的 URL 执行对冲。 | 最小尝试次数:1 最大尝试次数: 10 延迟:2 秒 |
3 | 速率限制器(每个终结点) | 速率限制器管道限制发送到依赖项的最大并发请求数。 | 队列:0 许可: 1_000 |
4 | 断路器(每个终结点) | 如果检测到过多的直接故障或超时,断路器会阻止执行。 | 故障率:10% 最小吞吐量: 100 采样持续时间:30 秒 中断持续时间:5 秒 |
5 | 尝试超时(每个终结点) | 尝试超时管道限制每个请求尝试持续时间,并在超出此持续时间时引发。 | 超时:10 秒 |
自定义对冲处理程序路由选择
使用标准对冲处理程序时,可通过对 IRoutingStrategyBuilder
类型调用各种扩展来自定义选择请求终结点的方式。 这对于 A/B 测试等场景非常有用,在这种情况下,你希望将一定比例的请求路由到其他终结点:
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 }
}
});
});
});
前面的代码:
- 将对冲处理程序添加到 IHttpClientBuilder。
- 配置
IRoutingStrategyBuilder
,使用ConfigureOrderedGroups
方法来配置有序组。 - 将
EndpointGroup
添加到orderedGroup
,它将 3% 的请求路由到https://example.net/api/experimental
终结点,将 97% 的请求路由到https://example.net/api/stable
终结点。 - 配置
IRoutingStrategyBuilder
,使用ConfigureWeightedGroups
方法来配置
若要配置加权组,请对 IRoutingStrategyBuilder
类型调用 ConfigureWeightedGroups
方法。 以下示例将 IRoutingStrategyBuilder
配置为使用 ConfigureWeightedGroups
方法来配置加权组。
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 }
}
});
});
});
前面的代码:
- 将对冲处理程序添加到 IHttpClientBuilder。
- 配置
IRoutingStrategyBuilder
,使用ConfigureWeightedGroups
方法来配置加权组。 - 将
SelectionMode
设置为WeightedGroupSelectionMode.EveryAttempt
。 - 将
WeightedEndpointGroup
添加到weightedGroup
,它将 33% 的请求路由到https://example.net/api/a
终结点,将 33% 的请求路由到https://example.net/api/b
终结点,将 33% 的请求路由到https://example.net/api/c
终结点。
提示
最大对冲尝试次数与配置的组数直接关联。 例如,如果你有两个组,最大尝试次数是两次。
有关详细信息,请参阅 Polly 文档:对冲复原策略。
通常配置有序组或加权组,但配置这两者也有效。 如果想要将一定比例的请求发送到其他终结点(例如 A/B 测试的情况),那么使用有序组和加权组非常有用。
添加自定义复原处理程序
若要获得更多控制,可以使用 AddResilienceHandler
API 来自定义复原处理程序。 此方法接受一个配置 ResiliencePipelineBuilder<HttpResponseMessage>
实例的委托,该实例用于创建复原策略。
若要配置命名的复原处理程序,请使用处理程序的名称调用 AddResilienceHandler
扩展方法。 以下示例配置一个名为 "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));
});
前面的代码:
- 将具有名称
"CustomPipeline"
的复原处理程序作为pipelineName
添加到服务容器。 - 向复原生成器添加具有指数退避、5 次重试和抖动首选项的重试策略。
- 向复原生成器添加一个断路器策略,该策略的采样持续时间为 10 秒、故障比率为 0.2 (20%)、最小吞吐量为 3,并且有一个谓词用来处理
RequestTimeout
和TooManyRequests
HTTP 状态代码。 - 向复原生成器添加一个超时时间为 5 秒的超时策略。
每个复原策略都有许多选项可用。 有关详细信息,请参阅 Polly 文档:策略。 若要详细了解如何配置 ShouldHandle
委托,请参阅 Polly 文档:响应式策略中的故障处理。
动态重载
Polly 支持动态重载已配置的复原策略。 这意味着可以在运行时更改复原策略的配置。 若要启用动态重载,请使用相应的公开 ResilienceHandlerContext
的 AddResilienceHandler
重载。 在给定上下文中,调用相应复原策略选项的 EnableReloads
:
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);
});
前面的代码:
- 将具有名称
"AdvancedPipeline"
的复原处理程序作为pipelineName
添加到服务容器。 - 每当已命名的
"AdvancedPipeline"
选项发生更改时,都启用RetryStrategyOptions
管道的重载。 - 从 IOptionsMonitor<TOptions> 服务中检索命名选项。
- 将具有检索选项的重试策略添加到复原生成器。
有关详细信息,请参阅 Polly 文档:高级依赖关系注入。
此示例依赖于可更改的选项部分,例如 appsettings.json 文件。 请考虑使用以下 appsettings.json 文件:
{
"RetryOptions": {
"Retry": {
"BackoffType": "Linear",
"UseJitter": false,
"MaxRetryAttempts": 7
}
}
}
现在,假设这些选项已绑定到应用的配置,将 HttpRetryStrategyOptions
绑定到 "RetryOptions"
部分:
var section = builder.Configuration.GetSection("RetryOptions");
builder.Services.Configure<HttpStandardResilienceOptions>(section);
有关详细信息,请参阅 .NET 中的选项模式。
用法示例
你的应用依靠依赖关系注入来解决 ExampleClient
及其相应的 HttpClient。 代码会生成 IServiceProvider 并从中解析 ExampleClient
。
IHost host = builder.Build();
ExampleClient client = host.Services.GetRequiredService<ExampleClient>();
await foreach (Comment? comment in client.GetCommentsAsync())
{
Console.WriteLine(comment);
}
前面的代码:
- 从 ServiceCollection 中构建 IServiceProvider。
- 从 IServiceProvider 中解析
ExampleClient
。 - 对
ExampleClient
方法调用GetCommentsAsync
以获取注释。 - 将每个注释写入控制台。
想象一下网络出现故障或服务器无响应的情况。 下图显示了在给定 ExampleClient
和 GetCommentsAsync
方法的情况下,复原策略如何处理该情况:
上图描述了:
ExampleClient
将 HTTP GET 请求发送到/comments
终结点。- 会计算 HttpResponseMessage:
- 如果响应成功 (HTTP 200),则返回响应。
- 如果响应失败(HTTP 非 200),复原管道会采用配置的复原策略。
虽然这是一个简单的示例,但它演示了如何使用复原策略来处理暂时性错误。 有关详细信息,请参阅 Polly 文档:策略。
已知问题
以下部分详细介绍了各种已知问题。
与 Grpc.Net.ClientFactory
包的兼容性
如果使用的是 Grpc.Net.ClientFactory
版本 2.63.0
或更早版本,则为 gRPC 客户端启用标准复原或对冲处理程序可能会导致运行时异常。 具体而言,请考虑以下代码示例:
services
.AddGrpcClient<Greeter.GreeterClient>()
.AddStandardResilienceHandler();
前面的代码将生成以下异常:
System.InvalidOperationException: The ConfigureHttpClient method is not supported when creating gRPC clients. Unable to create client with name 'GreeterClient'.
若要解决此问题,建议升级到 Grpc.Net.ClientFactory
版本 2.64.0
或更高版本。
生成时检查会验证您使用的是 Grpc.Net.ClientFactory
版本 2.63.0
还是更早版本,并且检查是否生成编译警告。 可以通过在项目文件中设置以下属性来禁止显示警告:
<PropertyGroup>
<SuppressCheckGrpcNetClientFactoryVersion>true</SuppressCheckGrpcNetClientFactoryVersion>
</PropertyGroup>
与 .NET Application Insights 的兼容性
如果使用 .NET Application Insights,则在应用程序中启用复原能力功能可能会导致所有 Application Insights 遥测数据丢失。 在 Application Insights 服务之前注册复原能力功能时,会出现此问题。 请考虑以下导致问题的示例:
// 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();
此问题是由 Application Insights 中的以下错误引起的,可以通过在复原能力功能之前注册 Application Insights 服务来修复,如下所示:
// We register Application Insights first, and now it will be working correctly.
services.AddApplicationInsightsTelemetry();
services.AddHttpClient().AddStandardResilienceHandler();