使用Polly构建可复原的.NET应用程序

目录

介绍

什么是分布式架构?

如果其中一个组件无响应,会发生什么情况?

手头的问题到底是什么?

安装环境

部署有缺陷的服务

信息

部署调用服务

信息 1

信息 2

重要

运行应用程序

什么是Polly?

上一个方案中的问题是什么?

安装 Polly

配置Polly

处理失败

什么是暂时性故障?

什么是重试策略?

模拟瞬态错误

使用Polly实现重试策略


介绍

微服务是一种体系结构风格,它将软件应用程序构建为小型、独立和松散耦合服务的集合。在微服务体系结构中,应用程序被分解为一组可独立部署的服务,每个服务代表特定的业务功能。这些服务可以独立开发、部署和扩展,从而实现更大的灵活性、可维护性和可伸缩性。

然而,这种新兴的范式带来了一些挑战,特别是在监督分布式架构和在其中一个微服务遇到性能问题时制定有效响应的领域。通常,这些方案最初在项目开始时被忽略,甚至在应用程序过渡到生产环境时偶尔也会被忽略。然后,它们被匆忙处理,通常是在用户投诉引发的紧急情况或深夜情况下。

在本系列中,我们将探索如何使用专为这些挑战设计的强大库来熟练处理此类问题。我们将深入研究使用Azure FunctionsC#中实现实际用例,观察它们在遇到问题时的响应方式。输入Polly

以下关于这个主题的教科书是永恒的经典,值得每个开发人员在书架上占有一席之地。它超越了弹性模式,涵盖了无数广泛和一般的实际用例。

本文最初发布在这里

什么是分布式架构?

分布式体系结构是指一种系统设计,其中软件应用程序的组件或模块分布在多台计算机或服务器上,通常分布在地理上。与传统的单体架构不同,在传统的单体架构中,所有组件都紧密互连并驻留在单个平台上,分布式架构将工作负载和功能分布在网络中的各个节点之间。

分布式架构的常见示例包括微服务架构,其主要目标是通过利用多个互连系统的功能来增强可伸缩性、提高性能和可靠性。为了更深入地探讨与实现微服务无服务器体系结构相关的所有挑战和复杂性,我们鼓励读者参考我们关于该主题的专门文章(如何在Azure上实现无服务器体系结构)。

下图说明了分布式体系结构的示例。

如果其中一个组件无响应,会发生什么情况?

冒着听起来像是破纪录的风险,我会再说一遍:期待失败。
Nygard Release It!:设计和部署生产就绪型软件

继续上述示例,如果帐户服务遇到延迟,可能会产生什么后果?

  • 用户开始尝试访问StoreFront应用程序中的页面。
  • 随后,应用程序分配一个新线程来处理此请求,并努力与各种服务(包括帐户服务)建立通信。
  • 帐户服务无响应,因此负责处理请求的线程将挂起,拼命等待服务器的响应。令人遗憾的是,在这段拖延期间,它无法执行任何其他任务。
  • 当其他用户访问该站点时,应用程序会分配一个线程来管理新请求。但是,由于帐户服务的无响应,此线程也挂起,因此会出现类似的情况。因此,连续请求会受到服务停机时间的影响。
  • 此循环反复持续,直到没有更多线程可用于处理新请求。在某些云平台中,可以部署StoreFront应用程序的其他实例作为解决方法。但是,它们会遇到相同的问题,导致许多服务器等待响应,所有服务器都具有阻塞的线程。更糟糕的是,这些服务器从云平台产生费用,所有这些都源于单个服务的不可用。

即使帐户服务不是主要煽动者,例如当帐户数据库遇到无响应时,也会出现此类问题。

每当Account服务需要来自此数据库的信息时,它都会分配一个线程,而该线程又会因数据库故障而挂起。因此,帐户服务中的所有线程都会被占用,等待响应,从而导致帐户服务开始挂起的先前情况。

这些图举例说明了连锁反应和级联故障,其中一层中的中断会在调用层中引发相应的中断。

一个明显的例子是数据库故障。如果整个数据库集群变暗,则调用该数据库的任何应用程序都将遇到某种问题。接下来会发生什么取决于调用方的写入方式。如果调用方处理不当,则调用方也将开始失败,从而导致级联故障。(就像我们把树倒过来画,树根指向天空一样,我们的问题会层层叠叠地向上层叠。
Nygard Release It!:设计和部署生产就绪型软件

手头的问题到底是什么?

在我们的方案中,当其中一个组件发生故障时,问题就会出现。但是,从根本上说,每当我们需要访问平台或服务器场中的其他资源时,就会出现这种情况。当多个服务器参与到该过程中时,这在分布式架构中很常见,遇到复杂情况的风险就会增加。这些潜在的故障点被标识为集成点:集成点是两台计算机之间的无数连接。

  • 集成点可以包含Web应用程序和服务之间的连接。
  • 它可以包含服务和数据库之间的连接。
  • 它可以包含应用程序内发生的任何HTTP请求。

因此,分布式系统在通信、数据一致性、容错和整体系统复杂性方面提出了独特的挑战。有效的设计和实施对于利用分销的好处,同时减轻与之相关的潜在挑战至关重要。

幸运的是,有既定的模式和最佳实践来缓解此类问题。可以使用通用数据结构来规避挑战,更有利的是存在一个库,其中所有内容都是预先实现的,无需手动实现。下一篇文章将深入探讨该库的介绍。

安装环境

部署有缺陷的服务

  • 创建一个新解决方案,并添加其中命名为EOCS.Polly.FaultyService的新Azure Function项目。

  • 例如,添加一个名为FaultyService.cs的新类,并向其添加以下代码。

public class FaultyService
{
    public FaultyService()
    {
    }

    [FunctionName(nameof(GetNeverResponding))]
    public async Task<IActionResult> GetNeverResponding
     ([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] 
       HttpRequest req, 
       ILogger log)
    {
        while (true)
        {

        }

        return new OkResult();
    }
}

信息

事实上,这段代码缺乏显著的复杂性,唯一值得注意的方面是请求永远不会结束。

  • 添加一个名为StartUp.cs的新类,并向其添加以下代码:

[assembly: WebJobsStartup(typeof(StartUp))]
namespace EOCS.Polly.FaultyService
{
    public class StartUp : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {            
        }
    }
}

  • 运行程序并记下url。

部署调用服务

  • 在同一解决方案中,添加一个名为EOCS.Polly.CallingService的新Azure Function项目。
  • 例如,添加一个名为CallingService.cs的新类,并向其添加以下代码。

public class CallingService
{
    public CallingService()
    {
    }

    [FunctionName(nameof(GetAccountById))]
    public async Task<IActionResult> GetAccountById
    ([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] 
      HttpRequest req, 
      ILogger log)
    {
        var client = new HttpClient();
        var response = await client.GetAsync
                       ("http://localhost:7271/api/GetNeverResponding");

        return new OkResult();
    }
}

信息 1

在实际场景中,URL应该存储在配置文件中,并通过依赖注入获取。

信息 2

上面的代码只是向我们有缺陷的服务发起一个HTTP请求。目的是检查服务无响应时的后果。

  • 添加一个名为StartUp.cs的新类,并向其添加以下代码。

[assembly: WebJobsStartup(typeof(StartUp))]
namespace EOCS.Polly.CallingService
{
    public class StartUp : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {            
        }
    }
}

最终配置应如下图所示。

重要

不要忘记正确配置启动项目。

运行应用程序

现在,我们将通过Fiddler(或Postman)执行GET请求来测试我们的应用程序,并观察随后的结果。

  • 启动应用程序。
  • 执行以下请求。

很明显,这个请求似乎没有回应。

正在发生的情况与我们之前说明的情况完全一致:由于下游服务遇到延迟,调用线程被挂起。如何应对这种情况?波莉来救援!

什么是Polly

Polly是一个弹性和暂时性故障处理库,旨在通过提供定义和实现故障处理逻辑的策略来帮助开发人员处理其应用程序中的故障。Polly允许开发人员为各种场景定义策略,例如处理瞬态故障、重试、超时和断路(后续文章中会详细介绍)。

Polly在微服务的上下文中特别有益,我们现在将探讨它的实际实现。

上一个方案中的问题是什么?

问题源于从未响应的服务。由于调用代码未强制超时,线程被挂起。游戏结束。

超时是一种简单的机制,一旦您认为答案不会到来,您就可以停止等待答案。(...)任何阻塞线程的资源池都必须有一个超时,以确保调用线程最终取消阻塞,无论资源是否可用。
Nygard Release It!:设计和部署生产就绪型软件

安装 Polly

  • 将Polly Nuget包添加到EOCS.Polly.CallingService项目。

重要

对于这个系列,我们使用的是Polly版本8

配置Polly

存在各种方法来解决集成点中的网络问题或故障带来的挑战。在Polly中,这些方法被称为弹性策略(以前称为策略)。这些策略也可以结合起来;例如,我们可以实现超时策略,并在发生故障时回退到默认值。这引入了弹性管道的概念,它是用于管理请求的多种策略的合并。

信息

其他详细信息可以在文档(此处)中找到。

  • 编辑GetAccountById方法中的代码。

[FunctionName(nameof(GetAccountById))]
public async Task<IActionResult> GetAccountById
  ([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, 
    ILogger log)
{
    var pipeline = new ResiliencePipelineBuilder().AddTimeout
                   (TimeSpan.FromSeconds(5)).Build();
                
    // Execute the pipeline asynchronously
    var response = await pipeline.ExecuteAsync(async token => 
    {
        var client = new HttpClient();
        return await client.GetAsync
        ("http://localhost:7271/api/GetNeverResponding", token).ConfigureAwait(false);
    });

    return response.IsSuccessStatusCode ? new OkResult() : new BadRequestResult();
}

通过Fiddler执行请求后,我们现在会收到响应。

信息 1

现在,我们确实收到了响应,即使它可能是错误500。需要注意的是,Polly并没有神奇地解决有缺陷的服务的问题,而是阻止了这个问题在整个系统中传播:这种现象就是所谓的弹性。在这种情况下,响应可能不是预期的响应,但至关重要的是,调用线程不会被阻止。必须承认,及时解决持续存在的问题的责任仍然在于我们。

信息 2

在实际场景中,我们可能更愿意使用依赖注入,而不是为每个请求创建管道。

处理失败

在前面的代码中,我们满足于返回错误500,但这样的错误相当通用,并且无法提供对潜在问题的太多见解。向开发人员提供其他信息会更有益,尤其是当我们确定已发生超时时。

  • 编辑GetAccountById方法中的代码。

[FunctionName(nameof(GetAccountById))]
public async Task<IActionResult> GetAccountById
    ([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] 
      HttpRequest req, 
      ILogger log)
{
    var pipeline = new ResiliencePipelineBuilder().AddTimeout
                   (TimeSpan.FromSeconds(5)).Build();

    try
    {
        // Execute the pipeline asynchronously
        var response = await pipeline.ExecuteAsync(async token =>
        {
            var client = new HttpClient();
            return await client.GetAsync
                   ("http://localhost:7271/api/GetNeverResponding", token);
        });

        return new OkResult();
    }
    catch (TimeoutRejectedException)
    {
        return new BadRequestObjectResult
               ("A timeout has occurred while processing the request.");
    }
}

通过Fiddler执行请求后,我们现在收到错误400

因此,我们在一个非常基本的场景中探索了Polly的安装和配置。但是,这种情况有些微不足道(从严格意义上讲,没有必要使用Polly进行简单的超时)。展望未来,我们将把重点放在研究更复杂的场景上。

什么是暂时性故障?

暂时性故障是暂时的,通常是系统中发生的短期错误或问题,但并不表示存在永久性问题。这些故障通常是暂时的,这意味着它们可能会在短暂或随后的尝试后自行解决。暂时性故障的常见示例包括临时网络问题、间歇性服务不可用或瞬时资源限制。

在分布式系统的上下文中,各种组件通过网络进行通信,暂时性故障可能更为普遍。这些故障通常是不可预测的,并且可能是由于网络拥塞、临时服务器不可用或资源利用率短暂峰值等因素而发生的。

什么是重试策略?

重试策略是一种用于自动重试最初失败的操作的机制。此方法涉及多次连续尝试执行同一操作,并期望后续尝试可能会成功,尤其是在失败是暂时性或由于间歇性问题的情况下。重试策略旨在通过提供一种无需手动干预即可从暂时性故障中恢复的机制来提高应用程序的复原能力和可靠性。

Polly的上下文中,重试策略涉及定义一种策略,该策略指定应发生重试的条件、最大重试次数以及连续重试尝试之间的持续时间。这在处理暂时性故障、网络故障或其他可能导致操作暂时失败的间歇性问题时特别有用。

模拟瞬态错误

  • 编辑FaultyService类。

public class FaultyService
{
    // ...

    [FunctionName(nameof(GetWithTransientFailures))]
    public async Task<IActionResult> GetWithTransientFailures
    ([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] 
      HttpRequest req, 
      ILogger log)
    {
        var counter = CounterSingleton.Instance.Increment();

        if (counter % 3 == 1) return new InternalServerErrorResult();
        else return new OkResult();
    }
}

public class CounterSingleton
{
    private static CounterSingleton _instance;
    private int _index = 0;

    private CounterSingleton() { }

    public static CounterSingleton Instance
    {
        get
        {
            if (_instance == null) _instance = new CounterSingleton();
            return _instance;
        }
    }

    public int Increment()
    {
        _index++;
        return _index;
    }
}

在这里,我们通过每三次尝试中有一次故意触发错误请求来模拟暂时性故障。此特定操作使用作为单一实例实现的计数器执行。

  • 编辑CallingService类以调用此方法。

public class CallingService
{
    public CallingService()
    {
    }

    [FunctionName(nameof(GetAccountById02))]
    public async Task<IActionResult> GetAccountById02([HttpTrigger
    (AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
    {
        var client = new HttpClient();
        var response = await client.GetAsync
        ("http://localhost:7271/api/GetWithTransientFailures").ConfigureAwait(false);

        return response.IsSuccessStatusCode ? new OkResult() : 
                                              new InternalServerErrorResult();
    }
}

如果发生错误,此请求将返回错误500,当一切按预期进行时,此请求将返回典型错误200

通过Fiddler执行此请求后,我们确实可以观察到每三次尝试中发生一次错误。

信息

在我们的具体案例中,我们的代码是确定性的,这意味着错误不是真正的瞬态。在现实生活中,此类错误通常会随机出现。但是,我们出于说明目的使用此模拟。

使用Polly实现重试策略

等待和重试策略通常用于处理暂时性错误,Polly提供了一种方便的方法来轻松实现此类策略。

  • 编辑CallingService类以实现重试策略。

[FunctionName(nameof(GetAccountById02))]
public async Task<IActionResult> GetAccountById02([HttpTrigger
 (AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
{
    var options = new RetryStrategyOptions<HttpResponseMessage>()
    {
        Delay = TimeSpan.Zero,
        MaxRetryAttempts = 3,
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>().HandleResult
        (response => response.StatusCode == HttpStatusCode.InternalServerError),
    };

    var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>().AddRetry
                   (options).Build();

    // Execute the pipeline asynchronously
    var response = await pipeline.ExecuteAsync(async token =>
    {
        var client = new HttpClient();
        return await client.GetAsync
        ("http://localhost:7271/api/GetWithTransientFailures", token);
    });

    return response.IsSuccessStatusCode ? new OkResult() : new BadRequestResult();
}

在这里,我们实现了最多3次尝试的重试策略。这意味着,如果发生暂时性故障,调用服务不会立即返回错误;相反,它将重新尝试处理请求至少3次。

通过Fiddler执行此请求后,我们现在可以观察到这些暂时性故障得到了有效处理。

信息

其他配置选项可用于自定义重试策略,包括最大尝试次数和两次重试之间的延迟设置。有关更全面的详细信息,请参阅文档。

不过,重试策略确实有一个缺点:如果远程服务完全关闭,我们仍将尝试访问它,从而消耗调用函数中的线程。弹性战略必须考虑到这一点并实施熔断机制。我们将探索如何使用Polly快速实现它。但是,为了避免本文过载,对此实现感兴趣的读者可以在此处找到续篇。

https://www.codeproject.com/Articles/5376166/Building-Resilient-NET-Applications-with-Polly

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值