在 ASP.NET Core Web API中使用 Polly 構建彈性容錯的微服務

溪源More發表於2021-10-25

在 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

相關文章