微服务架构可以通过定义明确的服务边界隔离故障。但就像在每个分布式系统中一样,网络、硬件或应用程序级别问题的可能性更高。由于服务依赖关系,任何组件都可能对其消费者暂时不可用。为了最大限度地减少部分中断的影响,我们需要构建可以优雅地响应某些类型的中断的容错服务。
架构师的宝库,每天一篇,开拓你的视野和深度。分享企业架构,业务架构,应用架构,数据架构,技术架构,安全架构等。讨论架构框架,规划,治理,标准,落地。交流新兴的架构风格和模型。如微服务,事件驱动,微前端,大数据,数仓,物联网,人工智能架构。
本文基于 RisingStack 的 Node.js 咨询与开发经验,介绍了构建和运行高可用微服务系统的最常用技术和架构模式。
如果您不熟悉本文中的模式,并不一定意味着您做错了什么。建立一个可靠的系统总是需要额外的成本。
更新:本文多次提到 Trace,RisingStack 的 Node.js 监控平台。2017 年 10 月,Trace 与 Keymetrics 的 APM 解决方案合并。点击这里试一试!
微服务架构的风险
微服务架构将应用程序逻辑转移到服务中,并使用网络层在它们之间进行通信。通过网络而不是内存调用进行通信会给系统带来额外的延迟和复杂性,这需要多个物理和逻辑组件之间的协作。分布式系统的复杂性增加导致特定网络故障的可能性更高。#microservices 允许您实现优雅的服务降级,因为可以将组件设置为单独失败。
与单体架构相比,微服务架构的最大优势之一是团队可以独立设计、开发和部署他们的服务。他们对其服务的生命周期拥有完全的所有权。这也意味着团队无法控制他们的服务依赖关系,因为它更有可能由不同的团队管理。对于微服务架构,我们需要记住,提供者服务可能会因发布、配置和其他更改的中断而暂时不可用,因为它们由其他人控制,并且组件彼此独立移动。
优雅的服务降级
微服务架构的最大优势之一是您可以隔离故障并在组件单独失败时实现优雅的服务降级。例如,在照片共享应用程序中断期间,客户可能无法上传新照片,但他们仍然可以浏览、编辑和共享现有照片。
微服务单独失败(理论上)
在大多数情况下,很难实现这种优雅的服务降级,因为分布式系统中的应用程序相互依赖,您需要应用几种故障转移逻辑(其中一些将在本文后面介绍)来准备 临时故障和中断。
Services depend on each other and fail together without failover logics.
变更管理
谷歌的网站可靠性团队发现,大约 70% 的中断是由实时系统的变化引起的。当您更改服务中的某些内容时——部署新版本的代码或更改某些配置——总是有可能失败或引入新错误。
在微服务架构中,服务相互依赖。这就是为什么你应该尽量减少失败并限制它们的负面影响。要处理变更带来的问题,您可以实施变更管理策略和自动推出。
例如,当您部署新代码或更改某些配置时,您应该逐渐将这些更改应用到您的实例子集,监控它们,甚至在您发现部署对您的关键指标产生负面影响时自动恢复。
Change Management – Rolling Deployment
另一种解决方案可能是您运行两个生产环境。您总是只部署到其中一个,并且只有在验证新版本按预期工作后才将负载均衡器指向新的。这称为蓝绿或红黑部署。
还原代码并不是一件坏事。您不应该将损坏的代码留在生产环境中,然后再考虑问题出在哪里。如有必要,请始终还原您的更改。越早越好。
健康检查和负载均衡
由于故障、部署或自动缩放,实例不断启动、重启和停止。它使它们暂时或永久不可用。为避免出现问题,您的负载均衡器应从路由中跳过不健康的实例,因为它们无法满足客户或子系统的需求。
应用程序实例的健康状况可以通过外部观察来确定。您可以通过重复调用 GET /health 端点或通过自我报告来做到这一点。现代服务发现解决方案不断从实例收集健康信息,并将负载均衡器配置为仅将流量路由到健康组件。
自我修复
自我修复可以帮助恢复应用程序。当应用程序可以执行必要的步骤从损坏状态中恢复时,我们可以谈论自我修复。在大多数情况下,它是由一个外部系统实现的,该系统监视实例的运行状况并在它们长时间处于损坏状态时重新启动它们。在大多数情况下,自我修复非常有用,但是在某些情况下,它可能会通过不断地重新启动应用程序而导致麻烦。当您的应用程序由于过载或数据库连接超时而无法提供积极的健康状态时,可能会发生这种情况。
实施先进的自我修复解决方案,为微妙的情况(如丢失的数据库连接)做好准备可能会很棘手。在这种情况下,您需要向应用程序添加额外的逻辑来处理边缘情况,并让外部系统知道不需要立即重新启动实例。
缓存故障转移
由于网络问题和我们系统的变化,服务通常会失败。然而,由于自我修复和高级负载平衡,这些中断中的大多数都是暂时的,我们应该找到一种解决方案,让我们的服务在这些故障期间正常工作。这就是故障转移缓存可以提供帮助并向我们的应用程序提供必要数据的地方。
故障转移缓存通常使用两个不同的到期日期;较短的表示您在正常情况下可以使用缓存多长时间,较长的表示您可以在故障期间使用缓存的数据多长时间。
故障转移缓存
值得一提的是,您只能在故障转移缓存为过时数据提供服务时使用总比没有好。
要设置缓存和故障转移缓存,您可以使用 HTTP 中的标准响应标头。
例如,使用 max-age 标头,您可以指定资源被视为新鲜的最长时间。使用 stale-if-error 标头,您可以确定在发生故障时应该从缓存中提供资源多长时间。
现代 CDN 和负载均衡器提供各种缓存和故障转移行为,但您也可以为您的公司创建一个包含标准可靠性解决方案的共享库。
重试逻辑
在某些情况下,我们无法缓存数据或想要对其进行更改,但我们的操作最终会失败。在这些情况下,我们可以重试我们的操作,因为我们可以预期资源会在一段时间后恢复,或者我们的负载均衡器将我们的请求发送到一个健康的实例。
向应用程序和客户端添加重试逻辑时应小心谨慎,因为大量重试会使情况变得更糟,甚至会阻止应用程序恢复。
在分布式系统中,一个微服务系统重试可以触发多个其他请求或重试,并启动级联效果。为了最大限度地减少重试的影响,您应该限制重试的数量并使用指数退避算法不断增加重试之间的延迟,直到达到最大限制。
由于重试是由客户端(浏览器、其他微服务等)发起的,并且客户端在处理请求之前或之后不知道操作失败,因此您应该准备应用程序来处理幂等性。例如,当您重试购买操作时,您不应向客户重复收费。为每个事务使用唯一的幂等键有助于处理重试。
速率限制器和减载器
速率限制是一种定义特定客户或应用程序在一段时间内可以接收或处理多少请求的技术。例如,通过速率限制,您可以过滤掉导致流量峰值的客户和微服务,或者您可以确保您的应用程序不会过载,直到自动缩放无法挽救。
您还可以阻止较低优先级的流量,为关键事务提供足够的资源。
A rate limiter can hold back traffic peaks
一种不同类型的速率限制器称为并发请求限制器。当您拥有不应超过指定时间调用的昂贵端点,而您仍想提供流量时,它会很有用。
车队使用负载卸载器可以确保始终有足够的资源可用于服务关键事务。它为高优先级请求保留一些资源,并且不允许低优先级事务使用所有这些资源。卸载程序根据系统的整个状态做出决策,而不是基于单个用户的请求桶大小。卸载程序可帮助您的系统恢复,因为它们可以在您遇到持续事件时保持核心功能正常工作。
要了解有关速率限制器和负载粉碎器的更多信息,我建议查看 Stripe 的文章。
快速失败和独立
在微服务架构中,我们希望让我们的服务能够快速且独立地失败。为了隔离服务级别的问题,我们可以使用隔板模式。您可以稍后在此博客文章中阅读有关隔板的更多信息。
我们还希望我们的组件快速失败,因为我们不想等待损坏的实例直到它们超时。没有什么比挂起的请求和无响应的 UI 更令人失望的了。这不仅浪费资源,还破坏了用户体验。我们的服务是链式调用的,所以我们应该特别注意在这些延迟总结之前防止挂起操作。
您想到的第一个想法是为每个服务调用应用精细等级超时。这种方法的问题在于,您无法真正知道什么是好的超时值,因为在某些情况下发生网络故障和其他问题时只会影响一两次操作。在这种情况下,如果只有少数几个超时,您可能不想拒绝这些请求。
我们可以说,通过使用超时来实现微服务中的快速失败范例是一种反模式,您应该避免它。您可以应用取决于操作的成功/失败统计信息的断路器模式,而不是超时。
隔板
舱壁在工业中用于将船舶分隔成多个部分,以便在船体破裂时可以将部分密封起来。
隔板的概念可以应用于软件开发以隔离资源。
通过应用舱壁模式,我们可以保护有限的资源不被耗尽。例如,如果我们有两种操作与连接数量有限的同一个数据库实例进行通信,我们可以使用两个连接池而不是 shared on。由于这个客户端 - 资源分离,超时或过度使用池的操作不会导致所有其他操作停止。
泰坦尼克号沉没的主要原因之一是它的舱壁设计失败,水可以通过上面的甲板从舱壁顶部倾泻而下,淹没整个船体。
Bulkheads in Titanic (they didn’t work)
断路器
为了限制操作的持续时间,我们可以使用超时。超时可以防止挂起操作并保持系统响应。然而,在微服务通信中使用静态的、微调的超时是一种反模式,因为我们处于一个高度动态的环境中,几乎不可能提出在每种情况下都能正常工作的正确时间限制。
我们可以使用断路器来处理错误,而不是使用小的和特定于事务的静态超时。断路器以真实世界的电子元件命名,因为它们的行为是相同的。您可以通过断路器保护资源并帮助它们恢复。它们在分布式系统中非常有用,其中重复性故障会导致滚雪球效应并导致整个系统瘫痪。
当特定类型的错误在短时间内多次发生时,断路器会打开。一个打开的断路器会阻止进一步的请求——就像真正的断路器阻止电子流动一样。断路器通常在一定时间后关闭,为底层服务恢复提供足够的空间。
请记住,并非所有错误都应该触发断路器。例如,您可能希望跳过客户端问题,例如具有 4xx 响应代码的请求,但包括 5xx 服务器端故障。一些断路器也可以处于半开状态。在这种状态下,服务发送第一个请求以检查系统可用性,同时让其他请求失败。如果第一个请求成功,它将断路器恢复到关闭状态并让流量流动。否则,它会保持打开状态。
Circuit Breaker
测试失败
您应该针对常见问题不断测试您的系统,以确保您的服务能够承受各种故障。您应该经常测试故障,以使您的团队为事件做好准备。
对于测试,您可以使用识别实例组并随机终止该组中的一个实例的外部服务。有了这个,您可以为单个实例故障做好准备,但您甚至可以关闭整个区域以模拟云提供商中断。
最受欢迎的测试解决方案之一是 Netflix 的 ChaosMonkey 弹性工具。
奥特罗
实施和运行可靠的服务并不容易。这需要您付出很多努力,也需要您的公司花钱。
可靠性有很多层面和方面,因此为您的团队找到最佳解决方案非常重要。您应该将可靠性作为业务决策过程中的一个因素,并为此分配足够的预算和时间。
关键要点
- 动态环境和分布式系统(如微服务)会导致更高的故障几率。
- 服务应该单独失败,实现优雅降级以改善用户体验。
- 70% 的中断是由更改引起的,还原代码并不是一件坏事。
- 快速而独立地失败。团队无法控制他们的服务依赖关系。
- 缓存、隔板、断路器和速率限制器等架构模式和技术有助于构建可靠的微服务。