当前位置: 首页 > 科技观察

避免大规模故障的微服务架构设计之道

时间:2023-03-22 11:04:50 科技观察

本文首先介绍了微服务架构的风险,然后针对如何避免微服务架构的故障,提出了微服务架构中的多种有效方法和技术。示例包括服务降级、变更管理、健康检查和修复、断路器、限流器等。微服务架构可以通过定义清晰的服务边界来有效隔离故障。与其他分布式系统一样,微服务在网络、硬件和应用层存在更多问题。由于服务相互依赖,任何组件都可能发生故障,用户可能无法访问它。为了最大限度地减少部分中断的影响,我们需要构建容错服务,以便它们可以在发生中断时进行处理。本文基于文章RisingStack的Node.js咨询与开发经验,介绍构建和运行高可用微服务架构系统中最常用的技术和架构模式。如果读者不熟悉上面的模式,那也没关系。构建可靠的系统并非一蹴而就。微服务架构的风险微服务架构将应用逻辑拆分成服务,服务通过网络进行交互。由于调用是通过网络进行的,而不是在进程中进行的,因此这会为需要多个物理和逻辑组件之间协作的系统带来潜在的问题和复杂性。分布式系统变得越来越复杂,这也增加了网络特定故障的可能性。与传统应用的庞大结构相比,微服务架构最大的优势之一就是团队可以独立设计、开发和部署自己的服务。团队可以控制其服务的整个生命周期。这也意味着团队无法控制服务的依赖关系,因为这些依赖的服务可能由其他团队管理。在微服务架构体系下,我们必须牢记,由于提供的服务受控于他人,可能会因为发布、配置等变化而暂时不可用,各组件之间是相互独立的。优雅的服务降级微服务架构的最大优势之一是能够隔离组件并在服务失败时优雅地降级服务。例如,在照片分享应用中,当出现故障时,用户可能无法上传图片,但仍可以浏览、编辑和分享上传的图片。微服务故障独立性(理论上)在大多数情况下,很难实现上图所示的服务优雅降级,因为在分布式环境中,应用程序是相互依赖的,开发人员需要实现几个错误处理逻辑(在本文后面讨论))来处理瞬态故障和中断。服务相互依赖,如果没有故障转移逻辑,它们会同时发生故障。变更管理Google的网站可靠性团队发现,大约70%的故障是由变更引起的。当对服务进行更改时(例如发布新版本的代码或更改某些配置),总是有可能出现故障或引入新的错误。在微服务架构中,服务是相互依赖的。这就是为什么您需要减少故障并将其负面影响降至最低的原因。应对变更带来的问题,可以实施变更策略管理,实现其自动回滚。例如,部署新代码或修改配置时,应将这些更改逐步部署到服务实例组中的某些实例并进行监控。如果发现关键指标有问题,可以自动回滚。变更管理-回滚部署另一种解决方案是运行两个生产环境。部署时,只将变更后的应用部署到其中一个环境,验证新发布的版本符合预期后,将负责均衡的流量指向新的应用。这种方法称为“蓝绿释放”或“红黑释放”。后备代码不是坏事。您不应该在生产环境中部署有缺陷的代码,您应该考虑哪里出了问题。必要时果断回滚代码,越快越好。由于健康检查和负载均衡,由于故障、部署、自动扩容等原因,服务实例会不断地启动、重启和停止。这会导致服务暂时或永久禁用。为了避免这些问题,应该配置负载均衡来忽略路由中的这些实例,因为它们不能为子系统或用户提供服务。我们可以通过外部观察来判断应用实例是否健康。可以多次调用Get/health端点(endpoint)或者通过自身服务的报告获取相关信息。今天的服务发现解决方案不断从实例中收集健康信息,并将负载均衡路由设置为仅指向健康的实例组件。自我修复自我修复有助于恢复应用程序。下面我们来讨论当应用程序遇到崩溃状态时,如何通过相关步骤进行自我修复。大多数情况下,实例的状态是通过外部系统监控的,当服务出现故障时,会在一段时间后重启服务。在大多数情况下,自我修复功能非常有用,但在某些情况下,由于服务不断重启,它可能会导致相关问题。例如,当服务过载或数据库连接超时时,应用无法反馈正确的服务健康状态。对于某些场景——比如数据库连接丢失,此时实现高级自愈功能是相当棘手的。在这种情况下,需要在应用程序中添加额外的逻辑来处理这些特殊情况,并让外部系统知道服务的实例不需要立即重启。故障转移缓存(FailoverCaching)服务经常会因为网络问题和系统变化而失败。然而,大多数这些中断都是暂时的,并且由于自我修复和高级负载平衡功能,我们应该找到一种解决方案,即使在发生故障时也能让服务继续工作。这就是故障转移缓存(FailoverCaching),它可以帮助我们的应用程序提供必要的数据。故障转移缓存通常使用两个不同的到期日期:一个较短的日期表示缓存在正常情况下可以使用多长时间,一个较长的日期表示缓存中的数据在发生故障时可以使用多长时间。故障转移缓存的一个特殊警告是故障转移缓存只应在提供陈旧数据总比没有数据好时才使用。要设置缓存和故障转移缓存,您可以使用HTTP中的标准响应标头。例如,使用max-age头来指定资源的最后一次是新资源(译者注:设置max-age后,浏览器将不再向服务器发送请求)。stale-if-error标头可用于确定在发生故障时从缓存中获取资源需要多长时间。今天的CDN和负载平衡器提供各种缓存和故障转移解决方案,但您也可以在您的公司中构建一个包含这些标准可靠性解决方案的共享库。重试逻辑在某些情况下,我们可能无法缓存数据,或者我们想对数据进行更改,但最终操作失败。在这种情况下,我们可以选择重试操作,因为我们可以预期资源会在一段时间后恢复,或者负载均衡器会将请求发送给健康的实例。在向应用程序和客户端添加重试逻辑时应该小心,因为更多的重试操作会使事情变得更糟,甚至会阻止应用程序恢复。在分布式系统中,一个微服务系统重试可能会触发多个其他请求或重试操作,造成级联效应。要减少重试的影响,应该减少重试的次数,并使用指数退避算法(exponentialbackoffalgorithm)不断增加重试之间的延迟,直到达到最大限制。由于重试是由客户端(浏览器、其他微服务等)发起的,客户端不知道处理请求前后的失败,所以你应该为你的应用提供幂等处理能力。例如,当您再次尝试购买时,您不应向客户收取两次费用。为每个事务使用唯一的幂等密钥是重试问题的解决方案。速率限制器和负载卸载器(RateLimitersandLoadShedders)速率限制器和负载卸载器是指定义某个客户端或应用程序在一段时间内可以接收或处理多少请求的技术。例如,通过节流,您可以过滤掉产生流量峰值的客户端和微服务,或者您可以确保您的应用程序在AutoScaling失败之前不会过载。您还可以阻止优先级较低的流量,以便为关键事务提供足够的资源。速率限制器可以防止流量高峰。还有另一种类型的速率限制器称为“并发请求限制器”。当您有一些昂贵且重要的端点时,您希望调用这些端点的次数不应超过指定次数,但仍想为流量提供服务时,此限制器很有用。使用负载开关可确保始终为关键事务提供足够的资源保证。它为高优先级的请求预留了一些资源,不允许低优先级的事务占用这些资源。负载开关根据系统的整体状态做出决定,而不是根据单个用户请求的存储桶大小。负载设备有助于您的系统恢复,因为它们在持续的故障事件中保持核心功能正常工作。关于限流器和负载开关的更多知识,建议读者参考Stripe的相关文章。快速且独立地失败在微服务架构中,我们希望服务能够快速且独立地失败。为了隔离服务级别的故障,我们可以使用隔板模式。您可以在本文后面阅读相关内容。我们还希望我们的组件快速失败,因为我们不想等待断开连接的实例超时。没有什么比挂起的请求和无响应的界面更令人沮丧的了。这不仅是资源的浪费,还会使用户体验变差。我们的服务相互调用,因此应特别注意防止在这些延迟加起来之前超时的操作。您想到的第一种方式可能是为每个服务调用定义一个超时级别。这种方法的问题是您无法真正知道正确的超时值是多少,因为会发生网络故障和其他问题,并且在某些情况下只会影响一两个操作。在这种情况下,如果只有其中一些请求超时,您可能不想拒绝所有这些请求。我们可以说,通过使用超时在微服务中实现快速失败是一种应该避免的反模式。可以使用基于操作成功/失败统计信息的断路器,而不是使用超时。舱壁模式(Bulkheads)在工业领域,舱壁常被用来将其分成几个部分,这样当船体的某一部分破裂时,其他部分仍然可以密封和安全。隔板的概念也可以用在软件开发中来隔离资源。通过使用舱壁模式,我们可以保护有限的资源不被耗尽。例如,如果我们有两种类型的操作与同一个数据库实例进行通信,并且数据库限制了连接数,那么我们可以使用两个连接池而不是一个共享连接池。由于客户端和资源的这种分离,超时或过度使用池的操作不会使所有其他操作无效。泰坦尼克号沉没的主要原因之一是其舱壁的设计使得水可以通过上方的甲板倾泻到舱壁顶部,最终淹没整艘船。泰坦尼克号失事的断路器为了限制操作的持续时间,我们可以使用超时。超时可防止挂起的操作并保持系统响应。然而,在微服务架构通信中使用静态、微调的超时是一种反模式,因为我们处于高度动态的环境中,几乎不可能确定在每种情况下都能正常工作的确切时间限制。我们可以使用断路器来处理错误,而不是使用小而具体的基于事务的静态超时机制。断路器以真实世界的电子元件命名,因为它们的行为都相同。您可以使用断路器保护资源并协助其恢复。断路器在分布式系统中非常有用,因为重复的故障会导致滚雪球效应并导致整个系统瘫痪。当指定类型的错误在短时间内多次发生时,断路器打开。断开的断路器可以拒绝进一步的请求——就像阻止电子的实际流动一样。断路器通常会在一定时间后关闭,以便为底层服务的恢复留出足够的空间。请记住,并非所有错误都应触发断路器。例如,您可能希望忽略客户端问题,例如具有4xx响应代码的请求,但包括5xx服务器端故障。一些断路器也可以有半开关状态。在这种状态下,服务发送第一个请求以检查系统的可用性,同时让其他请求失败。如果第一个请求成功,则将断路器返回到关闭状态并继续接受流量。否则,请保持打开状态。故障测试(TestingforFailures)你应该不断地测试系统的常见问题,以确保你的服务能够在各种故障情况下运行。您应该经常测试失败,让您的团队为可能发生的事故做好准备。对于测试,您可以使用外部服务来识别服务实例组并随机终止运行组中的实例。使用这种方法,您可以针对单个实例故障进行测试,甚至可以关闭整个服务组以模拟云提供商级别的中断。最好的测试解决方案之一是Netflix的ChaosMonkey工具。总结实施和运营可靠的服务并不容易。这需要您付出很多努力,并使公司付出更多代价。可靠性有很多层次和方面,因此为您的团队找到合适的解决方案很重要。您应该将可靠性作为业务决策过程中的一个因素,并为其分配足够的预算和时间。要点动态环境和分布式系统——如微服务将导致更高的失败机会。服务应该单独失败,从而实现优雅的服务降级以改善用户体验。70%的问题是由更改引起的,恢复可用代码并不总是坏事。快速且单独地失败。团队无法控制他们的服务依赖性。缓存、隔离技术、断路器和限流器等架构模式和技术有助于构建可靠的微服务。作者简介:PéterMarton,RisingStack的CTO,擅长使用nodejs构建微服务。