云计算设计模式翻译(二):Circuit Breaker Pattern(断路器模式),circuitbreaker


    当连接使用远端服务或资源时,可能需要花不少精力来做好错误处理。这个模式可以有效提高程序的稳定性和弹性。

Context and Problem

    在像云这种分布式的环境中,应用程序的操作经常访问远端的资源和服务。然而这类操作有可能因为网络响应慢、超时、资源暂时不可用等瞬时性故障(transient faults)而失败。这些故障通常情况下会在一小段时间后自动恢复,而对于一个好的云应用来说,必须要有一个好的策略(比如尝试重试)来处理这种情况。

    但是有时候错误是由一些难以预期的异常事件导致的,这种情况就可能会花费相对来说较多的时间来处理和修复,并且直接导致部分服务甚至所有服务无法使用。当发生了这种情况时,应用程序再进行不停地重试就没什么意义了,此时应用程序应该尽快将本次操作(包括重试)标为彻底失败,并在应用程序端触发对应的错误处理逻辑。

    此外,对于一个访问压力很大的服务来说,系统中某部分的异常可能会引发一系列连锁的异常。举个栗子:比如说我们现在有个操作会去调用一个远端服务,并且这个操作实现了超时机制,在指定的时间段内如果对方服务没有响应那么本次操作就会返回一个failure消息。假设此时系统压力很大,不停地有大量并行的请求都调用这个操作,如果此时远端服务出现异常没有响应了,那么这些并行的实例都会被阻塞直到超时。再假设这些被阻塞的请求都拥有一些系统临界资源(比如内存、线程、数据库连接等等),那么这种情况下这些资源会被很快消耗殆尽,这样一来其他一些无关但也用到这些资源的操作也会失败。在这种情况下,我们可能更希望这个操作能够快速失败,仅当服务有可能成功调用时再进行重试(而不是傻傻地来一个操作失败了不停重试调用这个服务)。另外,将超时时间设短一点也许能解决这个问题,但也不能设得太短(为了应对瞬时错误)。

Solution

    断路器模式能够防止应用程序不停尝试一个极有可能失败的操作,使应用程序能够继续运行而不用花费大量时间来进行没有必要的错误处理或等待异常恢复。另外,断路器模式能够使应用程序能够检测到异常是否已经被修复,如果对应异常已经被修复了,那么应用程序就又可以尝试来调用这个操作了。(断路器模式和单纯的重试区别非常大,具体比较在后面谈到重试模式时再介绍。另外,可以结合现实中的断路器的功能来理解这个模式)。

    一个断路器相当于是一个可能失败的操作的代理。这个代理会监控最近错误发生的次数,然后通过这些信息来决定是让一个应用程序来调用这个操作还是直接立即返回一个异常。这个代理可以用状态机的形式来实现从而模拟电力电子学中断路器的功能,主要有下面这些状态:

Closed: 应用程序能够直接通过这个代理来调用操作。在这个代理内部保存了最近发生错误的次数计数器,如果发生了一次失败的调用,那么这个计数器就会增加;如果在一定的时间段内这个计数器的值超过了一个设定的阈值,那么这个代理就会进入Open状态。此时,代理会启动一个超时计时器,当这个计时器到期时,代理就会进入Half-Open状态(设置这个超时器的目的是:在允许应用程序再次调用这个操作之前,给系统一段时间来解决导致异常的错误)。

Open: 这种状态下应用程序的调用请求会立即失败并且收到一个exception。

Half-Open: 这种状态下只允许限量调用这个操作。如果所有这些调用成功了,那么就认为之前导致异常的错误被解决了,此时代理的状态会被设置成Closed,同时重置错误计数器;如果这些调用中还存在错误的情况,那么就认为这个问题还没有被解决,此时代理的状态被重新设置成Open,并重置超时计时器,从而再给系统一个时间来解决这个问题。一个刚刚恢复的服务可能并不能一下子支持大量的服务请求直到恢复流程彻底走完,否则过大的访问压力也许会导致正在出于恢复过程中的服务再次因为超时或失败,而Half-Open状态能够防止一个刚刚恢复的服务一下子被请求淹没(限量访问,一旦有错就进入Open停止对服务的访问),从而安全地走完它的错误处理/恢复过程。


说明:

1.在Closed中的failure counter是基于时间的,每隔一段时间会自动清零。这是为了防止断路器因为一些偶发的错误而进入Open状态。所以总结一下就是从Closed状态切到Open状态需要让错误计数器在一个时间段内达到阈值,否则较低频率出现的错误不会导致进入Open状态。

2.在Half-Open状态中用到的success counter记录的是成功调用的次数。当连续成功的次数达到一个设定的阈值后状态就会切为Closed,在此期间一旦发生失败的调用,状态立即切为Open,success counter在下次状态从Open切换到Half-Open时被重置。

    

    实现断路器模式能够提高系统的稳定性和弹性,并使系统在错误恢复过程中保持稳定,同时也将错误对系统性能的影响最小化。通过直接拒绝那些很有可能失败(基本上是一定会失败)的请求而不是不断地无脑重试能够保证系统响应时间的稳定(尽管可能响应的是一个报错消息,那也比因为错误重试导致请求堆积引发更大的问题来的好)。我们也可以在断路器每次状态切换时加上打印或消息通知,这些日志信息可以有效帮助监听系统健康状态,或者在断路器状态变成Open时及时通知维护人员。

    在实际使用中可以根据实际情况对这个模式进行定制化。比如说你可以将Open状态定时器实现成一个自增的定时器,即最开始超时时间仅为一个很短的时间比如几秒,如果到期切成Half-Open状态又被打回来了,就延长超时时间;再比如说Open状态可以不返回一个异常而是返回一个默认值。

Issues and Considerations

当使用这一模式时需要注意以下几点:

1.异常处理。通过断路器来调用一个服务必须要对服务不可用的情况做好异常处理。不同的应用根据自己的需求来定制这个异常处理逻辑。

2.异常类型。一次请求失败的理由五花八门,有些异常相比其他的可能会严重得多(也就是说需要更多时间来做错误处理)。断路器可能需要针对不同的异常来配置不同策略(各种阈值根据不同的异常类型来进行配置)。

3.日志记录。断路器对于每次异常情况都需要添加日志记录使维护人员能够监测系统的监控状况,并协助开发人员来进行改进优化。

4.可恢复性。在实际使用中设置断路器的参数时应该考虑到其所保护操作的恢复特性。举个例子,如果Open超时时间设置得远远超过异常恢复所需要的时间,那么则极有可能出现断路器长时间处于Open状态,即使异常早已被解决,调用者也只会一直受到异常信息。类似的,如果将这个时间设置得相对异常恢复时间来说过分短,那么就会使断路器频繁地在Open状态和Half-Open状态之间跳动,从而使整个应用的响应时间变得非常不稳定。

5.对失败的操作进行测试。举个例子来说,在Open状态下,我们除了使用一个超时计时器来作为Open状态切换到Half-Open状态的依据外,也可以尝试周期地Ping远端服务/资源的IP,以此为依据来判断远端服务/资源是否可用。这种方式实际上是用一种相对来说开销更小的访问模式来测试远端服务的连通性和健康程度。

6.外部手动控制。在一个复杂系统之中,由于不同的异常恢复时间和异常处理流程都不尽相同,我们可以为系统管理员提供一个人工介入的接口来应对一些特殊的情况,比如可以提供重置计数器命令、重置超时器命令等,甚至也可以提供接口使管理员能够手动更改断路器的状态。

7.并发性。同一个断路器有可能同时被大量的请求使用,所以在设计和实现断路器时不能出现阻塞的情景,也要尽可能地减少调用过程中的资源开销。

8.资源差异性。当用一个断路器来控制一类资源(虽然为同一类资源甚至同一个资源,但很有可能是由不同独立的资源提供者组成)访问时需要格外注意。举个例子,一个数据存储器可能是由多个不同的分片组成的,此时存在一种可能就是有些分片可能能够正常的访问,但与此同时另一些分片无法正常访问。此时加在这个数据存储之上的断路器对于访问出现的错误如果混在一起用同样的处理,那么调用者则很有可能会尝试访问那些无法正常访问的分片。

9.对断路器进行加速。有时候尽管断路器没有达到标准的切换条件,但根据响应中所携带的信息已经可以确定要做状态切换以及维持这个状态的时间。比如如果访问一个正在进行重载的资源时收到的响应已经告诉访问端资源正在进行重载,可能需要几分钟,那么此时断路器在收到消息后可以立刻切成Open状态,并根据响应中包含的时间来配置超时时间。

10.调用重试。在Open状态期间,除了简单地对调用者返回一个错误或默认值外,也可以对这些被直接打回去的调用做记录,当资源可用时对这些记录中的调用请求进行重试。



相关内容