理解分布式系统的8个谬误,构建更好、更灵活的架构
了解这些对微服务,SOA等分布式系统的误解将有助于你构建更好,更灵活的软件架构。
你在分布式系统上工作吗?微服务,Web API,SOA,Web服务器,应用服务器,数据库服务器,缓存服务器,负载均衡。如果这些描述了系统设计中的组件,那么答案是肯定的。分布式系统由许多计算机组成,这些计算机协调以实现共同的目标。
20多年前,Peter Deutsch和James Gosling定义了分布式计算的8个谬误。这些是许多开发人员对分布式系统做出的错误假设。从长远来看,这些假设通常被证明是错误的,而且会导致难以修复的错误。
8个谬误分别是:
- 网络可靠。
- 延迟为零。
- 带宽是无限的。
- 网络是安全的。
- 拓扑不会改变。
- “无所不能”的管理员。
- 传输成本为零。
- 网络是同构的。
让我们来看看每个谬误,讨论,以及潜在的解决方案。
1.网络可靠
问题:通过网络调用将失败。
今天的大多数系统都会调用其他系统。你是否正在与第三方系统(支付网关,会计系统,CRM)集成?你在做web调用吗?如果调用失败会发生什么?如果你要查询数据,则可以进行简单的重试。但是如果你发送命令会发生什么?我们举一个简单的例子:
如果我们收到HTTP超时异常会怎么样?如果服务器没有处理请求,那么我们可以重试。但是,如果它确实处理了请求,需要确保我们不会对客户进行双重收费。你可以通过使服务器具有幂等性来实现此目的。这意味着如果你使用相同的收费请求调用10次,则客户只需支付一次费用。如果你没有正确处理这些错误,那么你的系统是不确定的。处理所有这些情况可能会非常复杂。
解决方案
因此,如果网络上的调用失败,我们能做什么?好吧,我们可以自动重试。队列系统非常擅长这一点。它们通常使用称为存储和转发的模式。它们在将消息转发给目的主机之前在本地存储消息。如果目的主机处于脱机状态,则队列系统将重试发送邮件。MSMQ是这种队列系统的一个例子。
但是这种变化将对你的系统设计产生重大影响。你正在从请求/响应模型转移到触发并丢掉。由于你不再等待响应,因此你需要更改系统中的用户目的。你不能只使用队列发送替换每个Web服务调用。
结论
你可能会说网络现在更可靠,但事情发生了,硬件和软件可能会出现故障:电源,路由器,更新或补丁失败,无线信号弱,网络拥塞,啮齿动物或鲨鱼破坏海底光缆。还有黑客的DDOS攻击,也可以破坏物理设备。
这是否意味着你需要删除当前的技术堆栈并使用消息传递系统?可能不会!你需要权衡失败的风险与你需要进行的投资。可以通过投资基础架构和软件来最小化失败的可能性。在许多情况下,失败意味着另一种选择。在设计分布式系统时,确实需要考虑失败的可能性。
2.延迟是零
问题:网络调用不是实时的。
内存调用和互联网调用之间存在七个数量级的差异。你的应用程序应该是网络感知。这意味着你应该清楚地将本地调用与远程调用分开。让我们看看在代码库中看到的一个例子:
没有进一步检查的话,代码看起来不错。但是,有两个远程调用。第2行进行一次调用以获取文档摘要列表。在第5行,还有另一个调用,它检索有关每个文档的更多信息。这是一个经典的Select n+1问题。为了解决网络延迟问题,你应该在一次调用中返回所有必需的数据。一般的建议是本地调用可以细粒度,但远程调用应该更粗粒度。这就是为什么分布式对象和“网络透明度”的想法过时。但是,即使每个人都同意分布式对象是一个坏主意,有些人仍然认为延迟加载总是一个好主意:
你不希望property属性getter进行网络调用。但是,每个“.”在上面的代码中调用实际上都可以触发数据库。
解决方案:请确保恢复可能需要的所有数据
如果你进行远程调用,请确保恢复可能需要的所有数据。
将数据靠近客户端
另一种可能的解决方案是将数据靠近客户端。如果你正在使用云,请根据客户的位置仔细选择可用区。缓存还可以帮助最小化网络调用的数量。对于静态内容,内容交付网络(CDN)是另一个不错的选择。
数据流转化
删除远程调用的另一个选项是数据流转化。我们可以使用Pub/Sub并在本地存储数据,而不是查询其他服务。这样,我们就可以在需要时获取数据。当然,这会带来一些复杂性,但它不失为一个好的选择。
结论
虽然延迟可能不是局域网中的问题,但当在广域网WAN或互联网时,你会注意到延迟。这就是为什么将网络调用与内存中的调用明确分开是很重要的。在采用微服务架构模式时,你应该牢记这一点。不应该只使用远程调用替换本地调用。这可能会使你的系统变成分布式泥潭。
3.带宽是无限的
问题:带宽有限。
带宽是网络在一段时间内发送数据的容量。到目前为止,我还没有发现它是一个问题,但我可以看到为什么它在某些条件下可能是一个问题。虽然带宽随着时间的推移而有所改善,但我们发送的数据量也有所增加。与通过网络传递简单DTO的应用相比,视频流或VoIP需要更多带宽。带宽对于移动应用程序来说更为重要,因此开发人员在设计后端API时需要考虑它。
错误地使用ORM也会造成伤害。我见过开发人员在查询中过早调用.ToList()的示例,因此在内存中加载整个表。
解决方案:领域驱动的设计模式(Domain-Driven Design Patterns)
那么我们怎样才能确保我们不会带来太多数据呢?领域驱动的设计模式可以帮助:
首先,你不应该谋求单一的企业级域模型。你应该将域划分为边界上下文(bounded contexts)。
要避免边界上下文中的大的、复杂的对象图(object graphs),可以使用聚合模式。聚合确保一致性并定义事务边界。
命令查询职责分离模式(CQRS)
我们有时会加载复杂的对象图,因为我们需要在屏幕上显示它的一部分。如果我们在很多地方这样做,我们最终会得到一个庞大而复杂的模型,对于编写和阅读来说都是次优的。另一种方法可以是使用命令查询职责分离模式。这意味着将域模型分为两部分:
写模型将确保不变保持为真且数据一致。由于编写模型不关心视图问题,因此可以保持较小且集中。
读取模型针对视图问题进行优化,因此我们可以检索特定视图所需的所有数据(例如,我们的应用程序中的screen)。
结论
延迟是零,以及带宽是无限的,二者一张一弛。你要传输更多数据,则要最大限度地减少网络往返次数。或者,你要传输较少的数据以最小化带宽使用。你需要平衡这两种需求,并找到通过网络发送的正确数据量。
虽然你可能不会经常遇到带宽限制,但考虑传输的数据非常重要。更少的数据更容易理解。数据越少意味着耦合越少。因此,只传输你可能需要的数据。
4.网络安全
问题:网络不安全。
分布式系统中有很多漏洞。比如:你正在使用HTTPS,除非与不支持它的第三方遗留系统进行通信;或你正在查看自己的代码,寻找安全问题,但正在使用可能存在风险的开源库。OpenSSL漏洞可能让黑客窃取受SSL/TLS保护的数据;抑或,Apache Struts中的一个错误允许攻击者在服务器上执行代码。即使你正在抵御所有这些,仍然存在人为因素。不够专业的DBA,可能没做数据库备份。今天的攻击者掌握着大量的计算技能,拥有时间和耐心。所以问题不在于他们是否会攻击你的系统,而是什么时候。
解决方案:深度防御
你应该使用分层方法来保护系统。需要在网络,基础架构和应用程序级别进行不同的安全检查。
安全心态
在设计系统时要牢记安全性。十大漏洞列表在过去5年中没有发生太大变化。你应遵循安全软件设计的最佳实践,并检查常见安全漏洞的代码。应该定期搜索第三方库以查找新漏洞。常见漏洞和漏洞列表可以提供帮助。
威胁建模
威胁建模是一种识别系统中可能存在的安全威胁的系统方法。首先确定系统中的所有资产(数据库中的用户数据,文件等)以及如何访问它们。之后,你可以识别可能的攻击并开始执行它们。建议阅读高级API安全性的第2章,以便更好地概述威胁建模。
结论
唯一安全的系统是关闭电源的系统,不连接到任何网络。事实是安全是艰难而且费用不菲。分布式系统中有许多组件,每个组件都是黑客的可能目标。企业需要平衡攻击的风险和概率与实施预防机制的成本。
攻击者很多耐心和计算能力。我们可以通过使用威胁建模来防止某些类型的攻击,但我们无法保证100%的安全性。 因此,向业务部门明确表示这一点,共同决定投资安全性的程度,并制定安全漏洞何时发生的计划。
5.拓扑不会改变
问题:网络拓扑不断变化。
网络拓扑始终在变化。有时它会因意外原因而发生变化。当你的应用服务器出现故障并需要更换时。在新服务器上添加新进程。如今,随着云和容器的增加,这一点更加明显。弹性扩展:根据工作负载添加或删除服务器的能力,需要一定程度的网络灵活性。
解决方案:规划网络的物理结构
需要做的第一件事是抽象网络的物理结构。有几种方法可以做到这一点:
- 停止硬编码IP(hardcoding IP):你应该更喜欢使用主机名。通过使用URI,我们依靠DNS将主机名解析为IP。
- 当DNS不够时(例如,当你需要映射IP和端口时),请使用发现服务。
- Service Bus框架还可以提供位置透明性。
对待服务器的正确思维
正确的思维模式:任何服务器都可能出现故障(从而改变拓扑结构),因此你应该尽可能地自动化。
测试
最后一条建议是测试你的假设。停止服务或关闭服务器,看看你的系统是否仍在运行。像Netflix的Chaos Monkey这样的工具可以通过随机关闭生产环境中的VM或容器来实现这一目标。不断试错,让你更有动力构建一个可以处理拓扑变化的、更具弹性的系统。
结论
十年前,大多数拓扑结构并没有经常改变。但是当它发生时,它可能发生在生产中并会带来一些停机时间。如今,随着云和容器的增加,很难忽视这种谬误。你需要为失败做好准备,并进行测试。不要等到它在生产环境中发生!
6.“无所不能”的管理员
问题:没有管理员知道一切。
当然,没有一个人知道一切。这是一个问题吗?但是,当出现问题时,你需要修复它。因为很多人接触了应用程序,知道如何解决问题的人可能不在那里。
有很多事情可能会出错。一个例子是配置。今天的应用程序在多个地方存储配置:配置文件,环境变量,数据库,命令行参数。没有人知道每个可能的配置值的影响是什么。
另一件可能出错的事情是系统升级。分布式应用程序有许多移动部件,你需要确保它们是同步的。例如,需要确保当前版本的代码适用于当前版本的数据库。如今,人们关注DevOps和持续交付。但支持零停机部署并非易事。
但是,至少这些东西都在你的控制之下。许多应用程序与第三方系统交互。这意味着,如果它们失效,你可以做的事情就不多了。因此,即使你的系统有一名管理员,你仍然无法控制第三方系统。
解决方案:每个人都应对过程负责
这意味着从一开始就涉及运维人员或系统管理员。理想情况下,他们将成为团队的一员。尽早让系统管理员了解你的进度可以帮助你发现限制因素。例如,生产环境可能具有与开发环境不同的配置,安全限制,防火墙规则或可用端口。
记录和监控
系统管理员应该拥有用于错误报告和管理问题的正确工具。你应该从一开始就考虑监控。分布式系统应具有集中式日志。访问十个不同服务器上的日志以调查问题是糟糕的方法。
解耦
你应该在系统升级期间争取最少的停机时间。这意味着你应该能够独立升级系统的不同部分。通过使组件向后兼容,你可以在不同时间更新服务器和客户端。
通过在组件之间放置队列,你可以暂时将它们分离。这意味着,例如,即使后端关闭,Web服务器仍然可以接受请求。
隔离第三方依赖关系
你应该以不同于你拥有的组件的方式处理控制之外的系统。这意味着使你的系统更能适应第三方故障。你可以通过引入抽象层来减少外部依赖的影响。这意味着当第三方系统出现故障时,将检查更少的地方来查找错误。
结论
要解决这个谬论,你需要使系统易于管理。DevOps,日志记录和监控可以提供帮助。还需要考虑系统的升级过程。如果升级需要数小时的停机时间,则无法部署每个sprint。没有“无所不能”的管理员,所以每个人都应该对发布过程负责。
7.传输成本为零
问题:传输成本不是零。
这个谬误与第二个谬误有关,即延迟为零。通过网络传输内容在时间和资源上都有代价。如果第二个谬误讨论了时间方面,那么谬误7就会解决资源消耗问题。
这种谬误有两个不同的方面:
- 网络基础设施的成本
网络基础设施需要付出代价。服务器,SAN,网络交换机,负载平衡以及负责此设备的人员,所有这些都需要花钱。如果你的系统是在内部部署的,那么需要预先支付这个价格。如果正在使用云,那么只需为你使用的内容付费,但仍然需要付费。
- 序列化/反序列化(Serialization/Deserialization)的成本
这种谬误的第二个方面是在传输级别和应用程序级别之间传输数据的成本。序列化和反序列化会消耗CPU时间,因此需要成本。如果你的应用程序是内部部署的,那么如果你不主动监视资源消耗,则会产生隐性成本。但是,如果你的应用程序部署在云端,那么这笔费用就会非常明显,因为你需要为使用的资源付费。
解决方案
关于基础设施的成本,你无能为力。只能确保尽可能高效地使用它。SOAP或XML比JSON更昂贵。JSON比像Google的Protocol Buffers这样的二进制协议更昂贵。根据系统的类型,这可能或多或少是重要的。例如,对于与视频流或VoIP有关的应用,传输成本更为重要。
结论
你应该注意传输成本以及应用程序正在执行的序列化和反序列化程度。这并不意味着你应该优化,除非需要它。你应该对资源消耗进行基准测试和监控,并确定传输成本是否对你有用。
8.网络是同构的
问题:网络不是同构的。
同构网络是使用类似配置和相同通信协议的计算机网络。拥有类似配置的计算机是一项艰巨的任务。例如,你几乎无法控制哪些移动设备可以连接到你的应用。这就是为什么重点关注标准协议。
解决方案:你应该选择标准格式以避免供应商锁定。这可能意味着XML,JSON或协议缓冲区。有很多选择可供选择。
结论
需要确保系统的组件可以相互通信。使用专有协议会损害应用程序的互操作性。
设计分布式系统很难
这些谬论发表于20多年前。但他们今天仍然深刻影响着IT基础设施的发展。我们必须接受这些事实:网络不可靠,不安全并且需要花钱,带宽有限,网络的拓扑结构将发生变化,其组件的配置方式不同。意识到这些限制,将有助于我们设计更好的分布式系统。