构建可复原的 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.SelectPipelineByAuthorityStandardHedgingHandlerBuilderExtensions.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,并且有一个谓词用来处理 RequestTimeoutTooManyRequests HTTP 状态代码。
  • 向复原生成器添加一个超时时间为 5 秒的超时策略。

每个复原策略都有许多选项可用。 有关详细信息,请参阅 Polly 文档:策略。 若要详细了解如何配置 ShouldHandle 委托,请参阅 Polly 文档:响应式策略中的故障处理

动态重载

Polly 支持动态重载已配置的复原策略。 这意味着可以在运行时更改复原策略的配置。 若要启用动态重载,请使用相应的公开 ResilienceHandlerContextAddResilienceHandler 重载。 在给定上下文中,调用相应复原策略选项的 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);
}

前面的代码:

想象一下网络出现故障或服务器无响应的情况。 下图显示了在给定 ExampleClientGetCommentsAsync 方法的情况下,复原策略如何处理该情况:

具有复原管道的示例 HTTP GET 工作流。

上图描述了:

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