微服务架构通过明确定义的边界使得故障隔离成为可能,但是每个分布式系统都存在同样的问题——故障可能发生在网络、硬件或应用程序层面。因为服务之间存在依赖关系,任何一个组件出现问题都会影响到组件依赖关系。为了最大限度地减少部分故障的影响,我们需要构建能够优雅地处理某些类型故障的容错服务。本文基于RisingStack的Node.js咨询和开发经验,介绍构建高可用微服务系统的常用技术和架构模式。如果您不熟悉本文介绍的模式,并不意味着您的做法是错误的。毕竟,构建可靠的系统需要额外的成本。微服务架构的风险微服务架构将业务逻辑分散到各个微服务中,微服务之间通过网络层进行通信。网络通信引入了额外的延迟和复杂性,需要多个物理和逻辑组件协同工作。分布式系统的额外复杂性增加了网络故障的可能性。微服务架构相对于单体架构的最大优势之一是不同的团队可以独立设计、开发和部署他们的服务。他们可以完全控制自己的微服务生命周期。当然,这也意味着他们无法控制服务依赖项,因为依赖项的控制权在其他团队手中。在采用微服务架构时,我们要时刻谨记发布、配置等问题可能会导致服务提供者暂时不可用。GracefulServiceDegradation微服务架构实现了故障隔离,即在某个组件出现故障时进行服务的优雅降级。例如,当一个照片分享应用程序宕机时,用户可能无法上传新图片,但他们仍然可以浏览、编辑和分享现有图片。图:理论上的微服务故障隔离在大多数情况下,很难实现这种优雅的服务降级,因为在分布式系统中,应用程序相互依赖。为了应对临时故障,一些Failover解决方案(后面会提到)。图:如果没有故障转移解决方案,相互依赖的服务将全部失败。变更管理Google的站点可靠性团队发现70%的中断是由系统变更引起的。更改服务、部署新代码和更改配置可能会引入新错误或导致服务失败。在微服务架构中,服务是相互依赖的。所以我们想把失败的概率降到最低,限制失败带来的负面影响。我们需要良好的变更管理策略和自动回滚机制。比如在部署新的代码时,或者在修改配置时,先在少量的服务实例上进行,然后进行监控,一旦发现关键指标异常就自动回滚。图:变更管理-回滚部署另一种解决方案是运行两个生产环境。部署时,只部署到其中一个生产环境,确认环境没问题后,负载均衡器才能指向这个环境。这种部署方式称为蓝绿部署或红黑部署。后备代码不是坏事。您永远不能在生产中留下有问题的代码并想知道哪里出了问题。所以,在必要的时候,回退到代码,越快越好。健康监控和负载均衡服务实例总是会因为各种原因(故障、部署或自动伸缩)经历启动、重启和停止的过程。此过程将使服务暂时或永久不可用。为避免出现问题,负载均衡器需要忽略出现问题的服务实例,因为它们不再能够为用户或其他子系统提供服务。应用程序的健康状态可以通过外部观察获得,比如反复调用/health端点来了解应用程序的状态,或者让应用程序报告自己的状态。服务发现机制将持续收集服务实例的健康信息,负载均衡器应配置为仅将流量路由到健康的服务实例。自我修复自我修复功能允许应用程序在发生故障时自行恢复。如果应用程序可以通过一系列步骤从故障状态中恢复过来,则称该应用程序具有自我修复能力。在大多数情况下,这是通过外部系统完成的。该系统监控服务实例的健康状态,如果服务长时间处于不健康状态,系统将重启它。自愈能力在大多数情况下是有用的,但如果应用程序不断重启,它也会导致问题。这通常发生在应用程序过载或数据库连接超时时。实施先进的自我修复解决方案会很麻烦。比如在数据库连接超时的情况下,需要在应用中增加额外的逻辑,让外部系统知道此时不需要重启服务实例。故障转移缓存服务总会因为各种原因失败,比如网络问题。然而,这些错误大多是暂时的,系统的自我修复能力和先进的负载均衡特性使得应用实例在这些问题发生时仍然可以提供服务能力。故障转移缓存这时候就可以派上用场了,它可以为应用程序提供必要的数据。故障转移缓存通常使用两个不同的过期时间,正常情况下缓存过期的短期时间,以及故障期间缓存过期的长期时间。图:故障转移缓存但是,需要注意的是,故障转移缓存中的数据可能有过期数据,因此请确保这对您的应用程序来说是可以接受的。可以通过标准HTTP响应标头设置缓存或故障转移缓存。比如通过max-age指定资源的最大过期时间,通过stale-if-error指定缓存在失效时的有效时间。现代CDN和负载平衡器提供各种缓存和故障转移机制,您可以创建适合您公司的缓存解决方案。重试在某些情况下,我们无法缓存数据,或者我们想更新缓存内容,更新失败。这时候我们可以重试,因为我们认为相关资源稍后会恢复,或者负载均衡器会把请求发给正常的实例。向应用程序添加重试逻辑时要非常小心,因为大量重试会使事情变得更糟,甚至会阻止应用程序从故障中恢复。在分布式系统中,一个微服务系统可能会触发多次请求或重试操作,从而产生级联效应。为了减少重试的影响,应该限制重试的次数。可以使用指数退避算法逐渐增加重试之间的延迟,直到达到重试上限。重试是由客户端(如浏览器、其他微服务等)发起的,客户端并不知道之前的请求处理是否成功,所以重试时要注意幂等性问题。例如,重试购买操作时不应重复计费。可以为每笔交易使用唯一的幂等密钥,这有助于解决幂等问题。速率限制和Shedder速率限制指定应用程序在一个时间窗口内可以接收或处理的请求数。通过限速,可以过滤掉一些用户请求或者在流量高峰期发出请求的微服务,保证你的应用不会过载。您还可以限制低优先级流量,让更多资源用于更关键的任务。图:速率限制器限制流量峰值另一种类型的速率限制器称为并发请求限制器,它在某些情况下很有用。例如,您不希望某些端点被多次调用,但同时希望为所有流量提供服务。使用负载回馈注入器确保始终有足够的资源来处理关键事务。它为高优先级的请求保留了一些资源,这些资源不会被用于低优先级的事务。负载注入器决定如何根据整个系统的状态预留资源,而不是根据请求桶的大小。负载回馈注入器帮助系统从故障中恢复,因为它们在发生故障时保持核心系统功能运行。Stripe文章详细介绍了速率限制器和负载注入器。快速独立地失败在微服务架构中,如果服务失败,我们需要让它们快速独立地发生。我们可以应用隔板模式来隔离服务级别的问题。稍后会详细介绍舱壁模式。如果一个服务组件失败了,它需要尽快失败,因为我们不想浪费太多时间等待它超时。没有什么比挂起的请求和无响应的界面更令人沮丧的了,这不仅会浪费资源,还会影响用户体验。在服务生态中,服务之间相互调用,我们需要防止挂起的请求操作造成雪崩效应。你可能首先想到为每个服务调用定义一个二级超时,但问题是你无法确切知道超时多长最合适,因为有时网络故障等问题只会影响一两个操作。显然,如果是这种情况,则不应仅仅因为一些请求已过时而拒绝其他请求。可以说,在微服务架构中使用超时来实现快速失败机制是一种反模式,应该尽可能避免。相反,我们可以使用断路器模式,它根据成功和失败操作的统计信息来确定服务是否失败。舱壁在造船业中,舱壁用于将船分成几个部分,这样如果船体发生泄漏,可以单独密封泄漏部分。隔板的概念也用于软件开发中以分隔资源。通过应用舱壁模式,我们可以防止有限的资源被耗尽。例如,如果我们对数据库有两个操作,我们可以使用两个连接池而不是一个。这样,如果某些操作超时或过度使用连接池,则不会影响另一个连接池上的操作。断路器为了限制操作的持续时间,我们可以为操作定义超时。超时机制可以防止长时间挂起操作,保证系统能够正常响应。然而,在微服务架构中使用固定的超时机制是一种反模式,因为我们的环境是高度动态的,所以几乎不可能为每种情况定义正确的绑定时间。我们可以使用断路器来代替固定的超时机制。断路器的名字来源于现实世界中的电子元件,因为它们的行为非常相似。它们保护资源并帮助系统恢复。在分布式系统中,它们可能非常有用,尤其是当重复故障导致雪崩效应威胁到整个系统时。如果某种类型的错误在一定时间内多次发生,断路器就会跳闸。断开的断路器会阻止后续请求,就像电子元件中的断路器一样。断路器在一段时间后关闭,让底层服务有更大的恢复空间。请记住,并非所有错误都需要触发断路器。例如,您可能希望忽略客户端问题,例如具有4xx响应代码的请求,但同时希望保留5xx服务器端错误。一些断路器出现半开半闭状态。这时,服务会发送一个请求来检查系统的可用性,同时拒绝其他请求。如果检查系统可用性的请求成功返回,则断路器将关闭,并继续处理后续请求。否则,它保持打开状态。图:CircuitBreaker做好故障测试我们应该不断地测试我们系统的各种常见问题,以确保我们的服务能够应对各种故障。例如,我们可以使用外部服务来识别一组服务实例,然后随机终止其中一个。这样你就知道如何处理单个实例故障,当然你也可以关闭一整组服务来模拟云服务中断。Netflix的ChaosMonkey是一种非常流行的弹性测试工具。总结实施和运行可靠的服务不是一件容易的事。这需要您付出很多努力,并且会让您的公司付出很多代价。可靠性可以分为很多层次,涉及很多方面。您必须找到适合您团队的解决方案。您应该将可靠性视为业务决策的一个因素,并为其分配足够的预算和时间。
