【微服务】设计弹性微服务架构模式
@TOC
推荐超级课程:
在微服务架构中,服务通常相互协作以提供业务用例。这些服务可能在可用性、可伸缩性、弹性等方面具有自己的软件特性,但总会出现这样的情况,即您试图通信的下游服务要么不可用 — 可能是由于计划的停机、临时网络故障等,要么可用但由于某种原因响应缓慢。
在这种情况下,依赖的消费者服务如何做出反应是非常重要的?理想情况下,期望是这些依赖服务具有强健的特性,能够优雅地处理此类问题。但如果它们不具有弹性,那么您将影响这些服务负责的功能的可用性。
这就是我们在这篇博客中要讨论的内容 — 构建具有弹性的微服务的模式!!!
模式#1 — 超时
在分布式系统中,当一个服务向另一个下游服务发出请求时,完成往返需要时间,并不是立即完成的。但是,如果下游服务的响应速度慢于平常,或者可能根本不响应,那么调用方服务在收到响应之前应等待多长时间才能得出结论 — 有些地方出了问题,让我选择其他路径(如果有的话)。该备用路径也可能是一种优雅的失败。
如果等待的时间太长,那么可能会减慢调用方服务。如果放弃得太早,则可能事情本来会好转,但您却变得过于激进并宣布失败。如果没有配置任何超时,则调用方服务很快就会陷入挂起状态,影响其负责的所有功能。
让我们通过一个示例来看看 — 一个处理内容创建和内容推广用例的内容微服务。
在这种情况下,如果在创建新内容时,内容审核服务需要时间来响应,则将影响内容创建时间。随着越来越多的内容创建请求到来,系统资源将被阻塞,等待内容审核服务的响应,最终导致资源争用。并且由于资源短缺,如果有人尝试执行共享内容用例,因为资源紧缺,内容服务将无法处理请求,尽管外部社交平台集成服务正在正常运行。
在此情况下,异步通信可能效果更好,但这里显示同步通信只是为了解释超时问题。
好的,我明白了,但您如何确定何时超时最合适?嗯,这取决于很多因素 —
- 了解进行外部调用的上下文。如果在上述情况下创建内容的服务级别目标(SLO)为3秒,则内容审核请求的超时时间不能超过3秒。
- 如果一个用例总体具有5秒的SLO,并且实现调用了多个 API 调用,则计划针对各个 API 调用设置超时时间,以使各个调用不超过5秒的总体 SLO。
- 可以根据调用所处的上下文计算等待的时间量。例如 — 如果内容审核的 SLO 为3秒,并且在1秒后调用内容审核 API,则在总体交易超时之前,您剩下的时间只有2秒可用于内容审核请求的超时。
- 观察外部 API 调用的 p90 或 p95 延迟,并相应地设置超时时间。
模式#2 — 重试
如果下游服务未能响应,则在情况出现临时问题且重试可能有所帮助时,您可能希望通过重试操作来处理该情况。
下游服务未响应可能有多种原因 —
- 当您第一次发出请求时,下游服务的新版本可能正在部署,因此无法响应
- 可能存在临时网络故障
- 可能 API 网关/代理服务负荷过大,无法将请求转发到目标服务。
但是,您如何确定重试是否有所帮助,或者是否应始终重试?这要取决于您从最初调用中收到的响应。例如 —
- 如果仅超时,则可能有所帮助。
- 假设您正在进行 HTTP 请求,如果获得 HTTP 502 或 504 状态码,则重试可能有所帮助。
- 如果收到 HTTP 状态码500,或者可能是 404 等状态码,则重试是没有意义的。
顺便说一句,在进行重试时,要考虑执行操作的上下文。从我们上面的内容创建用例中,如果整体 SLO 为3秒,则如果某个原因导致内容审核 API 失败,您可能希望重试内容审核 API,但要确保重试次数不超过您为内容创建超时设置的总时间。
模式#3— 隔离
这是一种隔离特定功能领域故障的模式,旨在确保服务的其余特性不受影响。
这类似于我们在船上使用的隔舱设计 — 如果船出现漏水,我们可以关闭该隔舱门,确保船的特定问题区域无法使用,但船的其余部分继续按预期工作。
这里的想法非常简单 —
- 为您想要连接的每个下游服务创建一个工作线程池
- 配置可以使用该池进行的最大并发调用
- 让消费者服务使用此池向外部服务发出请求,如果该池已满且没有空闲工作线程,则根据用例等待一段时间或立即失败。
- 通过这种方式,我们确保由于单个下游服务的故障而导致所有系统资源被窒息,其他健康的下游服务可以从服务提供的其他功能中调用。
如果我们尝试在我们的内容服务中应用这种模式,它将如下所示 —
模式#4— 断路器
断路器是一种广泛使用的模式,用于实现弹性的微服务。这个想法与我们家庭中的电气断路器非常类似 — 电源突然升高会直接切断电路,确保设备的安全。
在微服务世界中,如果下游服务无法及时回应消费者服务,那么消费者服务最好不要继续向下游服务发送更多请求,让下游服务从情况中恢复,暂时不发出请求。
如果在这种情况下,消费者服务继续向下游服务发出请求,它们将不得不等待超时发生。并且随着越来越多的请求等待超时,系统资源将用于处理其他用例而被耗尽。因此,对于消费者服务而言,快速失败是合理的,前提是它们知道下游服务无法处理请求。这就是断路器模式的用处。
顺便说一句 — 即使消费者服务已实施超时模式以处理缓慢响应、无响应或错误响应,新的请求仍继续发送到下游服务,这将进一步恶化已经处于困境中的下游服务的状况。
断路器模式的思想非常简单 — 观察当调用下游服务时连续失败的请求数。如果此数字超过阈值,则打开断路器 — 也就是说,不要再向下游服务发出更多请求,并快速失败。为下游服务留出休整时间以恢复。并在一段时间后,尝试几次调用下游服务,如果获得成功响应,则关闭断路器 — 也就是说所有随后的请求将继续发送到下游服务。
配置何时关闭断路器和何时重试以便意图关闭断路器可能有些棘手。您可能希望从明智的默认值开始,然后继续根据情况逐步调整值。
关键是检测失败 -> 快速失败 -> 优雅处理请求,并确保系统资源可用以处理不涉及此下游服务的其余用例的功能。
模式#5 — 冗余
顾名思义 — 始终具有冗余副本有助于实现韧性。这种冗余副本可以以主动-主动模式运行,与主实例一起提供服务,或者作为待命副本处于闲置状态,以便在主要实例由于某种原因而失败时接管请求。
如果运行多个下游微服务实例,并且其中一个实例退出运行,则消费者服务不受影响,因为还有其他实例可用于处理请求。这可能导致响应速度较慢和吞吐量下降,但系统将继续工作。您可以使用足够的自动化来启动一个新实例,因此在几秒钟或几分钟内,下游服务的吞吐量就可以恢复到全负荷。另一个例子可以是 — 拥有一个待命数据库。如果主数据库出现故障,那么备用数据库可以承担主要角色,因此有助于快速故障切换。
冗余还有助于确保服务的高可用性和可伸缩性。但涉及成本。您想要拥有的冗余副本越多,您付出的成本就越高。因此,我们需要在这里权衡。
正如他们所说,事情总会失败,最终。因此,您可以将精力投入到确保它不会失败,或者将相同精力引导到确保系统具有足够的弹性来处理失败,并具有从失败中快速恢复并适应自身以确保再次发生同样的失败时优雅处理的能力。