可伸缩架构(第2版):云环境下的高可用与风险管理
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

适当的行为

当错误发生时你应该做什么呢?这取决于错误的类型。以下是一些针对不同错误类型你可以采取的方案。

优雅降级

如果一个依赖服务出现了故障,你的服务没有响应还能够工作吗?它是否能够在缺少故障服务的响应下,继续执行所需的任务?如果在这种情况下它还能执行有限的功能,那么就是一个优雅降级的例子。

优雅降级指的是在当前服务缺少某个故障服务的结果时,可以通过降低工作量来尽可能地完成工作。

提供有限功能

假设你有一个Web应用程序,用来创建一个售卖T恤的电子商务网站。我们还假设有一个“图片服务”为网站上显示的图片提供URL。如果应用程序调用了该图片服务,但是图片服务出现了故障,那么应用程序应该怎么办呢?一种选择是,应用程序继续给用户显示所需产品,但是没有该产品的图片(或者显示一条“图片不可用”的消息)。这个Web应用程序可以继续作为电子商务网站运行,只是缺少了显示产品图片的能力。

这种做法,比起只是因为图片不可用,整个电子商务网站就只好停运并返回错误的做法已经好太多了。

这就是一个提供有限功能的例子。对于一个服务(或者应用程序)来说,即使因为依赖服务由于故障不能再提供所需数据,也应当尽可能地提供服务价值,这一点是非常重要的。

优雅补偿

有观点认为,如果请求没有返回足够有用的结果,那么也应该算作失败。除了生成一条错误消息,你还能做些什么为用户提供价值呢?

即使你无法完全满足用户的需要,但是按照为用户提供价值的方向去改进,这就是一个优雅补偿的例子。

优雅补偿示例

我们继续“提供有限功能”例子中所描述的请求,假设提供指定产品详情内容的服务失败了。这意味着网站无法为所需产品提供任何可显示的信息。显然只显示一个空白页是不合理的,因为这样对用户来说没有任何用处;显示一条错误消息(“对不起,发生了一个错误”)也不是一个好主意。

但是,你可以显示一个向用户道歉但同时提供网站上当前最流行产品链接的页面。虽然这并不是用户真正想要的,但是它给用户提供了一些价值,避免了仅仅显示一个突兀的错误页面的情况。

尽早失败

如果你的服务无法收到故障服务的响应就无法工作呢?如果无法选择提供有限的功能或者优雅补偿呢?当你无法接收来自故障服务的响应时,就无计可施了。在这种情况下,你能做的只有让请求失败了。

如果你已经确定无法挽救请求,那么应当尽快让请求失败。当你知道请求一定会失败后,就不要再执行请求中其他的操作或任务了。

这样做的结果就是你会尽可能地对请求进行完备的检查,尽可能地提前确认请求的正确性,当你继续发送请求时,该请求很可能成功。

除以零

假设有一个计算两个整数相除的服务。我们都知道除数是不能为0的。如果你接到一个像“3/0”这样的请求,你可能会试着计算结果。在计算过程中,最终会发现无法得到结果,于是抛出一个错误。

既然你知道所有除以0的计算都会失败,那么只需对请求的数据进行一下检查。如果除数是0,则立刻返回一个错误。没有任何理由再去尝试进行计算。

为什么应该尽早失败?有以下几个原因。

资源节约

如果请求注定失败,那么在它失败之前进行的所有工作都是徒劳的。如果这些工作包括需要多次调用依赖的服务,那么浪费这么多宝贵的资源却只能得到一个错误。

响应性

你越快确定一个请求会失败,就能越快地把这个结果返回给发起请求的一方。这使得请求方可以更快地做出其他决定。

错误复杂性

有些时候,如果你继续处理注定会失败的请求,很可能会造成更难诊断的问题。我们还以“3/0”这个计算为例,你可以立即确定这次计算会失败,并返回错误消息。如果你选择继续计算,就会产生错误,但是可能会以更加复杂的方式出现—例如,根据你计算除法的算法不同,这可能会导致一个无限循环,最终只有等到超时后才能退出。

因此,除了得到一个提示,例如,“除以0”的错误,你还可能会等待很长时间并得到一个“操作超时”的错误。相比而言,你应该很清楚哪个错误信息在诊断问题时会更加有用。

用户导致的问题

当你的服务可以接收来自用户的无效输入时,尽早失败变得更为重要。如果你知道服务已经规定了哪些合理的限制条件,请尽早检查它们。

一个浪费资源的真实案例

在我之前工作过的一家公司中,有一个账户服务存在性能问题。该服务逐渐变得越来越慢,直到它几乎不可用。

在深入研究问题之后,我们发现有人向账户服务发送了一个错误请求。有人请求账户服务去获取100,000个用户的账户列表,包括所有账户的详细内容。

现如今,没有任何正常的业务会有这样的需要(在这个环境中),因此这个请求本身显然是无效的。100,000这个值超出了这个请求的合理范围。

但是,账户服务很负责任地尝试去处理这个请求,于是处理、处理、不断地处理……

这个服务最终由于没有足够资源来处理如此巨大的请求,所以失败了。当处理了几千个账户后它停止了服务,并返回了一个简单的错误消息。

而发起无效请求的调用方服务,由于接收到了失败消息,于是决定重试这个请求,于是重试、重试、不断地重试。

账户服务不断地处理这几千个账户,将这些结果扔到了一个失败信息中。但是它不断在重复做这件事。

不断失败的请求消耗了大量的可用资源。随着过多资源被消耗,导致其他合法请求也开始排队,并最终导致整个服务出现故障。

如果能在账户服务处理请求前进行一个简单的检查(例如,检查请求的账户数量是否合理),就可以避免对资源的大量浪费。此外,如果返回的错误消息能够表明这个错误是永久性的,并且是由一个无效的参数所导致的,那么调用方服务能够看到这个“永久错误”的消息,也就不会再去重试一定会失败的请求了。

提供服务约束

这个案例的结果告诉我们一定要提供服务约束。例如,如果你知道服务不能同时处理超过5000个账户的数据,一定要在服务约定中声明这个限制条件,进行测试,让所有超过该限制数量的请求都直接失败。