当前位置: 首页 > news >正文

Azure设计模式之断路模式

断路模式


在连接远程服务或资源时,遇到故障时的故障恢复时间是不等的。就可以使用断路模式。来提高应用程序的稳定性和弹性。


问题背景
在分布式环境中,对远程资源和服务的调用可能由于故障而失败,例如网络连接速度慢,超时或资源暂时不可用。这些故障通常会在短时间内自行修正,并应通过使用适当策略(如Retry模式)(https://docs.microsoft.com/en-us/azure/architecture/patterns/retry)来处理。


但是,也可能故障是由于意外事件造成的,修复可能需要更长时间。这些故障的严重程度可能导致部分连接失效甚至服务完全失效。在这些情况下,应用程序可能在无意义地重试不太可能成功的操作,应该使快速操作失败并相应地处理此故障。


另外,如果服务非常繁忙,系统的部分故障可能会导致级联故障。例如,调用服务的操作可以实现超时机制,或如果服务在该时间段内没有响应,则回复失败消息。但是,此策略可能导致许多并发请求被阻止,直到超时时间到为止。这些被阻止的请求可能正占用系统的关键资源,如内存,线程,数据库连接等。因此,这些资源可能会耗尽,导致系统其他需要请求相同资源的部分出现故障。在这种情况下,最好使操作立即失败,而只有在可能成功的情况下才试图调用该服务。而且,设置较短的超时可能有助于解决此问题,但超时不应太短,以至于大多数时间内操作失败,即使对服务的请求最终会成功。


解决方案


断路器模式可以防止应用程序重复执行很可能失败的操作。它的目的是不必等待长时间未处理故障而浪费CPU资源。断路器模式还使应用程序能够检测故障是否已解决。如果问题已经修复,应用程序可以尝试再次调用。


断路器模式的目的不同于重试模式。重试模式使应用程序能够重试一个操作,期望它会成功。断路器模式的目的是防止应用程序执行可能失败的操作。应用程序可以组合使用重试模式和断路器模式。然而,如果断路器返回的故障不是瞬态的,则重试器应对断路器返回的异常敏感,并放弃重试。


断路器作为可能失败操作的代理。应监视最近发生的故障数,并使用此信息来决定是否允许操作继续,或者立即返回异常。




代理可以实现为具有类似电路断路器功能的状态机:


关闭:应用程序的请求被路由到操作。代理中维护最近故障数量的计数,如果对操作的调用不成功,代理会增加此计数。如果最近的故障数量在给定的时间段内超过了指定的阈值,则代理将被置于打开状态。此时,代理启动一个超时定时器,当该定时器到期时,代理被置于半打开状态。
超时定时器的目的是让系统有时间来解决导致故障的问题,然后再次允许应用程序尝试执行该操作。


打开:应用程序的请求立即失败,返回异常给应用程序。


半开:允许来自应用程序的有限数量的请求执行调用操作。如果这些请求成功,则假定先前的故障已经修复,断路器切换到关闭状态(故障计数器被复位)。如果任何请求失败,断路器假定故障仍然存在,它将恢复到打开状态并重新启动超时定时器,给系统更多的时间从故障中恢复。
半开状态有助于防止恢复服务突然被请求压垮。随着服务的恢复,它可能只能支持有限数量的请求,直到恢复完成,但是在恢复正在进行中,大并发请求可能导致服务超时或失败。





在图中,由关闭状态使用的故障计数器是基于时间的。它会周期性的自动重置。防止断路器遇到偶然故障时进入打开状态。断路器所要切换到断开状态的故障阈值仅当指定间隔内发生指定数量的故障时才算达到。半开状态下使用的计数器记录调用成功的次数。在连续完成指定数量的成果调用后,断路器将恢复为闭合状态。如果任何调用失败,断路器将立即进入打开状态,下一次进入半开状态时,半开计数器被复位。


系统恢复的方式是从外部修复,可能是通过恢复或重新启动有故障组件或修复网络连接。


当系统从故障中恢复并最大限度地减少对性能的影响,断路器提供稳定性。它可以帮助快速拒绝对很可能失败的操作请求来保持系统的响应时间,而不是等待操作超时或永不返回。如果断路器在每次改变状态时触发事件并记录,则该信息可用于监视由断路器保护的系统部分的健康状况,或者当断路器切换到打开状态时可以向管理员发出警报。


该模式是可定制的,可以根据故障类型进行调整。例如,可以将越来越多的超时定时器应用于断路器。最初可以将断路器置于打开状态几秒钟,然后如果故障未解决,则将超时时间延长至几分钟,依此类推。在某些情况下,在断路器是打开状态时返回对应用程序有意义的默认值更很有用,相比返回失败或异常。




问题和思考
考虑如何实现这种模式时,您应该考虑以下几点:
异常处理。如果操作不可用,应用程序需要被断路器来调用处理异常。处理异常的方式将是具体的应用程序。例如,应用程序可能会暂时停止了某功能,则调用替代操作来尝试执行相同任务,或向用户报告异常,请他们稍后再试。


异常类型。由于许多原因,会导致请求失败,其中一些故障可能更严重。例如,请求可能会失败,因为远程服务已经崩溃,并且需要几分钟的时间才能恢复,或者因为服务暂时负载过大而导致超时。断路器要对不同异常类型和性质来调整策略。例如,与由于服务完全不可用的故障次数相比,可能需要更多的超时异常来将断路器切换打开状态。


日志记录。断路器应记录所有失败的请求(也可以记录成功的请求),使管理员能够监视系统的健康状况。


可恢复性。应该对断路器进行配置,使其与恢复模式相匹配。例如,如果断路器长时间保持在打开状态,即使解决了故障,也可能引发异常。类似地,如果断路器从开发状态切换到半开状态太快,则可能会减少应用的响应次数。


测试失败的操作。在打开状态下,断路器不是使用定时器来确定何时切换到半开状态,而是可以定时ping远程服务和资源,以确定它是否可用。这里的ping也可能是对服务的调用,或者使用由远程服务提供的专用于测试服务健康状况的接口来完成,如健康端点监控模式(https: //docs.microsoft.com/en-us/azure/architecture/patterns/health-endpoint-monitoring)。


手动覆盖。在故障恢复时间多变的系统中,为管理员提供手动复位来关闭断路器(并重置故障计数器)。类似地,如果由断路器所监听的操作不可用,管理员可以手动强制断路器进入打开状态(并重新启动超时定时器)。


并发。断路器可被大并发实例的应用程序访问。该实现不应阻塞并发请求,负载也不应随调用次数的增加而增加。


资源差异化。对于同一类型但不同供应商的资源使用单个断路器时要小心。例如,在包含多个分片的数据库中,一个分片可能完全可访问,而另一个分片暂时有问题。如果服务的返回值被合并,应用程序就有可能在极可能再次失败的情况下,再次访问某些分片;部分访问成功,部分失败。


加速断路。有时,故障响应包含足够的信息,使断路器立即切换并保持最短时间的切换。例如,对于超负荷共享资源的错误响应就不推荐立即重试,而应该在几分钟之内重试。


如果服务不可用,服务可以返回HTTP 429(太多请求),或HTTP 503(服务不可用)。响应可以包括附加信息,例如延迟的预期持续时间。


重试失败的请求。在打开状态下,断路器可以将每个请求的细节记录到日志中,而不是简单的快速失败,并且在远程资源或服务可用时再次尝试发送这些请求。


外部服务不当的超时配置。当应用程序的外部服务配置的超时过长,断路器可能无法完全发挥作用。如果超时时间过长,则在断路器指示操作失败之前,运行断路器的线程可能会被长时间阻塞。在这个时候,许多其他应用程序实例也可能尝试通过断路器调用该服务,并且在请求失败之前绑定了大量的线程。


何时使用这个模式
防止应用程序重复尝试调用远程服务或访问共享资源,如果此操作很可能失败。


不建议使用此模式:
用于处理应用程序中本地私有资源的访问,例如内存中数据结构。在这种环境中,使用断路器会增加系统的额外开销。
作为处理应用程序业务逻辑中异常的替代品。


例子。


在1个Web应用程序中,有几个页面,数据从外部服务调用获得。如果系统实现最小的缓存,这些页面的请求大多数会调用到服务(缓存不会命中)。从Web应用程序到服务的连接可以配置1个超时时间(通常为60秒),如果服务在这段时间内没有响应,则每个网页将假设该服务不可用并且抛异常。


但是,如果服务失败并且系统非常繁忙,在发生异常之前,会迫使用户等待长达60秒。最终,诸如内存,连接和线程之类的资源可能会被耗尽,从而会阻塞更多用户连接到系统,即使他们只是访问静态页面。


添加更多Web服务器实例并实现负载平衡来扩展系统会提高并发能力,但用户的请求仍然无响应,所有Web服务器最终可能依然会耗尽资源,因此解决问题依然没有解决。


将连接到服务的逻辑包含在断路器中来获取数据可以帮助解决这个问题,并且更加优雅地处理了服务故障。用户请求仍将失败,但会失败的更快,资源访问不会被阻塞。


CircuitBreaker类维护了一些断路器的状态信息,断路器对象实现ICircuitBreakerStateStore接口,如下面的代码所示。


interface ICircuitBreakerStateStore
{
  CircuitBreakerStateEnum State { get; }


  Exception LastException { get; }


  DateTime LastStateChangedDateUtc { get; }


  void Trip(Exception ex);


  void Reset();


  void HalfOpen();


  bool IsClosed { get; }
}






State属性指示了断路器的当前状态,并且将由CircuitBreakerStateEnum枚举定义为Open,HalfOpen或Closed。断路器闭合时,IsClosed属性应为True,如果断路器处于断开状态,值为False。 


Trip方法将断路器的状态切换到打开状态,并记录状态更改的异常以及发生异常的日期和时间。 LastException和LastStateChangedDateUtc属性包含此信息。 


Reset方法会关闭断路器,HalfOpen方法将断路器设置为半开路。


该示例中的InMemoryCircuitBreakerStateStore类包含ICircuitBreakerStateStore接口的实现。 CircuitBreaker类创建一个该类的实例维护断路器的状态。


CircuitBreaker类中的ExecuteAction方法使用了Action对是操作进行封装。如果断路器闭合,ExecuteAction将调用Action方法。如果操作失败,则异常处理程序将调用TrackException方法,将断路器状态设置为打开。以下代码示例显示了这个流程是如何工作的。


public class CircuitBreaker
{
  private readonly ICircuitBreakerStateStore stateStore =
    CircuitBreakerStateStoreFactory.GetCircuitBreakerStateStore();


  private readonly object halfOpenSyncObject = new object ();
  ...
  public bool IsClosed { get { return stateStore.IsClosed; } }


  public bool IsOpen { get { return !IsClosed; } }


  public void ExecuteAction(Action action)
  {
    ...
    if (IsOpen)
    {
      // The circuit breaker is Open.
      ... (see code sample below for details)
    }


    // The circuit breaker is Closed, execute the action.
    try
    {
      action();
    }
    catch (Exception ex)
    {
      // If an exception still occurs here, simply
      // retrip the breaker immediately.
      this.TrackException(ex);


      // Throw the exception so that the caller can tell
      // the type of exception that was thrown.
      throw;
    }
  }


  private void TrackException(Exception ex)
  {
    // For simplicity in this example, open the circuit breaker on the first exception.
    // In reality this would be more complex. A certain type of exception, such as one
    // that indicates a service is offline, might trip the circuit breaker immediately.
    // Alternatively it might count exceptions locally or across multiple instances and
    // use this value over time, or the exception/success ratio based on the exception
    // types, to open the circuit breaker.
    this.stateStore.Trip(ex);
  }
}





以下代码示例演示了断路器未闭合时执行的逻辑。它首先检查断路器的打开时间是否比CircuitBreaker类中OpenToHalfOpenWaitTime字段的时间更长。如果是,ExecuteAction方法将断路器设置为半开,然后尝试执行Action。


如果操作成功,断路器将复位到关闭状态。如果操作失败,它将切换到打开状态,异常发生的时间被更新,以便在再次执行操作之前,断路器会先等待一段时间。


如果断路器只打开了很短的时间,小于OpenToHalfOpenWaitTime值,则ExecuteAction方法会抛出CircuitBreakerOpenException异常并返回使断路器打开状态对应的错误。


此外,对断路器在半开时的调用进行了加锁,处理并发情况。并发操作会当做断路器打开状态来处理,请求会失败,稍后会进行描述。


...
    if (IsOpen)
    {
      // The circuit breaker is Open. Check if the Open timeout has expired.
      // If it has, set the state to HalfOpen. Another approach might be to
      // check for the HalfOpen state that had be set by some other operation.
      if (stateStore.LastStateChangedDateUtc + OpenToHalfOpenWaitTime < DateTime.UtcNow)
      {
        // The Open timeout has expired. Allow one operation to execute. Note that, in
        // this example, the circuit breaker is set to HalfOpen after being
        // in the Open state for some period of time. An alternative would be to set
        // this using some other approach such as a timer, test method, manually, and
        // so on, and check the state here to determine how to handle execution
        // of the action.
        // Limit the number of threads to be executed when the breaker is HalfOpen.
        // An alternative would be to use a more complex approach to determine which
        // threads or how many are allowed to execute, or to execute a simple test
        // method instead.
        bool lockTaken = false;
        try
        {
          Monitor.TryEnter(halfOpenSyncObject, ref lockTaken)
          if (lockTaken)
          {
            // Set the circuit breaker state to HalfOpen.
            stateStore.HalfOpen();


            // Attempt the operation.
            action();


            // If this action succeeds, reset the state and allow other operations.
            // In reality, instead of immediately returning to the Closed state, a counter
            // here would record the number of successful operations and return the
            // circuit breaker to the Closed state only after a specified number succeed.
            this.stateStore.Reset();
            return;
          }
          catch (Exception ex)
          {
            // If there's still an exception, trip the breaker again immediately.
            this.stateStore.Trip(ex);


            // Throw the exception so that the caller knows which exception occurred.
            throw;
          }
          finally
          {
            if (lockTaken)
            {
              Monitor.Exit(halfOpenSyncObject);
            }
          }
        }
      }
      // The Open timeout hasn't yet expired. Throw a CircuitBreakerOpen exception to
      // inform the caller that the call was not actually attempted,
      // and return the most recent exception received.
      throw new CircuitBreakerOpenException(stateStore.LastException);
    }
    ...





若要将CircuitBreaker对象应用到一个操作,应用程序要创建CircuitBreaker类的实例,并调用ExecuteAction方法,指定参数。如果由于断路器断开而导致操作失败,应用程序应捕获CircuitBreakerOpenException异常。可参考以下示例:




var breaker = new CircuitBreaker();


	try
	{
	  breaker.ExecuteAction(() =>
	  {
		// Operation protected by the circuit breaker.
		...
	  });
	}
	catch (CircuitBreakerOpenException ex)
	{
	  // Perform some different action when the breaker is open.
	  // Last exception details are in the inner exception.
	  ...
	}
	catch (Exception ex)
	{
	  ...
	}




相关模式的阅读
实现此模式时,以下模式也可能很有用:
重试模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/retry)。介绍了当应用程序对访问服务或网络资源失败的请求进行重试时,应用程序如何处理预期的故障。


健康端点监控模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/health-endpoint-monitoring)。断路器能够通过向服务器公开的端点发送请求来测试服务的健康状况。服务端返回其状态信息。

相关文章:

  • 为什么主流Java MVC框架如此难以使用
  • Azure设计模式之命令与查询隔离(CQRS)
  • 用ICC编译MP3编码器LAME
  • Azure设计模式之补偿事务模式
  • Azure设计模式之资源争竞下的消费者模式
  • 搜狗状告腾讯的几个重要看点
  • Azure 设计模式之资源整合
  • Azure 设计模式之事件源模式
  • 新加坡Ruby开发者6月聚会
  • Azure设计模式之外配存储模式
  • [Web 开发] URL 的最大长度
  • Azure设计模式之联邦身份模式
  • 从Oracle到DB2(四)--字符集
  • Azure设计模式之看门人模式
  • 圣淘沙闲逛,傻照两张!
  • 9月CHINA-PUB-OPENDAY技术沙龙——IPHONE
  • Java Agent 学习笔记
  • JavaScript/HTML5图表开发工具JavaScript Charts v3.19.6发布【附下载】
  • JavaScript设计模式系列一:工厂模式
  • Spring框架之我见(三)——IOC、AOP
  • windows下如何用phpstorm同步测试服务器
  • 给第三方使用接口的 URL 签名实现
  • 技术发展面试
  • 七牛云 DV OV EV SSL 证书上线,限时折扣低至 6.75 折!
  • 前端性能优化——回流与重绘
  • 体验javascript之美-第五课 匿名函数自执行和闭包是一回事儿吗?
  • 智能合约Solidity教程-事件和日志(一)
  • 好程序员大数据教程Hadoop全分布安装(非HA)
  • ​ ​Redis(五)主从复制:主从模式介绍、配置、拓扑(一主一从结构、一主多从结构、树形主从结构)、原理(复制过程、​​​​​​​数据同步psync)、总结
  • ​LeetCode解法汇总518. 零钱兑换 II
  • # include “ “ 和 # include < >两者的区别
  • ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException
  • #HarmonyOS:软件安装window和mac预览Hello World
  • #laravel 通过手动安装依赖PHPExcel#
  • $.ajax()方法详解
  • %check_box% in rails :coditions={:has_many , :through}
  • (AtCoder Beginner Contest 340) -- F - S = 1 -- 题解
  • (四)docker:为mysql和java jar运行环境创建同一网络,容器互联
  • .NET 除了用 Task 之外,如何自己写一个可以 await 的对象?
  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖
  • .NET 使用配置文件
  • .NET企业级应用架构设计系列之结尾篇
  • .Net转前端开发-启航篇,如何定制博客园主题
  • .set 数据导入matlab,设置变量导入选项 - MATLAB setvaropts - MathWorks 中国
  • ?php echo ?,?php echo Hello world!;?
  • @RequestMapping处理请求异常
  • @四年级家长,这条香港优才计划+华侨生联考捷径,一定要看!
  • [Android] Upload package to device fails #2720
  • [Bugku]密码???[writeup]
  • [BUUCTF NewStarCTF 2023 公开赛道] week3 crypto/pwn
  • [C# 基础知识系列]专题十六:Linq介绍
  • [CSS]CSS 字体属性
  • [EFI]英特尔 冥王峡谷 NUC8i7HVK 电脑 Hackintosh 黑苹果efi引导文件
  • [HTML API]HTMLCollection
  • [HTML]Web前端开发技术12(HTML5、CSS3、JavaScript )——喵喵画网页