性能文章>【译】设计分布式系统时,我们容易忽视的8个经典错误假设>

【译】设计分布式系统时,我们容易忽视的8个经典错误假设转载

2年前
427128

你在做分布式系统吗?微服务、Web API、SOA、Web服务、服务器、数据库、缓存、负载均衡——是不是把这些组合到一起就行了呢?

当然不是,分布式系统不仅仅要组合在一起,更重要的是它们之间如何高效的合作。

20 多年前,Peter Deutsch 和 James Gosling 整理了分布式系统设计背后的 8 个错误假设,这也是许多开发人员刚开始设计分布式系统时容易忽略的东西。

从长远来看,这8个错误假设最终都会导致大问题。

8个谬误是:

  1. 网络可靠
  2. 延迟为零
  3. 带宽无上限
  4. 网络安全
  5. 结构永远不变
  6. 全能的超级管理员
  7. 传输零成本
  8. 各个网络都一样

让我们通过每个误解,讨论问题和潜在的解决方案。

1.网络可靠

问题

通过网络调用将失败。

今天的大多数系统都会通过网络调用各自其他服务。你是否与第三方系统(支付网关、会计系统、CRM)集成了?你做了网络服务调用吗?如果调用失败会怎样?

如果你正在查询数据,则可以进行简单的重试。但是如果你发送一个命令会发生什么呢?我们举一个简单的例子:

如果我们收到 HTTP 超时异常会发生什么?如果服务器没有处理请求,那么我们可以重试。

但是,如果它确实处​​理了请求,我们需要确保不会向客户重复收费。你可以通过使服务器具有幂等性来做到这一点。这意味着如果你使用相同的收费请求调用它 10 次,客户将只被收费一次。但是如果你没有正确处理这些错误,那么你的系统就有风险,出现问题以后处理起来也很复杂。

解决方案

既然通过网络调用可能会失败,我们能做些什么呢?

好吧,我们可以自动重试。排队系统非常擅长这一点。他们通常使用一种称为存储转发的模式。他们在本地存储消息,然后将其转发给收件人。如果收件人离线,排队系统将重试发送消息。MSMQ就是这种排队系统的一个例子。

但是这种变化会对你的系统设计产生很大的影响。毕竟你正在从请求/响应模型转变为触发和忘记。

而且由于你不再等待响应,因此需要更改系统中的用户旅程,不能仅只用队列发送替换每个 Web 服务调用。

结论

你可能会说,如今的网络更加可靠,确实如此!但是事情发生了。硬件和软件可能会出现故障 - 电源、路由器、失败的更新或补丁、弱无线信号、网络拥塞、啮齿动物或鲨鱼。是的,鲨鱼:在一系列鲨鱼咬伤之后,谷歌正在用 Kevlar 加固海底数据电缆

还有人为的一面。人们可以发起 DDOS 攻击,也可以破坏物理设备。

这是否意味着你需要放弃当前的技术堆栈并使用消息传递系统?

也不一定!你需要权衡失败的风险和需要进行的投资,可以通过投资基础架构和软件来最大程度地减少失败的机会。

有时候,失败是一种选择,但是在设计分布式系统时确实需要考虑失败。

2. 延迟为零

问题

网络上的呼叫不是即时的。

内存调用和互联网调用之间存在几个数量级的差异。你的应用程序连着网,也得将本地调用与远程调用分开。让我们看一下我在代码库中看到的示例:

粗看好像没问题,但是,这有两个远程调用。第 2 行调用一次以获取文档摘要列表。在第 5 行,还有另一个调用,用于检索有关每个文档的更多信息。这是一个经典的Select n+1 问题。为了考虑网络延迟,你应该在一次调用中返回所有必需的数据。一般的建议是本地调用可以是细粒度的,但远程调用应该更粗粒度。这就是为什么分布式对象和“网络透明”的想法消失了。但是,尽管每个人都同意分布式对象是一个坏主意,但有些人仍然认为延迟加载更好:

你不会期望属性 getter 进行网络调用。但是,每个“.” 上面代码中的调用实际上可以触发对数据库的访问。

解决方案

带回你可能需要的所有数据

如果你进行远程呼叫,请确保带回可能需要的所有数据,网络通信不应该是闲聊。

将数据放在更靠近客户端的地方

另一种可能的解决方案是将数据移近客户端。如果你使用的是云,请根据客户的位置仔细选择可用区。缓存还可以帮助减少网络调用的数量。对于静态内容,内容交付网络 (CDN) 是另一个不错的选择。

反转数据流

删除远程调用的另一个选择是反转数据流。我们可以使用 Pub/Sub 将数据存储在本地,而不是查询其他服务。

这样,我们将在需要时获得数据,这虽然有点复杂,但不可否认它同时也是一个好工具。

结论

尽管延迟在 LAN 中可能不是问题,但当你移动到 ​​WAN 或 Internet 时,延迟就是大问题。这也是为什么将网络调用与内存调用明确分开的原因。在采用微服务架构模式时,你应该牢记这一点。我们不应该只用远程调用替换本地调用,因为这很可能会将系统变成一个分布式的大泥球。

3.带宽无上限

问题

带宽有限。

带宽是网络在一段时间内发送数据的能力。大部分时候带宽问题都不是问题,但是在某些特殊情况下会成为一个大问题,比如视频流服务。

带宽问题,其实随着时间的推移在不断改善,但我们发送的数据量也日益庞大。与通过网络传递简单 DTO 的应用程序相比,视频流或 VoIP 需要更多的带宽。特别是对于移动应用程序来说更为重要,因此开发人员在设计后端 API 时就得注意这个问题。

错误地使用 ORM 也会造成问题。我见过开发人员在查询中过早调用 .ToList() 的示例,从而将整个表加载到内存中。

解决方案

领域驱动设计模式

那么我们如何确保我们不会带来太多数据呢?试试领域驱动设计模式:

  • 首先,你不应该争取单一的企业范围的域模型。您应该将你的域划分为有界上下文。
  • 为了避免在有界上下文中出现大而复杂的对象图,你可以使用聚合模式。聚合确保一致性并定义事务边界。

命令和查询职责分离

我们有时会加载复杂的对象图,因为我们需要在屏幕上显示其中的一部分。如果我们在很多地方都这样做,我们最终会得到一个庞大而复杂的模型,对于写作和阅读来说都是次优的。另一种方法可能是使用命令和查询职责分离- CQRS。这意味着将域模型一分为二:

  • 写入模型将确保不变量成立并且数据是一致的。由于写模型不关心视图问题,它可以保持小而专注。
  • 读取模型针对视图问题进行了优化,因此我们可以检索特定视图(例如我们应用程序中的屏幕)所需的所有数据。

结论

第二个误解,延迟不是 0和第三个谬误, 带宽无上限的之间有关联 。你应该传输更多数据以尽量减少网络往返次数。同时又应该传输较少的数据以尽量减少带宽使用。这就需要平衡这两种方案,并找到合适的数据以通过网络发送。

尽管我们可能很少遇到带宽限制,但考虑你传输的数据的重要性。更少的数据更容易理解。更少的数据意味着更少的耦合。因此,仅传输你可能需要的数据。

4. 网络安全

问题

网络不安全。

这是一个知名度更广的假设。你的系统有多安全?其实就看你最薄弱的环节怎么样?而分布式系统中有太多环节了。

你正在使用 HTTPS,但与不支持它的第 3 方遗留系统通信时除外。你正在审查自己的代码,寻找安全问题,但正在使用可能存在风险的开源库。OpenSSL 漏洞允许人们窃取受 SSL/TLS 保护的数据。Apache Struts 中的一个错误允许攻击者在服务器上执行代码。即使你要防范所有这些,仍然存在人为因素。恶意 DBA 可以“放错”数据库备份。

今天的攻击者手中有大量的计算能力和极大的耐心。所以问题不是他们是否会攻击你的系统,而是什么时候来攻击你的系统。

解决方案

纵深防御

你应该使用分层方法来保护系统,需要在网络、基础架构和应用程序级别进行不同的安全检查。

安全心态

设计系统时请牢记安全性。在过去的 5 年中,前十名漏洞列表没有太大变化。你应该遵循安全软件设计的最佳实践并检查常见安全漏洞的代码,而且应该定期搜索 3rd 方库以查找新漏洞。

这个常见漏洞和暴露列表可以提供帮助。

威胁建模

威胁建模是一种识别系统中可能存在的安全威胁的系统方法。你首先确定系统中的所有资产(数据库中的用户数据、文件等)以及它们的访问方式。之后,就可以识别可能的攻击并开始执行它们。我建议阅读高级 API 安全性中的第 2 章,以全面了解威胁建模。

结论

唯一安全的系统是断电的,未连接到任何网络(理想情况下是浇筑在一块混凝土中)。多么有用的系统啊!事实是,安全是困难和昂贵的。分布式系统中有很多组件和链接,每一个都是恶意用户的可能目标。业务需要平衡攻击的风险和概率与实施预防机制的成本。

攻击者手上有很大的耐心和计算能力。我们可以通过使用威胁建模来防止某些类型的攻击,但我们不能保证 100% 的安全性。因此,向业务部门说明这一点是一个好主意,共同决定在安全方面投资多少,并为何时发生安全漏洞制定计划。

5. 网络结构不变

问题

网络结构不断变化。

网络结构一直在变化。有时它会因意外原因而改变 - 当你的应用服务器出现故障并且需要更换它时。很多时候这是我们主动的——在新服务器上添加新进程。如今,随着云计算和容器的兴起,这一点更加明显。弹性扩展——根据工作负载添加或删除服务器的能力——需要一定程度的网络敏捷性。

解决方案

抽象网络的物理结构

你需要做的第一件事是抽象网络的物理结构。有几种方法可以做到这一点:

  • 停止对 IP 进行硬编码 - 应该更喜欢使用主机名。通过使用 URI,我们依靠 DNS 将主机名解析为 IP。
  • 当 DNS 不够用时(例如,当您需要映射 IP 和端口时),则使用发现服务。
  • 服务总线框架还可以提供位置透明性。

牛,不是宠物

通过将您的服务器视为一头牛,而不是宠物,你可以确保没有服务器是不可替代的。这一点智慧可以帮助您进入正确的心态:任何服务器都可能发生故障(从而改变拓扑),因此你应该尽可能多地自动化。

测试

最后一条建议是测试你的假设。停止服务或关闭服务器,看看系统是否仍在运行。像 Netflix 的Chaos Monkey这样的工具通过随机关闭生产环境中的虚拟机或容器来提升这一点。通过提出问题,就更有动力去构建一个可以处理结构变化的弹性系统。

结论

十年前,大多数网络结构并没有那么频繁地改变。但是当它发生时,它可能发生在生产中并引入了一些停机时间。如今,随着云计算和容器的兴起,这个错误假设很难被忽视。我们需要为失败做好准备并对其进行测试。不要等待它在生产中发生!

6. 全能的超级管理员

问题

没有一个人无所不知。

嗯,这个似乎很明显。当然,没有一个人是无所不知的。这是一个问题吗?只要应用程序运行顺利,它就不会。但是,当出现问题时,你就需要修复它。因为有这么多人接触了这个应用程序,知道如何解决问题的人可能不在。

有很多事情可能出错。一个例子是配置。今天的应用程序将配置存储在多个存储中:配置文件、环境变量、数据库、命令行参数。没有人知道每个可能的配置值的影响。

另一件可能出错的事情是系统升级。分布式应用程序有许多活动部分,你需要确保它们是同步的。例如,你需要确保当前版本的代码适用于当前版本的数据库。如今,人们更关注 DevOps 和持续交付,支持零停机部署并非易事。

但是,至少这些事情都在你的掌控之中。许多应用程序与第 3 方系统交互。这意味着,如果它们倒下,我们也无能为力。因此,即使系统只有超级管理员,我们仍然无法控制第三方系统。

解决方案

每个人都应该对发布过程负责

这意味着从一开始就涉及 Ops 人员或系统管理员。理想情况下,他们将成为团队的一员。尽早让系统管理员了解你的进度可以帮助你发现限制。例如,生产环境可能具有与开发环境不同的配置、安全限制、防火墙规则或可用端口。

记录和监控

系统管理员应该有正确的工具来报告错误和管理问题。你应该从一开始就考虑监控。分布式系统应该有集中的日志记录。访问十个不同服务器上的日志来调查问题是不可接受的方法。

解耦

在系统升级期间,你应该争取最少的停机时间。这意味着你应该能够独立升级系统的不同部分。通过使组件向后兼容,你可以在不同时间更新服务器和客户端。

通过在组件之间放置队列,你可以暂时解耦它们。这意味着,例如,Web 服务器仍然可以接受请求,即使后端已关闭。

隔离第三方依赖

你应该区别对待控制之外的系统与组件。这意味着你的系统对第 3 方故障更具弹性。同时我们可以通过引入抽象层来减少外部依赖的影响,这意味着当第 3 方系统出现故障时,我们将更快的来寻找到错误。

结论

要解决这个谬误,您需要使您的系统易于管理。DevOps、日志记录和监控可以提供帮助。您还需要考虑系统的升级过程。如果升级需要数小时的停机时间,则无法部署每个 sprint。没有一个管理员,所以每个人都应该对发布过程负责。

7、传输零成本

问题

运输成本不是零。

这个错误假设与第二个错误假设有关,即 延迟为零。通过网络传输东西在时间和资源上都是有代价的。如果第二个谬误讨论了时间方面,那么假设 #7 解决了资源消耗问题。

这个假设有两个不同的方面:

网络基础设施的成本

网络基础设施是有成本的。服务器、SAN、网络交换机、负载平衡器和维护这些设备的人员——所有这些都需要花钱。如果你的系统是在本地部署的,那么你需要预先支付此价格。如果你使用的是云,那么你只需为使用的内容付费,但仍然需要为此付费。

序列化/反序列化的成本

这种错误假设的第二个方面是在传输层和应用层之间传输数据的成本。序列化和反序列化会消耗 CPU 时间,因此需要花钱。如果你的应用程序是在本地部署的,那么如果不主动监控资源消耗,则此成本会有些隐藏。但是,如果应用程序部署在云中,那么这个成本就会非常明显,因为在为使用的东西付费。

解决方案

对于基础设施的成本,我们无能为力,只能确保尽可能高效地使用它。SOAP 或 XML 比 JSON 更昂贵。JSON 比 Google 的 Protocol Buffers 等二进制协议更昂贵。根据系统的类型,这可能或多或少重要。例如,对于与视频流或 VoIP 相关的应用程序,传输成本更为重要。

结论

我们应该注意传输成本以及您的应用程序正在执行多少序列化和反序列化。这并不意味着你应该优化,除非有需要。你应该对资源消耗进行基准测试和监控,并确定运输成本是否对你来说是个问题。

8. 网络都一样

问题

各个网络不一样

同构网络是使用相似配置和相同通信协议的计算机网络。拥有具有相似配置的计算机是一项艰巨的任务。例如,你几乎无法控制哪些移动设备可以连接到你的应用程序。这就是为什么关注标准协议很重要的原因。

解决方案

你应该选择标准格式以避免供应商锁定。这可能意味着 XML、JSON 或协议缓冲区。有很多选择可供选择。

结论

你需要确保系统的组件可以相互通信。使用专有协议会损害您的应用程序的互操作性。

设计分布式系统很难

这些错误假设整理于 20 多年前。但它们今天仍然适用,而且其中一些变得其他更重要。

今天许多开发人员其实都知道,但是在实际应用中却常常忽略。

我们必须接受这些事实:网络不可靠、不安全并且需要花钱。带宽有限。网络的结构将不断发生变化,其组件的配置方式也不同。

意识到这些限制将有助于我们设计一个更好的分布式系统。

点赞收藏
金色梦想

终身学习。

请先登录,查看2条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

API性能调优
8
2