实现断路器模式

小窍门

此内容摘自电子书《适用于容器化 .NET 应用程序的 .NET 微服务体系结构》,可以在 .NET Docs 上获取,也可以下载免费的 PDF 以供离线阅读。

适用于容器化 .NET 应用程序的 .NET 微服务体系结构电子书封面缩略图。

如前所述,应处理那些可能出现的故障,因为这些故障可能需要一定的时间才能恢复,比如在尝试连接到远程服务或资源时可能出现的情况。 处理这种类型的故障可以提高应用程序的稳定性和复原能力。

在分布式环境中,由于暂时性故障(例如网络连接缓慢和超时),或者资源响应缓慢或暂时不可用,对远程资源和服务的调用可能会失败。 这些故障通常在短时间后自行更正,而可靠的云应用程序应准备好使用“重试模式”等策略来处理它们。

但是,在某些情况下,故障是由于意外的事件,可能需要更长的时间才能修复。 这些故障轻则导致部分连接中断,重则导致服务完全瘫痪。 在这些情况下,应用程序不断重试那些不太可能成功的操作可能是没有意义的。

应编写代码使应用程序能够接受操作失败,并相应地进行处理。

如果不恰当地使用 Http 重试,可能会在自己的软件中产生拒绝服务 (DoS) 攻击。 由于微服务失败或性能缓慢,多个客户端可能会重复重试失败的请求。 这会产生以故障服务为目标的流量呈指数级增长的危险风险。

因此,你需要某种防御屏障,以便在不值得继续尝试时,过多的请求停止。 这种防御屏障正是断路器。

断路器模式与“重试模式”的目的和用途不同。 “重试模式”使应用程序能够重试作,以预期作最终会成功。 断路器设计模式可防止应用程序执行可能失败的操作。 应用程序可以组合这两种模式。 但是,重试逻辑应对断路器返回的任何异常敏感,如果断路器指示故障不是暂时性的,则重试逻辑应放弃重试尝试。

使用 IHttpClientFactory 和 Polly 实现断路器模式

在实现重试操作时,建议的方法为断路器使用 Polly 这种经过验证的 .NET 库以及它与 IHttpClientFactory 的本机集成。

在使用 IHttpClientFactory 时,将断路器策略添加到 IHttpClientFactory 传出中间件管道就像将单个增量代码片段添加到已有代码一样简单。

此处用于 HTTP 调用重试的代码的唯一补充是将断路器策略添加到要使用的策略列表中的代码,如以下增量代码所示。

// Program.cs
var retryPolicy = GetRetryPolicy();
var circuitBreakerPolicy = GetCircuitBreakerPolicy();

builder.Services.AddHttpClient<IBasketService, BasketService>()
        .SetHandlerLifetime(TimeSpan.FromMinutes(5))  // Sample: default lifetime is 2 minutes
        .AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>()
        .AddPolicyHandler(retryPolicy)
        .AddPolicyHandler(circuitBreakerPolicy);

AddPolicyHandler() 方法将策略添加至将要使用的 HttpClient 对象。 在这种情况下,它会为断路器添加 Polly 策略。

若要采用更模块化的方法,断路器策略是在一 GetCircuitBreakerPolicy()个名为单独的方法中定义的,如以下代码所示:

// also in Program.cs
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}

在上面的代码示例中,配置断路器策略,以便在重试 Http 请求时发生五个连续故障时中断或打开线路。 发生这种情况时,线路将中断 30 秒:在此期间,断路器会立即阻止呼叫,而不是实际接通。 该策略会自动将 相关异常和 HTTP 状态代码 解释为错误。

如果一个特定资源出现问题,且该资源部署在不同于执行 HTTP 调用的客户端应用程序或服务的环境中,则还应使用断路器将请求重定向到回退基础结构。 这样,如果数据中心发生中断,仅影响后端微服务,但不影响客户端应用程序,客户端应用程序可以重定向到回退服务。 Polly 正在规划新策略,用于自动实现此故障转移策略方案。

所有这些功能均适用于从 .NET 代码内部管理故障转移的情况,与由 Azure 自动管理的情况相反(位置透明化)。

从使用的角度来看,使用 HttpClient 时,无需在此处添加新内容,因为代码与使用HttpClientIHttpClientFactory时相同,如前面的部分所示。

在 eShopOnContainers 中测试 Http 重试和断路器

每当在 Docker 主机中启动 eShopOnContainers 解决方案时,它都需要启动多个容器。 某些容器启动和初始化速度较慢,例如 SQL Server 容器。 首次将 eShopOnContainers 应用程序部署到 Docker 时尤其如此,因为它需要设置映像和数据库。 事实上,某些容器的启动速度比其他容器慢,可能会导致其余服务最初引发 HTTP 异常,即使你在 docker-compose 级别设置了容器之间的依赖关系,如前几节中所述。 容器之间的这些 Docker Compose 依赖关系仅存在于进程级别。 容器的入口点进程可能已启动,但 SQL Server 可能尚未准备好进行查询。 结果可能是一系列错误,应用程序在尝试使用该特定容器时可能会收到异常。

当应用程序部署到云时,在启动时还可能会出现此类错误。 在这种情况下,编排器可能会在集群节点之间均衡容器数量时,将容器从一个节点或虚拟机(VM)移动到另一个节点或虚拟机(即启动新实例)。

启动所有容器时,“eShopOnContainers”解决这些问题的方式是使用前面所示的重试模式。

在 eShopOnContainers 中测试断路器

有几种方法可以中断/打开线路,并使用 eShopOnContainers 对其进行测试。

一个选项是将断路器策略中允许的重试次数降低到 1,并将整个解决方案重新部署到 Docker。 很有可能,在单次重试时,部署期间 HTTP 请求会失败,断路器将打开,从而导致你收到错误。

另一种方法是使用 Basket 微服务中实现的自定义中间件。 启用此中间件后,它会捕获所有 HTTP 请求并返回状态代码 500。 可以通过向失败 URI 发出 GET 请求来启用中间件,如下所示:

  • GET http://localhost:5103/failing
    此请求返回中间件的当前状态。 如果启用了中间件,则请求返回状态代码 500。 如果中间件已禁用,则没有响应。

  • GET http://localhost:5103/failing?enable
    此请求启用中间件。

  • GET http://localhost:5103/failing?disable
    此请求禁用中间件。

例如,应用程序运行后,可以通过在任何浏览器中使用以下 URI 发出请求来启用中间件。 请注意,订购微服务使用端口 5103。

http://localhost:5103/failing?enable

然后,可以使用 URI http://localhost:5103/failing检查状态,如图 8-5 所示。

检查失败中间件模拟状态的屏幕截图。

图 8-5. 检查“失败”ASP.NET 中间件的状态 - 在这种情况下,已禁用。

每当您调用购物篮微服务时,它都会返回状态代码500。

中间件运行后,可以尝试从 MVC Web 应用程序发出订单。 由于请求失败,线路将打开。

在下面的示例中,您会看到 MVC Web 应用程序在处理下单逻辑时包含一个捕获块。 如果代码捕获了开放线路异常,则会向用户显示一条友好消息,告知他们等待。

public class CartController : Controller
{
    //…
    public async Task<IActionResult> Index()
    {
        try
        {
            var user = _appUserParser.Parse(HttpContext.User);
            //Http requests using the Typed Client (Service Agent)
            var vm = await _basketSvc.GetBasket(user);
            return View(vm);
        }
        catch (BrokenCircuitException)
        {
            // Catches error when Basket.api is in circuit-opened mode
            HandleBrokenCircuitException();
        }
        return View();
    }

    private void HandleBrokenCircuitException()
    {
        TempData["BasketInoperativeMsg"] = "Basket Service is inoperative, please try later on. (Business message due to Circuit-Breaker)";
    }
}

下面是摘要。 重试策略尝试多次发出 HTTP 请求并获取 HTTP 错误。 当重试次数达到断路器策略设置的最大数目时(在本例中为 5),应用程序将引发 BreakCircuitException。 结果是一条友好的消息,如图 8-6 所示。

MVC Web 应用的屏幕截图,其中包含购物篮服务无法正常运行的错误。

图 8-6. 断路器向 UI 返回一个错误

可以为何时打开/中断线路实现不同的逻辑。 或者,如果存在备用数据中心或冗余后端系统,则可以尝试向不同的后端微服务发送 HTTP 请求。

最后,针对 CircuitBreakerPolicy 的另一种可能操作是使用 Isolate(强制打开线路并保持为打开状态)和 Reset(再次关闭路线)。 这些可以用于构建一个实用的 HTTP 端点,以便在策略上直接调用隔离和重置功能。 还可以在生产中适当保护此类 HTTP 终结点,以暂时隔离下游系统,例如,在想要升级它时。 或者,它可以手动断开电路,以保护你怀疑有故障的下游系统。

其他资源