目录
微服务架构通过一种良好的服务边界划分,能够有效地进行故障隔离。但就像其他分布式系统一样,在网络、硬件或者应用级别上容易出现问题的机率会更高。服务的依赖关系,导致在任何组件暂时不可用的情况下,就它们的消费者而言都是可以接受的。为了能够降低部分服务中断所带来的影响,我们需要构建一个容错服务,来优雅地应对特定类型的服务中断。
本文基于一些在RisingStack( https://risingstack.com/ )的顾问咨询与开发经验,介绍了如何运用一些最常用的技术和架构模型,去构建与维护一个高可用的微服务系统。
如果你不熟悉本文中的模式,并不意味着你做错了什么。毕竟构建一个高可用的系统需要很多额外的付出。
*微服务架构的风险 The Risk of the Microservices Architecture
微服务的架构将应用的逻辑移动到一个服务里面,服务之间通过网络层进行通信交互。通过网络通信交互的方式取代了内存的调用,同时需要多个物理和逻辑组件之间的相互协作,给系统带来了额外的延迟性与复杂性。分布式系统复杂性的增加,导致了特定网络故障的可能性变得更大。
团队可以独立地设计、开发与部署他们的服务,是微服务的最大优点之一。他们完全拥有整个服务的生命周期,这也意味着团队无法控制他们的服务依赖,因为这些服务更有可能是不同的团队在管理。我们需要记住,提供者的服务由于发布中断、配置等等其他的改变而暂时不可用,他们是由别人控制,并且组件之间独立活动。
微服务最佳优势之一,当某个组件单独失败时,你可以实现优雅的服务降级,进行故障隔离。例如,一个照片共享的应用,由于中断,用户可能无法上传新的照片,但他们仍然可以浏览、编辑和分享他们现有的照片。
微服务的独立失败(理论上)
在大多数情况下,在一个分布式系统中,应用程序之间互相依赖,实现一种优雅的服务降级,这是很困难的,你需要采取多种故障切换逻辑(其中一些会在本文后面进行讨论),应对临时的故障与中断。
服务之间彼此依赖,在没有故障切换逻辑的情况下,一起失败。
谷歌网站的可靠性团队(SRE)发现,大约70%的中断是由一个实时系统的改变而引起。当你在服务中更改某些内容时——你部署了新版本的代码或更改了一些配置——总会导致更高的失败机率或者引入一个新的bug。
在微服务架构中,服务之间彼此依赖。这就是为什么你应该尽量减少失败,并限制它们的负面影响。如果要处理来自变更的问题,你可以使用变更管理策略和自动升级。
例如,当需要部署新代码或者更改某些配置时,你应该逐渐地将这些更改应用于实例的子集,监控它们,甚至当你看到关键指标有负面影响时,它们会自动回滚恢复。
变更管理 - 滚动发布
另一个解决方案,就是运行两个生产环境。只部署其中一个,并且在验证新版本的运行符合预期之后,才会将负载均衡指向新版本。这被称为蓝绿色部署,或红黑色部署。
恢复代码不是一件坏事情。你不应该把坏的代码留在生产中,然后再思考哪里出了问题。必要的时候,总是要恢复你的改变(回滚),越快越好。
实例会因为失败、部署或自动伸缩,而不断地启动、重新启动和停止。这会导致服务暂时或永久不可用。为了避免问题,你的负载均衡应该跳过不健康的实例,因为它们不能满足你的用户或子系统的需要。
应用实例健康可以通过外部观察来决策。你可以反复调用 GET /health 请求埋点或自身报告。现代服务发现解决方案,将不断从实例中收集健康信息,并配置负载均衡以保证健康的组件路由流量。
自我修复可以帮助恢复应用程序。我们谈论的自愈,是指应用程序可以做一些必要的步骤来恢复崩溃状态。在大多数情况下,这样的操作是经由一个外部系统来实现的,它会监控实例的健康,并在它们较长时间处于错误状态的情况下,重新启动应用程序。自愈是非常有用的,但是在某些情况下,不断地重启应用程序会引起麻烦。由于负载过高或者数据库连接超时,你的应用程序不停的重启,会导致无法提供一个正确的健康状态。
实现一种为微妙的情况而准备的高级自我修复解决方案,可能会很棘手,比如数据库连接丢失。在这种情况下,你需要为应用程序添加额外的逻辑来处理一些极端情况,并让外部系统知道不需要立即重启实例。
服务通常会因为网络问题和系统的变更而失败。由于自愈和先进的负载均衡,大多数中断只是暂时的,然而我们还应该找到一个解决方案,让我们的服务在这些故障中能够正常工作。这就是故障切换缓存,它可以帮助应用程序提供一些必要的数据。
故障切换缓存一般使用两个不同的过期时间。设置一个较短的时间,显示在正常情况下可以使用多长时间的缓存;设置另一个较长的时间,显示在发生故障期间,可供使用缓存数据的时间会有多久。
故障切换缓存
很重要的一点是,只有当过时的数据比什么都不做要好的情况出现时,才可运行故障切换缓。
可以通过使用HTTP中的标准响应头(response header)来设置缓存和故障转移缓存。
例如,通过设定 header 参数 max-age 来指定一个资源被刷新时最大时间;也可以通过设定 header 参数 stale-if-error 来决定,在服务失败的情况下,需要多长时间从缓存获取数据。
现代的CDN和负载均衡器提供了各种缓存和故障切换的方式,你也可以为公司建立一个包含了统一的可靠性解决方案的共享标准库。
在某些特定的场景下,我们可能无法缓存数据,或者我们想对其做出一些更改,但是我们的操作最终还是会失败。在这些情况下,我们可以重新尝试我们的操作,因为我们可以预计资源在一段时间后会恢复,或者我们的负载均衡将我们的请求转发到一个健康的实例。
在应用程序和客户端添加重试逻辑需保持谨慎,因为大量的重试会让事情变得更糟,甚至会阻止应用程序的恢复。
在分布式系统中,微服务系统重试会触发多个其他的请求或重试,引起一个级联效应。为了尽量减少重试带来的影响,你应该最大限度限制它们的发生次数,并使用指数补偿算法来持续增加重试之间的延迟。
重试由客户端(浏览器,其他微服务等)发起,客户端不知道这个操作是在处理请求之前失败还是之后失败的,你应该准备好应用程序来处理幂等性(idempotency)。例如,当操作重试购买时,不应该对用户进行重复扣费。对于每个事务,使用唯一的 幂等令牌(idempotency-key ),可以帮助处理重试。
限流是指在一个时间段内,特定的用户或应用程序可以接收或处理多少请求的技术。例如,有了限流,你就可以找出引起流量高峰的用户和微服务,或者可以确保应用不会在负载过高情况下,发生自动扩容都不能拯救。
你还可以限制业务优先级较低的流量,以便为核心业务提供足够的资源。
限速器可以抑制流量高峰
另外一种类型的限速器称为并发请求限制。当有一些昂贵的端点不应该超过指定的调用次数,但你仍然希望提供流量服务时,选择这样的操作是很有用的。
快速降级可以确保总是有足够的可用资源去服务关键的事务。它为高优先级请求保留一些资源,并且不允许低优先级事务使用所有的资源。降级与否是根据系统的整个状态进行判断的,而不是基于单个用户的请求桶大小。服务降级用于帮助恢复系统,当发生一些事故时,它们可以保证核心功能仍然继续工作。
如需获取更多有关限流与降级的信息,推荐前往 https://stripe.com/blog/rate-... ,阅读Stripe的文章。
在微服务体系结构中,我们希望我们的服务能够快速、独立地失败。为了在服务级别上隔离问题,我们可以采用舱壁模式(bulkhead pattern)。你稍后可以在这篇文章中读到更多关于舱壁的信息。
我们还希望我们的组件快速失败,因为我们不想等待坏的实例超时。没有什么比一个挂着的请求和一个没有响应的UI更令人失望的了。这样不仅浪费资源,而且还会对用户体验造成影响。我们的服务是相互调用的,所以更应该额外注意,在这些延迟结束之前,阻止挂起操作。
第一个想到的想法是在每个服务调用上运用一个较好级别的超时时间。这种方法的问题在于,你不可能真正知道什么是一个好的超时时间值,因为在某些情况下,网络故障和其他问题只会影响到一两个操作。在这种情况下,如果只有少数几个请求超时,你可能不想拒绝这些请求。
我们可以说,在微服务中使用超时来实现快速失败的例子是一种反模式,你应该避免它。你可以依赖于操作成功/失败统计数据的断路器(circuit-breaker)模式,而不是超时。
舱壁被用来将一艘船划分成多个部分,这样就可以在船体破裂的情况下对部分封闭。
隔离壁的概念可以应用于软件开发中,做到资源隔离。
通过采用舱壁模式,我们可以保护有限的资源不被耗尽。例如,如果我们有两种操作,它们与相同的数据库实例交互,我们的连接数量有限,那么我们可以使用两个连接池,而不是共享连接池。由于此客户端资源分离,当发生超时或者过度使用连接池的操作,不会导致所有其他操作的关闭。
泰坦尼克号沉没的主要原因之一,就是它的舱壁有一个设计上的失败,水可以通过舱壁顶部上的甲板注入,淹没整个船体。
泰坦尼克的舱壁(他们没有工作)
为了限制操作的持续时间,我们可以使用超时。超时可以防止挂起操作并保持系统响应。然而,在微服务通信中使用静态的、微调的超时是一种反模式,因为我们处在一个高度动态的环境中,几乎不可能发现正确的时间限制,以确保在每个场景下都能很好地工作。
我们可以使用熔断来处理错误,而不是使用小的特定事务的静态超时。断路器是以真实世界电子元件命名的,因为它们的行为是相同的(简单的说,这种模式主要是参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾)。你可以保护资源,帮助他们用断路器恢复。它们在分布式系统中非常有用,因为重复的失败会导致滚雪球效应(snowball effect),导致整个系统瘫痪。
当一个特定类型的错误在短时间内多次出现时,断路器就会打开。断路器的打开,阻止了进一步的资源请求——就像真的阻止了电流的流动。断路器通常在一定时间后关闭,为基础服务提供足够的空间来恢复。
请记住,并非所有的错误都应该触发断路器。例如,你可能希望跳过客户端问题,比如跳过 4xx 状态码响应的请求,但不包括 5xx 服务器端错误的请求。一些断路器也可以有半开状态,在此状态下,服务发送第一个请求检测系统的可用性,同时让其他请求失败。如果第一个请求成功,它将断路器恢复到一个关闭状态,并允许流量进入。否则,它就会打开。
你应该不断地测试你的系统以防止常见问题,以确保你的服务能够承受住各种失败。你应该频繁地测试失败,让你的团队为发生事故而做好准备。
对于测试,你可以使用一个外部服务来标识实例组,并随机终止该组中的一个实例。有了这个,你就可以为单个实例的失败做准备,你甚至可以关闭整个可用区来模拟云提供商的中断。
最流行的测试解决方案之一是由Netflix提供的ChaosMonkey弹性工具。