在 ASP.NET Core Web API中使用 Polly 构建弹性容错的微服务

在 ASP.NET Core Web API中使用 Polly 构建弹性容错的微服务

https://procodeguide.com/programming/polly-in-aspnet-core/​

在本文中,我们将了解如何在微服务中实现容错,即在 ASP.NET Core 中使用 Polly 构建弹性微服务(Web API)。通过在微服务中实现容错,我们确保在其中一项服务发生任何故障时,整个系统不会受到影响。

在本文中,我不会介绍如何在 ASP.NET Core 中构建微服务,因为我已经在我的另一篇关于使用 ASP.NET Core的微服务的文章中详细介绍了这一点。在这里,我们将看到如何在 ASP.NET Core 中使用 Polly 在微服务中实现容错。

无论您是在处理微服务还是单体应用程序,很有可能需要调用外部第三方或内部 API,因此您需要以一种可以处理该 API 故障的方式构建您的代码作为您的应用程序流取决于来自该 API 的响应。

在应用程序或弹性 Web 服务中构建弹性意味着我们需要确保 Web 服务始终可用且具有可接受的功能,即使在服务高负载、网络故障、我们的服务所依赖的其他服务出现故障等情况下,等等。

ASP.NET Core 中的 Polly

目录

什么是 Polly,我们为什么需要它?

处理部分故障的设计原则

Polly 支持的弹性策略

重试

断路器

超时

Bulkhead(隔板)

缓存

回退

策略封装

在 ASP.NET Core 中实现 Polly 的策略

演示的总体方法

创建 ASP.NET Core Web API 项目

创建客户服务

创建订单服务

在 ASP.NET Core 订单服务中配置 Polly 的策略

重试策略

超时策略

回退策略

断路器策略

Bulkhead策略

概括

下载源代码

什么是 Polly,我们为什么需要它?

Polly是一个 .NET 弹性和瞬态故障处理库,允许开发人员以流畅和线程安全的方式表达重试、断路器、超时、隔板隔离和回退等策略。

如果我们说我们已经彻底测试了我们的应用程序并且生产环境中不会出现任何中断,那我们就完全错了。由于应用程序崩溃、响应缓慢、系统负载过大、硬件故障、网络问题等等,将会出现应用程序故障。

为了在我们的应用程序中处理这些故障,首先我们必须承认这些故障会发生,其次我们必须在我们的应用程序中加入容错,即我们确保整个系统不会由于一个或多个服务故障而失败。

例如,微服务是一种设计,其中一个大型应用程序被开发为一组具有自己的数据存储的小型独立服务。通过在微服务中构建容错,我们以这样一种方式设计它,即一个服务的故障不会影响其他服务的工作,即如果与配置文件更新相关的服务关闭,那么用户应该无法更新配置文件,但其他事务如订单输入\查询应该可以正常工作。

此外,构建弹性服务不是为了避免故障,而是关于从故障中恢复并以避免停机和数据丢失的方式执行其功能的能力。微服务应设计为处理部分故障。如果您不设计和实施确保容错的技术,即使是部分故障也可能被放大。

在 ASP.NET Core 中使用 Polly 只需几行代码,我们就可以构建具有弹性的应用程序,尽管在复杂的微服务或基于云的部署中发生部分故障,但仍能顺利运行。在 ASP.NET Core 中使用 Polly 在微服务中实现容错后,我们确保如果服务失败或宕机,整个系统不会宕机。

使用 ASP.NET Core 中的 Polly 策略,我们可以设计我们的应用程序以在出现故障时以指定的方式进行响应。

处理部分故障的设计原则

以下是推荐用于处理微服务中的部分故障的一些设计原则列表

通讯订阅

强烈建议使用异步通信,而不是跨内部微服务的长链同步 HTTP 调用。唯一的同步调用应该是客户端应用程序和入门级微服务或 API 网关之间的前端调用。

可以通过在服务调用中实现重试来避免间歇性网络或通道故障。这些重试应该是有限的次数,不能是无限的。

始终为每个网络调用实现超时。调用客户端不应无休止地等待来自任何服务的响应,而应等待预定义的时间限制,一旦该时间过去,则调用失败。

使用断路器模式,对失败的服务进行重试,如果服务仍然失败,则在一些固定的重试之后,断路器跳闸,以便进一步的尝试立即失败,即不会对失败的服务进行新的调用,而不是它将假定其失败或已关闭。有一个时间限制,不会对失败的服务进行新的调用,一旦时间过去,新的调用将转到失败的服务以验证服务是否再次启动和运行。如果新请求成功,则断路器将关闭,请求将转发到服务。

为失败的服务提供一些回退或默认行为,即如果服务请求失败,则提供一些回退逻辑,如返回缓存数据或默认数据。这可以解决难以实现插入和更新的查询。

对于调用(客户端)的两个微服务之间的通信,微服务应该对来自特定服务的待处理请求数量实施一些限制,即如果已达到限制,则向同一服务发送额外请求可能毫无意义,而是其他请求应立即失败。

Polly 支持的弹性策略

这是 Polly 在 ASP.NET Core 中支持的弹性策略列表:

重试

ASP.NET Core 中 Polly 的这个策略允许我们在调用服务时配置自动重试。

假设我们有一个订单服务,它调用产品服务来获取所订购商品的详细信息。现在,如果产品服务具有大部分时间有效但有时会失败的随机行为。

现在在这种情况下,如果订单服务收到来自产品服务的失败响应,那么重试请求可能会从产品服务中获取结果。

Polly 帮助我们实现了这个重试策略,限制了从订单服务到产品服务的最大重试次数。

断路器

当服务请求失败计数超过某个预先配置的阈值时,ASP.NET Core 中的 Polly 策略帮助我们打破链路,即在配置的时间段内阻止服务请求的执行。

我们将采用相同的订单服务示例,向产品服务请求商品详细信息。现在假设从订单服务到产品服务的请求即使在重试时也不断失败,那么在这种情况下,我们阻止调用产品服务并提供缓存或默认数据。

这种在服务失败达到配置次数时不调用服务并依靠回退机制的设计称为断路器。当订单服务成功连续调用产品服务时,我们说链路关闭(关闭状态)。但是当订单服务不调用产品服务并依赖回退机制时,在这种情况下,我们说链路是开放的(开放状态)。

超时

ASP.NET Core 中 Polly 的这个策略允许我们在对另一个服务的 HTTP 请求期间实现超时,以确保调用者服务不必等待超时。

当订单服务调用产品服务获取商品详细信息时,如果产品服务的响应延迟(产品服务可能正在等待来自慢速/挂起数据库的响应),那么订单服务会假设产品服务在超时时间之外不太可能成功。因此,超出超时期限的订单服务假定产品服务存在问题,它将停止等待产品服务的响应并采取适当的措施。

隔板隔离

ASP.NET Core 中的 Polly 策略允许我们限制应用程序的任何部分可以消耗的资源总量,这样应用程序的失败部分就不会导致级联故障也导致应用程序的其他部分瘫痪。

当订单服务调用产品服务以获取商品详细信息时,如果由于某些原因导致产品服务不可用,那么请求将开始备份订单服务,并可能导致订单服务性能下降甚至导致订单服务崩溃。

Bulkhead Isolation 有助于隔离部分应用程序并控制内存、CPU、套接字、线程等的使用,以便如果您的应用程序的某一部分工作不顺利,此策略将防止该部分影响或停止整个应用程序。

它还允许您指定可以执行的并发请求数量以及可以排队执行的请求数量,以便一旦并发和队列插槽已满,新请求将立即失败。

缓存

ASP.NET Core 中 Polly 中的此策略允许在第一次检索响应时自动将响应存储在缓存中(在内存或分布式缓存中),以便可以从缓存中返回对同一资源的后续请求。

当订单服务为商品详情调用产品服务时,订单服务可以将商品详情存储在缓存中,以便可以从缓存中获取相同产品的下一个请求,而不是再次调用产品服务对于相同的产品。

回退

ASP.NET Core 中 Polly 中的此策略允许我们提供替代路径,即可以返回的值或可以采取的操作,以防被调用的服务关闭,即返回错误或发生超时。

当订单服务调用产品服务以获取商品详细信息时,如果对产品服务的请求失败,则配置的回退将允许订单服务决定在产品服务失败的情况下如何处理。订单服务可以返回默认数据或根据失败采取一些行动。

无论您重试多少次,都必然会发生故障,因此您需要计划在发生故障时应该做什么。回退通常与重试、断路器等其他策略结合使用。

封装策略( Policy Wrap)

ASP.NET Core 中 Polly 中的此策略允许灵活组合 Polly 中支持的任何策略,从而能够组合弹性策略。会有不同类型的故障需要不同的策略,我们可以根据故障类型应用策略组合。

简而言之,当您想同时使用多个策略时,请使用 Policy Wrap。

现在让我们看看如何在 ASP.NET Core 中实现 Polly 支持的这些策略。

在 ASP.NET Core 中实现 Polly 的策略

演示的总体方法

以下是此演示所采用的完整方法的详细信息

  1. 我们将为客户微服务创建第一个 ASP.NET Core Web API 项目,该项目包含一个 Get 操作方法以返回给定客户代码的客户名称

  2. 我们将为订单微服务添加第二个 ASP.NET Core Web API 项目,该项目包含一个 Get 操作方法来返回客户的订单详细信息。

  3. 除了订单详细信息,此订单服务还返回客户名称。要获取此客户名称订单服务,请调用客户服务 get 方法。

  4. 我们已经实现了这个从订单服务到客户服务的 HTTP 调用来获取客户名称。

  5. 我们将在向客户服务发出 HTTP 请求的同时,在订单服务中实施和测试各种 Polly 策略。

  6. 我们将模拟客户服务的故障,并了解如何通过使用 ASP.NET Core 中的 Polly 策略让我们的订单服务容错。

    让我们首先创建所需的 ASP.NET Core Web API 类型的项目,该项目将用于演示如何在 ASP.NET Core 中使用 Polly 的策略

创建 ASP.NET Core Web API 项目

为了演示 Polly 策略在 ASP.NET Core 中的实现,我们将创建几个 ASP.NET Core Web API 项目并按照下面指定的详细信息配置它们。

创建客户服务

创建一个名为 ProCodeGuide.Polly.Customer 的 ASP.NET Core Web API 类型的新项目

创建项目后,默认的 WeatherForecast 控制器已被删除,因为演示不需要它。

添加客户控制器

我们需要添加一个 Customer 控制器,该控制器将有一个 get 操作方法,该方法根据输入的客户代码返回客户名称。我们将添加 Controllers\CustomerController.cs 如下所示

[Route("api/[controller]")]
[ApiController]
public class CustomerController : ControllerBase
{
    private Dictionary<int, string> _customerNameDict = null;

    public CustomerController()
    {
        if(_customerNameDict == null)
        {
            _customerNameDict = new Dictionary<int, string>();
            _customerNameDict.Add(1, "Pro Code Guide");
            _customerNameDict.Add(2, "Support - Pro Code Guide");
            _customerNameDict.Add(3, "Sanjay");
            _customerNameDict.Add(4, "Sanjay - Pro Code Guide");
        }
    }

    [HttpGet]
    [Route("GetCustomerName/{customerCode}")]
    public ActionResult<string> GetCustomerName(int customerCode)
    {
        if (_customerNameDict != null && _customerNameDict.ContainsKey(customerCode))
        {
            return _customerNameDict[customerCode];
        }
        return "Customer Not Found";
    }
}

出于演示目的,我在控制器本身中硬编码了客户代码和名称列表,但理想情况下,这些数据应该来自使用实体框架的数据库。

运行并测试客户服务

从 Visual Studio 构建和运行应用程序后,您应该会从 swagger (OpenAPI) 看到以下屏幕。

在执行获取操作 /api/Customer/GetCustomerName/2 时,您应该从操作方法中获得以下响应。

![运行 ASP.NET Core Web API 项目操作方法](/Users/zouding/文档/blog/文章/如何使用polly/

)

创建订单服务

在同一个解决方案中创建 ASP.NET Core Web API 类型的第二个项目,名称为 ProCodeGuide.Polly.Order

![创建 ASP.NET Core Web API 订单](/Users/zouding/文档/blog/文章/如何使用polly/

)

创建项目后,默认的 WeatherForecast 控制器已被删除,因为演示不需要它。

添加模型

首先在 Models\Item.cs & Models\OrderDetails.cs 中添加 Order details 所需的模型,如下所示

public class Item
{
    public int Id { get; set; }
    public string Name { get; set; }
}
public class OrderDetails
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public DateTime SetupDate { get; set; }
    public List<Item> Items { get; set; }
}
添加订单控制器

我们需要添加一个 Order 控制器,它将有一个 get action 方法,该方法根据输入的客户代码返回详细的订单。此方法还将对客户服务进行 HTTP 调用以获取客户代码的客户名称。

让我们首先在依赖容器中添加 httpclient 服务,以便我们可以在订单控制器中获取该对象 httpclient 以对客户服务进行 HTTP 调用。要在依赖容器中添加 httpclient 服务,请将以下行添加到 Startup.cs 中的 ConfigureServices 方法

services.AddHttpClient();

我们将添加 Controllers\OrderController.cs 如下所示

[Route("api/[controller]")]
[ApiController]
public class OrderController : ControllerBase
{
    private readonly ILogger<OrderController> _logger;
    private readonly IHttpClientFactory _httpClientFactory;
    private HttpClient _httpClient;
    private string apiurl = @"http://localhost:23833/";

    private OrderDetails _orderDetails = null;
    public OrderController(ILogger<OrderController> logger, IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _httpClientFactory = httpClientFactory;

        if (_orderDetails == null)
        {
            _orderDetails = new OrderDetails
            {
                Id = 7261,
                SetupDate = DateTime.Now.AddDays(-10),
                Items = new List<Item>()
            };
            _orderDetails.Items.Add(new Item
            {
                Id = 6514,
                Name = ".NET Core Book"
            });
        }
    }

    [HttpGet]
    [Route("GetOrderByCustomer/{customerCode}")]
    public OrderDetails GetOrderByCustomer(int customerCode)
    {
        _httpClient = _httpClientFactory.CreateClient();
        _httpClient.BaseAddress = new Uri(apiurl);
        var uri = "/api/Customer/GetCustomerName/" + customerCode;
        var result = _httpClient.GetStringAsync(uri).Result;

        _orderDetails.CustomerName = result;

        return _orderDetails;
    }
}

apiurl – 是客户服务的 URL(主机和端口号)

出于演示目的,我对订单详细信息进行了硬编码,即所有客户的相同订单详细信息,但理想情况下,这些数据应该来自使用实体框架的数据库。

使用 Serilog 启用文件日志记录

接下来检查添加 Polly 策略后代码的行为,我们将添加对 Serilog 日志记录的支持以记录到代码中的文件。

使用包管理器控制台将以下包安装到项目中

Install-Package Serilog.AspNetCore
Install-Package Serilog.Settings.Configuration
Install-Package Serilog.Sinks.File

将 Serilog 的配置添加到 appsettings.json 文件中,如下所示

"Serilog": {
  "MinimumLevel": "Information",
  "Override": {
    "Microsoft.AspNetCore": "Information"
  },
  "WriteTo": [
    {
      "Name": "File",
      "Args": {
        "path": "Serilogs\\AppLogs.log"

      }
    }
  ]
}

在 Program.cs 文件中的 CreateHostBuilder 方法中配置 Serilog,如下代码所示

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseSerilog()
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

在 Startup.cs 文件中的 Startup Constructor 中配置 Serilog,如下代码所示

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
    Log.Logger = new LoggerConfiguration()
                    .ReadFrom.Configuration(configuration)
                    .CreateLogger();
}

以上配置会生成日志到路径{Project Path}\Serilogs\AppLogs.log下的文件

如果您想进一步详细了解如何将 Serilog 日志添加到项目中,那么您可以在此处查看我的详细文章

现在我们已经添加了所需的项目并配置了项目,让我们运行并检查项目。由于此订单服务依赖于客户服务,因此我们需要确保在测试时两个项目都已启动并正在运行。为了从 Visual Studio 一起启动这两个项目,我们将对启动项目进行更改。

右键单击解决方案资源管理器中的解决方案文件,然后选择将加载属性屏幕的属性,您可以通过选择多个启动项目选项来配置以同时启动两个项目,如下所示

现在,当您从 Visual Studio 运行项目时,订单和客户服务项目都将启动。

运行并测试订单服务

从 Visual Studio 构建和运行应用程序后,您应该会从 swagger (OpenAPI) 看到以下屏幕。

在执行获取操作 /api/Order/GetOrderByCustomer/2 时,您应该从操作方法中获得以下响应。

现在让我们看看当客户服务不可用时会发生什么,即订单服务没有问题但客户服务没有启动和运行。为了模拟这种情况,我刚刚启动了 Order 服务,但没有启动客户服务,因此客户服务没有启动和运行。

正如我们在上面看到的,当客户服务没有启动并运行时,订单服务也会开始抛出错误。从 Serilog 中,您将能够看到订单服务向客户服务发出请求,该请求返回了异常,因此在级联效果中,订单服务也返回了 500

让我们探讨如何使用 ASP.NET Core 中的 Polly 策略来避免这种行为

在 ASP.NET Core 订单服务中配置 Polly 的策略

要在 ASP.NET Core 中配置 Polly 的策略,您需要在项目中安装 Polly 包。您可以通过在包管理器控制台窗口中运行以下命令来添加 Polly 包

Install-Package Polly

现在我们在订单服务项目中安装了 Polly 包文件,让我们看看如何在我们的 ASP.NET Core Web API(订单服务)项目中使用 Polly 的策略来使我们的订单服务容错,尽管客户服务没有运行或失败。

声明 Polly 策略的方法不止一种,即使用注册表或通过 Startup 添加它们。但是,为了在这篇介绍文章中保持简单,我们将直接在构造函数的控制器类中创建 Polly 策略。

重试策略

根据 name 的定义,此策略建议您需要在第一次尝试失败的情况下重试请求。现在,这些重试必须是固定的次数,因为这种重试业务不可能永远持续下去。此重试策略可让您配置要进行的重试次数。

此重试策略允许在重试之前添加延迟,或者在对失败的服务进行重试调用之前不要等待,因此如果您希望服务中返回错误的问题将立即得到纠正,那么您应该只实现重试逻辑而不任何延误。

考虑从订单服务到客户服务的 HTTP 请求失败的场景。来自客户服务的这个错误可能是永久性的,也可能是暂时的。要处理临时故障,您需要添加逻辑以将请求重试至客户服务至少 2 次,以确保使用重试处理来自客户服务的临时故障。

按照这个重试逻辑,订单服务将向客户服务请求客户名称,如果客户服务返回异常,那么订单服务仍将再向客户服务重试请求 2 次,然后得出结论,现在现在不可能了获得客户服务的成功回复。

要模拟来自客户服务的随机故障,请将以下操作方法添加到客户服务中。这种方法是随机返回数据或错误。为了实现这种随机行为,我们生成一个 1 到 10 之间的数字,如果这个生成的数字是偶数,那么我们将返回一个带有 HTTP 状态代码 500 的服务器错误,如果生成的数字不是偶数,即它是奇数,那么我们将返回根据客户代码使用客户名称的成功响应。

因此,此客户服务操作方法 GetCustomerNameWithTempFailure 将随机运行,即有时会返回错误或在某些情况下会返回成功响应

[HttpGet]
[Route("GetCustomerNameWithTempFailure/{customerCode}")]
public ActionResult<string> GetCustomerNameWithTempFailure(int customerCode)
{
    try
    {
        Random rnd = new Random();
        int randomError = rnd.Next(1, 11);  // creates a number between 1 and 10

        if (randomError % 2 == 0)
            throw new Exception();

        if (_customerNameDict != null && _customerNameDict.ContainsKey(customerCode))
        {
            return _customerNameDict[customerCode];
        }
        return "Customer Not Found";
    }
    catch
    {
        //Log Error
        return StatusCode(StatusCodes.Status500InternalServerError);
    }
}

要在 ASP.NET Core 中使用 Polly 实现重试逻辑,我们需要声明 RetryPolicy 类型的对象并定义策略,如下面的代码所示

//Remaining Code has been removed for readability

private readonly RetryPolicy _retryPolicy;

public OrderController(ILogger<OrderController> logger, IHttpClientFactory httpClientFactory)
{

    //Remaining Code has been removed for readability

    _retryPolicy = Policy
        .Handle<Exception>()
        .Retry(2);
}

上面的代码示例将创建一个重试策略,如果 HTTP 服务调用失败并且策略处理异常,该策略将重试最多两次。在这里,我们指定了重试策略处理通用异常,因此它将重试所有类型的异常,但您甚至可以为更具体的异常(如 HttpRequestException)配置重试策略,然后它只会重试类型为 HttpRequestException 的异常。

接下来,我们将在订单服务中添加一个新的操作方法,它将利用 RetryPolicy 对象向客户服务的新操作方法 (GetCustomerNameWithTempFailure) 发出 HTTP 请求,该方法随机返回错误。重试策略用于处理来自客户服务的随机故障。

[HttpGet]
[Route("GetOrderByCustomerWithRetry/{customerCode}")]
public OrderDetails GetOrderByCustomerWithRetry(int customerCode)
{
    _httpClient = _httpClientFactory.CreateClient();
    _httpClient.BaseAddress = new Uri(apiurl);
    var uri = "/api/Customer/GetCustomerNameWithTempFailure/" + customerCode;
    var result = _retryPolicy.Execute(() => _httpClient.GetStringAsync(uri).Result);

    _orderDetails.CustomerName = result;

    return _orderDetails;
}

RetryPolicy 对象使用委托在 Execute() 委托中对客户服务执行所需的 HTTP 调用。如果 HTTP 调用引发由重试策略处理的异常,则 HTTP 调用将重试配置的次数。

让我们在 ASP.NET Core 中运行和测试 Polly 的重试策略。在 Visual Studio 中运行解决方案后,两个项目(即客户和订单)都应该启动。两个服务都开始转到订购服务后,您应该会看到以下来自 swagger (OpenAPI) 的屏幕

在上面的屏幕上选择操作 /api/Order/GetOrderByCustomerWithRetry/(customerCode) 它应该展开,然后单击 Try it out 按钮。之后,您应该会看到以下屏幕,您需要在其中输入客户代码的值并单击执行按钮。

如上所示,单击执行后,我们根据为客户代码输入的值获得了正确客户名称的成功响应。

但是订单服务中的 GetOrderByCustomerWithRetry 操作正在对客户服务进行 HTTP 调用,该调用会随机返回错误,因此让我们检查日志,看看在客户服务中对 GetCustomerNameWithTempFailure 的 HTTP 调用期间发生了什么

正如我们在上面日志的屏幕截图中看到的,当我们从订单服务调用客户服务时,第一次调用返回了一个错误,但由于我们已经配置了重试策略并且它被重试,并且在第一次重试时客户服务返回成功响应并带有正确的客户名称根据客户代码的价值。因此,通过在订单服务中使用重试策略,我们能够处理客户服务中的临时故障。

超时策略

根据 name 的定义,此策略建议您需要在设置的时间限制内没有来自其他服务的响应的情况下终止请求。

考虑从订单服务到客户服务的 HTTP 请求被延迟的场景。来自客户服务的此错误可能永无止境,因为客户服务可能正在等待来自慢速/挂起数据库的响应或来自第三方服务的响应,并且客户服务尚未为这些调用实现超时。

要处理延迟响应,您需要添加逻辑以在设定的时间限制过后对客户服务的请求超时,以确保订单服务不会无休止地等待客户服务的响应,因为它会使线程永远忙碌。这种无休止的等待也会对订单服务产生级联效应,并可能耗尽订单服务服务器上的所有可用资源。

按照这个超时逻辑,订单服务会向客服请求客户名称,如果客服在设定的时限内没有得到响应,那么订单服务假设现在没有机会得到客户的成功响应服务,因此它终止或超时请求并采取适当的行动并返回响应。

要模拟客户服务响应的延迟,请将以下操作方法添加到客户服务中。此方法在 2 分钟延迟后返回响应。为了实现这种行为,我们使用了 Thread 类中的 sleep 方法,该方法在指定的时间内停止线程执行。

所以这个客户服务操作方法 GetCustomerNameWithDelay 将延迟响应 2 分钟以订购服务。

[HttpGet]
[Route("GetCustomerNameWithDelay/{customerCode}")]
public ActionResult<string> GetCustomerNameWithDelay(int customerCode)
{
    Thread.Sleep(new TimeSpan(0, 2, 0));
    if (_customerNameDict != null && _customerNameDict.ContainsKey(customerCode))
    {
        return _customerNameDict[customerCode];
    }
    return "Customer Not Found";
}

要在 ASP.NET Core 中使用 Polly 实现超时逻辑,我们需要声明 TimeoutPolicy 类型的对象并定义策略,如下面的代码所示

private static TimeoutPolicy _timeoutPolicy;

public OrderController(ILogger<OrderController> logger, IHttpClientFactory httpClientFactory)
{
    _timeoutPolicy = Policy.Timeout(20, TimeoutStrategy.Pessimistic);
}

上面的代码示例将创建一个超时策略,该策略将等待响应 20 秒,在 20 秒后,它将假定不可能成功响应并且将超时请求,即应放弃执行委托或函数。

ASP.NET Core 中 Polly 中的超时策略支持乐观和悲观超时。建议尽可能使用乐观超时,因为它消耗的资源较少。

乐观 - 假设您执行的委托支持取消,并且委托通过抛出异常来表达该超时

悲观 - 认识到在某些情况下您可能需要执行没有内置超时的委托,并且不接受取消,即调用者停止等待底层委托完成

接下来,我们将在订单服务中添加一个新的操作方法,它将使用 TimeoutPolicy 对象向客户服务的新操作方法 (GetCustomerNameWithDelay) 发出 HTTP 请求,该方法返回延迟响应。超时策略用于处理来自客户服务的延迟。

[HttpGet]
[Route("GetOrderByCustomerWithTimeout/{customerCode}")]
public OrderDetails GetOrderByCustomerWithTimeout(int customerCode)
{
    try
    {
        _httpClient = _httpClientFactory.CreateClient();
        _httpClient.BaseAddress = new Uri(apiurl);
        var uri = "/api/Customer/GetCustomerNameWithDelay/" + customerCode;
        var result = _timeoutPolicy.Execute(() => _httpClient.GetStringAsync(uri).Result);

        _orderDetails.CustomerName = result;

        return _orderDetails;
    }
    catch(Exception ex)
    {
        _logger.LogError(ex, "Excpetion Occurred");
        _orderDetails.CustomerName = "Customer Name Not Available as of Now";
        return _orderDetails;
    }
}

TimeoutPolicy 对象使用委托在 Execute() 委托中对客户服务执行所需的 HTTP 调用。如果 HTTP 调用在 20 秒内没有返回响应,即根据超时策略中设置的时间,则 HTTP 调用将终止并引发超时 – operationcancelledexception 和在 catch 块中,我们已将客户名称设置为 '客户名称目前不可用'。这仅用于演示目的,在实践中您将向用户返回错误,同时将错误通知管理员,以便修复。

让我们在 ASP.NET Core 中运行和测试 Polly 的超时策略。在 Visual Studio 中运行解决方案后,两个项目(即客户和订单)都应该启动。两个服务都开始转到订购服务后,您应该会看到以下来自 swagger (OpenAPI) 的屏幕

在上面的屏幕上选择操作 /api/Order/GetOrderByCustomerWithTimeout/(customerCode) 它应该展开,然后单击 Try it out 按钮。之后,您应该会看到以下屏幕,您需要在其中输入客户代码的值并单击执行按钮。

如上所示,点击执行后,根据我们对超时事件的处理,我们得到了客户名称为“客户名称截至目前不可用”的成功响应。

但是订单服务中的 GetOrderByCustomerWithTimeout 操作正在对客户服务进行 HTTP 调用,该调用返回延迟响应,因此让我们检查日志,看看在客户服务中对 GetCustomerNameWithDelay 的 HTTP 调用期间发生了什么

正如我们在上面的日志截图中看到的那样,当我们从订单服务调用客户服务时,由于订单服务超时策略引发的超时事件导致客户服务响应延迟,Polly.Timeout.TimeoutRejectedException 类型的异常是引发并取消操作。在 catch 块中,我们添加了返回成功的代码,但具有自定义客户名称。因此,通过在订单服务中使用超时策略,我们能够处理客户服务的延迟并避免无休止地等待订单服务。

回退策略

根据 name 的定义,此策略表明您需要在调用请求失败的情况下进行一些后备(计划 B)。现在,在这里您可以首先实施重试策略以排除正在调用的服务的临时故障,在所有重试服务也失败之后,您可以有一些后备机制,即在失败的情况下该怎么做。此回退策略允许您在被调用的服务失败时为响应提供替代值(或要执行的替代操作)。

考虑从订单服务到客户服务的 HTTP 请求失败的场景。来自客户服务的这个错误可能是永久性的,也可能是暂时的。现在即使在重试过程中请求也失败了,所以不要因为客户服务失败而导致订单服务失败,而是希望为响应提供一些替代值,以便订单服务可以将其作为响应(而不是失败)和根据该响应执行剩余的代码。

根据此回退逻辑,订单服务将向客户服务请求客户名称,如果客户服务返回异常,则订单服务将使用回退策略中配置的替代值作为来自客户服务的最终响应并处理该响应。

要模拟客户服务中的永久性故障,请将以下操作方法添加到客户服务中。此方法始终返回错误。

所以这个客户服务操作方法 GetCustomerNameWithPermFailure 将抛出异常并且在所有情况下总是返回错误

[HttpGet]
[Route("GetCustomerNameWithPermFailure/{customerCode}")]
public ActionResult<string> GetCustomerNameWithPermFailure(int customerCode)
{
    try
    {
        throw new Exception("Database Not Available");
    }
    catch
    {
        //Log Error
        return StatusCode(StatusCodes.Status500InternalServerError);
    }
}

要在 ASP.NET Core 中使用 Polly 实现回退逻辑,我们需要声明 FallbackPolicy 类型的对象并定义策略,如下面的代码所示

private readonly FallbackPolicy<string> _fallbackPolicy;

public OrderController(ILogger<OrderController> logger, IHttpClientFactory httpClientFactory)
{
    _fallbackPolicy = Policy<string>
                        .Handle<Exception>()
                        .Fallback("Customer Name Not Available - Please retry later");
}

上面的代码示例将创建一个回退策略,如果 HTTP 服务调用因策略处理的异常而失败,则会将响应值替换为“客户名称不可用 - 请稍后重试”。数据类型为字符串 (TResult) 的回退策略被用作客户服务操作,返回字符串(客户名称)作为响应。

您甚至可以将回退策略用于返回无效的调用。在 void 的情况下,如果策略处理错误(而不是替代返回值),它指定要运行的备用操作.Fallback(() => DoSomeFallbackAction())

同样在上面的代码中,我们已经指定回退策略处理通用异常,因此它将为所有类型的异常提供替代值,但您甚至可以为更具体的异常(如 HttpRequestException)配置回退策略,然后它将仅为HttpRequestException 类型的异常。

接下来,我们将在订单服务中添加一个新的操作方法,该方法将使用 FallbackPolicy 对象向返回错误的客户服务的新操作方法 (GetCustomerNameWithPermFailure) 发出 HTTP 请求。回退策略用于通过在发生故障时为响应提供回退或替代值来处理客户服务失败。

[HttpGet]
[Route("GetOrderByCustomerWithFallback/{customerCode}")]
public OrderDetails GetOrderByCustomerWithFallback(int customerCode)
{
    _httpClient = _httpClientFactory.CreateClient();
    _httpClient.BaseAddress = new Uri(apiurl);
    var uri = "/api/Customer/GetCustomerNameWithPermFailure/" + customerCode;
    var result = _fallbackPolicy.Execute(() => _httpClient.GetStringAsync(uri).Result);![](https://img2020.cnblogs.com/blog/191302/202110/191302-20211025232132690-134030604.png)



    _orderDetails.CustomerName = result;

    return _orderDetails;
}

FallbackPolicy 对象使用委托在 Execute() 委托中对客户服务执行所需的 HTTP 调用。如果 HTTP 调用引发了由回退策略处理的异常,则在失败时提供替代值。

让我们在 ASP.NET Core 中运行和测试 Polly 的回退策略。在 Visual Studio 中运行解决方案后,两个项目(即客户和订单)都应该启动。两个服务都开始转到订购服务后,您应该会看到以下来自 swagger (OpenAPI) 的屏幕

在上面的屏幕上选择操作 /api/Order/GetOrderByCustomerWithFallback/(customerCode) 它应该展开,然后单击 Try it out 按钮。之后,您应该会看到以下屏幕,您需要在其中输入客户代码的值并单击执行按钮。

如上所示,点击执行后,我们得到了一个成功的响应(即使客户服务永久失败),客户名称根据为客户代码配置的后备替代值。

但是订单服务中的 GetOrderByCustomerWithFallback 操作正在对客户服务进行 HTTP 调用,该调用返回错误,因此让我们检查日志,看看在客户服务中对 GetCustomerNameWithPermFailure 的 HTTP 调用期间发生了什么。

正如我们在上面日志的屏幕截图中看到的,当我们从订单服务调用客户服务并返回错误但订单服务仍然成功并且回退值用于响应。因此,通过在订单服务中使用回退策略,我们能够处理客户服务中的故障。

断路器策略

此断路器策略建议您需要有某种机制或逻辑来不调用特定服务,以防该服务因前几个请求而永久失败。当服务请求失败计数超过某个预先配置的阈值时,此断路器策略允许您配置为在配置的时间段内阻止对特定失败服务的 HTTP 请求。

考虑从订单服务到客户服务的 HTTP 请求失败的场景。来自客户服务的这个错误可能是永久性的,也可能是暂时的。现在即使在重试期间请求也失败了,因此您使用回退策略为响应提供了一些替代值。但是现在由于很少有连续呼叫客户服务失败,所以一段时间(比如几分钟)你不想浪费时间打电话给客户服务而是假设它会返回一个错误并使用备用响应来处理请求订购服务。

此逻辑假设,如果服务连续失败几次,则该服务存在一些永久性问题,可能需要一些时间来纠正问题。因此,让我们不要浪费时间调用或重试失败的服务,而是采取备用回退路径为服务提供一些时间来恢复。

按照这个断路器逻辑,订单服务会向客服请求客户姓名,如果客服连续2次返回异常,则电路将断开(即电路将打开)1分钟,并持续1分钟订单服务不会打电话给客户服务,而是自己假设客户服务会返回错误。

为了模拟客户服务的永久性故障,我们将使用客户服务中的 GetCustomerNameWithPermFailure 操作,我们用于演示回退策略。

要在 ASP.NET Core 中使用 Polly 实现断路器逻辑,我们需要声明 CircuitBreakerPolicy 类型的对象并定义策略,如下面的代码所示

private static CircuitBreakerPolicy _circuitBreakerPolicy;

public OrderController(ILogger<OrderController> logger, IHttpClientFactory httpClientFactory)
{
    if (_circuitBreakerPolicy == null)
    {
        _circuitBreakerPolicy = Policy.Handle<Exception>()
                                        .CircuitBreaker(2, TimeSpan.FromMinutes(1));
    }
}

上面的代码示例将创建一个断路器策略,该策略定义在调用服务时如果连续 2 次出现异常,则电路将中断(对服务的调用将被阻止)2 分钟的时间跨度。

同样在上面的代码中,我们已经指定断路器策略处理通用异常,因此它会因所有类型的异常而中断,但您甚至可以为更具体的异常(如 HttpRequestException)配置断路器策略,然后它只会因以下异常而中断类型 HttpRequestException。

接下来,我们将在订单服务中添加一个新的操作方法,它将利用断路器策略对象向客户服务的操作方法 (GetCustomerNameWithPermFailure) 发出 HTTP 请求,该方法返回错误。断路器策略用于处理客户服务的故障,在客户服务连续 2 次失败后 1 分钟内不拨打任何电话。

[HttpGet]
[Route("GetOrderByCustomerWithCircuitBreaker/{customerCode}")]
public OrderDetails GetOrderByCustomerWithCircuitBreaker(int customerCode)
{
    try
    {
        _httpClient = _httpClientFactory.CreateClient();
        _httpClient.BaseAddress = new Uri(apiurl);
        var uri = "/api/Customer/GetCustomerNameWithPermFailure/" + customerCode;
        var result = _circuitBreakerPolicy.Execute(() => _httpClient.GetStringAsync(uri).Result);

        _orderDetails.CustomerName = result;
        return _orderDetails;
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Excpetion Occurred");
        _orderDetails.CustomerName = "Customer Name Not Available as of Now";
        return _orderDetails;
    }
}

断路器策略对象使用委托在 Execute() 委托中执行所需的对客户服务的 HTTP 调用。如果 HTTP 调用引发异常,该异常正在由 catch 块处理以提供客户名称的替代值。

让我们在 ASP.NET Core 中运行和测试 Polly 的断路器策略。在 Visual Studio 中运行解决方案后,两个项目(即客户和订单)都应该启动。两个服务都开始转到订购服务后,您应该会看到以下来自 swagger (OpenAPI) 的屏幕

在上面的屏幕上选择操作 /api/Order/GetOrderByCustomerWithCircuitBreaker/(customerCode) 它应该展开,然后单击 Try it out 按钮。之后,您应该会看到以下屏幕,您需要在其中输入客户代码的值并单击执行按钮。

如上所示,点击执行后,我们得到了一个成功的响应(即使客户服务永久失败),客户名称与 catch 块中配置的回退值一致。

但是订单服务中的 GetOrderByCustomerWithCircuitBreaker 操作正在对客户服务进行 HTTP 调用,该调用返回错误,因此让我们检查日志,看看在客户服务中对 GetCustomerNameWithPermFailure 进行 HTTP 调用期间发生了什么

![Polly 与断路器策略](/Users/zouding/文档/blog/文章/如何使用polly/

)

正如我们在上面日志的屏幕截图中看到的那样,当我们从订单服务调用客户服务时,它返回了一个错误,订单服务使用了来自 catch 块的客户名称的替代值。此外,我们可以从日志中看到,当我们在电路打开时尝试致电客户服务时,Polly 没有致电客户服务,而是为该 Policy.CircuitBreaker.BrokenCircuitException 提供了一个异常 – 电路现在已打开并且不允许通话。

Bulkhead策略

要在 ASP.NET Core 中使用 Polly 实现舱壁隔离逻辑,我们需要声明 BulkheadPolicy 类型的对象并定义策略,如下面的代码所示

private static BulkheadPolicy _bulkheadPolicy;

public OrderController(ILogger<OrderController> logger, IHttpClientFactory httpClientFactory)
{
    _bulkheadPolicy = Policy.Bulkhead(3, 6);
}

上面的代码示例将创建一个Bulkhead策略,该策略定义在调用服务时限制调用服务的资源数量,即通过隔板的最多 3 个并行执行和最多 6 个可能正在排队的请求数(等待获取执行槽)随时。

接下来,我们将在 order 服务中添加一个新的 action 方法,它将使用 Bulkhead Isolation Policy 对象向客户服务的 action 方法 (GetCustomerName) 发出 HTTP 请求。隔板隔离策略用于限制用于调用客户服务的资源,即在任何给定时间将有 3 个并行请求执行,另外 6 个请求可以在队列中。这样如果客户服务的响应被延迟或阻塞,那么我们不会在订单服务上使用太多资源,也会导致订单服务发生级联故障。

[HttpGet]
[Route("GetOrderByCustomerWithBulkHead/{customerCode}")]
public OrderDetails GetOrderByCustomerWithBulkHead(int customerCode)
{
    _httpClient = _httpClientFactory.CreateClient();
    _httpClient.BaseAddress = new Uri(apiurl);
    var uri = "/api/Customer/GetCustomerName/" + customerCode;
    var result = _bulkheadPolicy.Execute(() => _httpClient.GetStringAsync(uri).Result);

    _orderDetails.CustomerName = result;

    return _orderDetails;
}

隔板隔离策略对象使用委托在 Execute() 委托中执行所需的对客户服务的 HTTP 调用。

隔板隔离策略适用于一个错误不应导致整艘船瘫痪的策略!即当服务开始失败时,它会建立大量请求,这些请求都在并行缓慢失败,这可能导致订单服务中资源(CPU/线程/内存)的使用,从而降低能力或导致服务失败订购服务。

对于缓存策略,我建议不要实现基于异常缓存数据的逻辑,而是根据数据设计缓存逻辑,即静态/动态数据、常用数据等。您可以阅读我关于在 ASP 中缓存的详细文章。 NET Core在这里

到目前为止,我们已经了解了 ASP.NET Core 中 Polly 的重要策略。此外,可以在 ASP.NET Core 中为单个服务调用组合 Polly 的多个策略,例如

fallback.Wrap(waitAndRetry).Wrap(breaker).Execute(action);

fallback.Execute(() => waitAndRetry.Execute(() =>breaker.Execute(action)));

概括

我们在 ASP.NET Core Web API 中了解了 Polly 的各种策略。我们在本文中看到的只是冰山一角,即我们刚刚开始并查看了策略的基本实施。

您甚至可以尝试组合多个策略,称为策略包装,您可以将重试策略与回退策略或断路器策略相结合。

我们看到了 ASP.NET Core 中 Polly 的实现以及策略的同步方法,所有策略也存在类似的异步方法。

下载源代码

在这里您可以下载作为本文一部分开发的完整源代码,即 ASP.NET Core Web API 中的 Polly

posted @ 2021-10-25 23:23  溪源More  阅读(5551)  评论(6编辑  收藏  举报