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

如何确定故障

既然知道了如何响应失败,接下来让我们讨论如何确定依赖的服务是否出现故障。如何确定一个依赖服务是否出现了故障呢?这取决于故障的模式。以下是几种需要重点了解的故障模式。

乱码响应

这种响应是无法理解的。它是一种以无法理解的形式存在的“垃圾”数据。这可能代表响应的格式错误,或者格式中存在语法错误。

表示致命错误发生的响应

这种响应是可以理解的。它表示在处理请求时发生了严重的错误。这通常可能不是因为网络传输的原因,而是服务本身出现了错误。它还可能是由于发送给服务的请求无法被理解而引起的。

结果可以理解,但是所需的结果不匹配

这种响应是可以理解的。它表示操作执行成功,没有严重错误,但是返回的数据无法与预期的结果相匹配。

结果超出预期范围

这种响应是可以理解的。它表示操作执行成功,没有严重错误。返回的数据是合理的,并且格式正确,但是数据本身不在预期之内。例如,假设一个计算当天距离当年第一天日期的请求,如果它返回一个“843”的结果会怎样呢?这个结果是可理解的、可解析的,没有失败,但是显然没有在预期的结果范围之内。

没有接收到响应

请求已经发出,但是没有接收到服务发回的响应。这可能是由于网络通信故障、服务出现问题,或者服务故障导致的。

接收响应很慢

请求已经发出,并且响应也接收到了。响应的内容正确,也在预期范围之内。但是,响应比预期到达时间晚很长时间才接收到。这通常表明服务或者网络压力过大,或者存在其他资源分配不合理的问题。

当接收到乱码时,你立即知道响应不可用,并且可以采取适当行动。一个可理解但是不匹配所需结果的响应可能更难被发现,也更加难以决定如何行动,但是至少我们还可以得到这些响应。

一个永远无法到达的响应,使你很难对结果采取适当的行动。如果你只想给用户返回一个错误响应,那么在依赖服务处加上一个简单的超时处理,可能就能够捕获到丢失的响应。

捕获永远无法到达的响应的更好方式

但是,超时不一定总是行得通。例如,如果一个服务通常需要50毫秒响应请求,但是有些时候可能只需要10毫秒,但有些时候又需要500毫秒,你该怎么办呢?你应该将超时时间设置成多少呢?一个显然的答案是大于500毫秒。但是,如果你对用户承诺的响应时间小于150毫秒呢?显然,仅仅将时间设置成500毫秒并不合理,因为这就相当于将依赖服务的错误直接传递给了用户。这违反了可预测和可理解的测试原则。

如何解决这个问题呢?一个可能的答案是使用断路器模式。这种代码模式让你的服务可以追踪对依赖服务的所有调用,了解有多少次调用成功,多少次调用失败(或者超时)。如果失败次数达到某个阈值,断路器会“切断”与依赖服务的连接,让你的服务以为依赖服务已经宕机,并停止向该服务发送请求或者接收响应。这使得你的服务可以立即检测到依赖服务发生故障并采取适当的行动,从而保证上游服务的SLA。

随后,你可以通过向故障服务定期发送请求,来检查它是否恢复正常。如果它又可以成功处理请求(超过一个预定的阈值)了,那么断路器会“重置”,此时你的服务就可以恢复使用该依赖服务。

一个很慢的响应(相对于永远无法接收的响应)可能是最难处理的。这个问题变成了到底多慢算是慢?这是一个很难回答的问题,仅仅通过简单的超时(不管使用或者不使用断路器)很难很好地处理这种情况,因为慢响应可能在“某些时候”又很快,从而导致出现一些很古怪的结果。记住,响应的可预测性是服务的一个重要特征,如果你所依赖的服务总是不可预测地出现故障(因为慢响应和不稳定的超时),这也会影响你对依赖服务提供可预测的响应。

一种检测慢响应的更优雅的办法

除了断路器和相似的方法,还有一种更优雅的超时机制可以处理慢响应情况。例如,你可以创建多个“桶”来捕获最近调用依赖服务的性能。每次调用依赖服务时,你将返回响应的时间存储到一个桶中。你只在桶中保存一段时间内的响应时间。然后,可以通过这些桶来计算出一个触发断路器的规则。例如,你可以创建以下这些规则:

  • 如果“1分钟之内接收到500个请求超过150毫秒”,则触发断路器。

  • 如果“1分钟之内接收到50个请求超过500毫秒”,则触发断路器。

  • 如果“1分钟之内接收到5个请求超过1000毫秒”,则触发断路器。

这类分级技巧可以帮助你更早地发现更严重的慢响应问题,又不会忽略轻微的慢响应问题。