系统理论告诉我们,系统中相互关联的部分越多,发生大故障的可能性就越大。因此,要构建一个弹性系统,我们需要减少连接数量。如果无法做到这一点,我们需要实施“临时”切断与故障部分的连接的方法,以便错误不会级联到其他部分。
每个组件都必须假设每个其他组件都会在某个时候发生故障,并决定当这些故障发生时它会做什么。
最后,我们需要在系统中建立一些缓冲区——一些放松的方法,如果不消除对它的要求,以便有松弛来处理意外情况。
1 最小化组件间依赖
分布式系统的组件相互通信以获取数据或功能。在这两种情况下,我们都可以通过将数据/功能推送到调用组件而不是远程访问来减少连接需求。
构建大规模分布式系统迫使我们放弃标准软件工程的许多“最佳实践”。要记住的关键是,当我们采用分布式系统的复杂性来实现可扩展性时,我们还需要尽可能地控制“分布”。
1.1 重复数据
如果我们经常从另一个组件访问一些数据,我们可以在我们的组件中复制它,而不必在运行时检索它。这可以大大减少运行时依赖并帮助改善我们组件的延迟。
经常访问但有一定规律性变化的数据可以通过定期缓存刷新来临时缓存。更改频率更低或从不更改的数据(例如客户姓名)可以直接存储在我们的组件中。如果/当这些数据发生变化时,我们可能需要做一些额外的工作,但是这种增加的小开销通常是值得的,因为它可以提高弹性。
1.2 非规范化数据
非规范化是在组件内发生的一种特殊形式的重复。如果我们使用关系数据存储,我们可以通过在主实体中复制数据来降低查看多个实体的成本。本地化分散数据以获得更好性能的原则也适用于此。
1.3 库
为了减轻另一个组件的功能依赖性,我们可以将远程组件打包为库并将其嵌入到我们的组件中。这并不总是可能的(它可能是用其他语言编写的,或者太大而不能成为一个库)并且会带来一系列问题(功能的变化需要跨多个组件进行库升级),但是如果功能很关键并且经常被大规模访问,这是打破组件间连接并使其成为本地的可行方法。
2 隔离错误
错误隔离很重要,原因有两个。一是个别错误在分布式系统中更常见(许多移动部件的简单功能)。另一个是,如果我们不能防止整个系统中的联锁错误,那么我们首先就失去了构建复杂体的理由。
错误隔离的主要结构是 SLA。每个组件都声明了一些质量参数,它将在执行功能时得到尊重。这些参数可以包括延迟、错误率、并发性等。
在此 SLA 之外,调用它的组件会假定它已失败并需要自行采取适当的措施。如果组件本身检测到它无法维护其 SLA,它可以先发制人地告诉其调用者暂停并稍后再来调用。
为了保持整体系统健康,最好是快速失败而不是在违反 SLA 的情况下成功。两个组件(一个被唤起的和一个唤起的)都必须为此设置机制。
2.1 保护调用者
超时:如果被调用的组件在其 SLA 内没有响应,调用者必须超时(放弃)并改用一些回退机制(即使它抛出错误)来维护自己的 SLA 并防止一连串的 SLA 违规。
重试:由于网络不可靠,分布式系统中的许多错误只是随机的。如果调用者自己的 SLA 允许,调用者可以重试该操作。重试的前提是操作的幂等性。即它不应该改变状态或只做一次,即使它被调用了两次。
断路器:如果对组件的调用连续失败,调用者可以通过“打开电路”切断连接并停止调用一段时间。由于调用者已经有一些错误场景的备份行为,这节省了调用者宝贵的资源,这些资源本来会被浪费掉。停止调用还可以减少被调用组件的负载,并给它一些恢复的喘息空间。
断路器库具有定期轮询有问题的组件并在其性能似乎已恢复正常时重新启动调用流程的机制。
2.2 保护被调用
随机间隔:虽然重试可以减少错误,但在一个频繁使用的组件中出现一个小的性能问题可能会导致其所有调用者一次重试。这种“重试风暴”会造成负载峰值并阻止该组件恢复。为了防止这种情况,重试应该在它们之间有一个随机的时间间隔,以便交错加载。
背压:如果一个组件检测到自己承受过多的负载并且即将违反其 SLA,它可以抢先开始丢弃新请求,直到其性能得到控制。这比接受它知道它不能在 SLA 内提供服务或没有完全崩溃风险的请求要好得多。
3 在系统中建立缓冲区
3.1 异步通信
消息总线之类的异步通信通道允许调用远程组件,而无需非常严格的 SLA 依赖。通过让被调用组件准备好而不是立即使用消息,系统对增加的工作负载的需求变得更加灵活。
3.2 弹性配置
可扩展性最终归结为充分利用可用硬件。但是,如果看到规模增长,让系统缓口气的一个简单方法是分配更多硬件。虽然这仅在我们能够承受的成本范围内是可行的,但它为我们提供了抵御不可预测的负载变化的最后一道防线。