脆弱、狭窄、滞后的异步不匹配管道会扼杀生产力

2020-05-18 00:04:45

我最近一直在想的是,当我在任何一种分布式系统上工作时,包括像Web应用程序这样简单的前端和后端代码,我可能有80%以上的时间花在了如果不是分布式的情况下我就不需要做的事情上。对于为什么我认为这类编程需要如此多的工作,我提出了以下描述:一切都是脆弱的、狭窄的、滞后的、异步的、不匹配的、不可信的管道。我认为每一个在网络系统上工作的程序员都遇到过这些问题,这只是我在一个地方连贯地描述所有这些问题的努力。我希望能促使你同时考虑所有不同的麻烦,并想一想,如果你必须/不需要处理这些事情,你的工作会变得多么困难/容易。我认为这就是为什么像Twitter这样的网络公司的人均工程师生产率似乎比游戏公司或SpaceX等其他公司要低得多,尽管这个谜团还有其他方面。虽然分布式系统的部分困难是物理上固有的,但我认为有很多想法可以使问题的每个部分变得更容易,其中许多已经是常用的,我将尝试提到其中的许多想法。我希望我们作为程序员不断开发更多这样的技术,特别是简化问题的通用实现。就像序列化库减少了对手工编写的解析器/编写器的需求一样,我认为通过实现通用的解决方案可以节省大量的开发人员时间,在这些解决方案中,我们目前正费劲地重新实现通用模式。我还认为,所有这些成本意味着,如果没有必要,您应该真正努力避免使您的系统分布式。

我将详细介绍每一部分,但简要地说,每当我们介绍网络连接时,我们通常都必须处理以下内容:

脆弱:网络连接或另一端可能出现硬件故障,这些故障有不同的含义,但两者都表现为超时。一切都需要处理失败。

窄:带宽有限,因此我们需要仔细设计协议,以便只发送他们需要的内容。

异步:特别是使用>;2输入源(UI计数)时,可能会发生各种竞争和边缘情况,需要考虑和处理。

不匹配:通常不可能自动升级所有系统,因此您需要处理使用不同协议版本的不同目的。

不可信:如果您不希望所有内容都因一次故障而关闭,则需要防御无效输入和不堪重负的情况。有时,您还需要防御实际的攻击者。

管道:所有内容都打包为字节,因此您需要能够(反)序列化您的数据。

当编写在一台计算机上运行的程序时,所有这些事情基本上都可以避免,也就是说,除非您最终优化了性能,并意识到您的计算机实际上是一个分布式的核心系统,并且其中一些核心又回来了。一些领域设法避免了其中的一些问题,但我在网络应用、自动驾驶汽车、文本编辑器和高性能系统上工作时经历过这些问题的子集,它们无处不在。

这甚至不是所有的问题,只是关于网络的事情。在各种瓶颈通常如何需要复杂的缓存层次结构(需要与底层数据存储保持同步)等问题上也花费了大量的精力。

避免这一切的一种方法就是不编写分布式系统。有很多情况可以做到这一点,我认为比一些人更努力地尝试将所有事情打包到一个过程中是值得的。然而,超过一定的可靠性或规模,物理意味着您将不得不使用多台机器(除非您想走大型机路线)。

随着您连接机器或提高可靠性目标,当一个部件崩溃时只使所有东西崩溃的策略(多线程/多核系统所做的)变得越来越不可行。硬件将出现故障,无线连接将中断,整个数据中心的电力或网络将被松鼠切断。一些域名,如互联网不稳定的客户,也不可避免地会导致频繁的连接故障。

在实践中,您需要编写代码来处理失败案例,并仔细考虑它们是什么以及要做什么。当仅仅注意到故障会丢失重要数据,并且您需要实现数据存储或传输的冗余时,情况会变得更糟。更糟糕的是,另一台机器发生故障和网络连接中断都变得可见,就像一些预期的网络数据包在“太长”之后没有到达一样,这不仅带来了延迟,而且带来了可能导致分裂大脑问题的模棱两可。通常,像TCP这样的东西会为您实现它,但有时您必须实现自己的心跳,以定期检查另一个系统是否仍在运行。

试图简化这一点的尝试包括例外、TCP、合并协议和现成的冗余数据库,但没有任何解决方案可以消除所有地方的问题。我最喜欢的尝试之一是Erlang的流程链接、监视和监督,它提供了一种理念,试图将所有类型的故障合并成一个更容易处理的一般情况。

网络带宽通常是有限的,特别是在消费者或蜂窝互联网上。这似乎不是一个经常的限制,因为您很少达到带宽限制,但这是因为有限的带宽在您做的每件事中都根深蒂固。无论何时设计分布式系统,您都需要提出一种通信协议,该协议按照需要的顺序进行通信,而不是按照数据总大小的顺序进行通信。

在多线程程序中,您可能只传递一个指向千兆字节的不可变或锁定数据的指针,让线程读取它想要的内容,而不去想任何事情。在分布式系统中,传递代表数据库的整个内存是不可想象的,您需要花费时间实现其他方法。

虽然多核系统实际上是一种特定的分布式系统,并且它们在幕后使用协议来仅传输必要的数据,但是涉及比大多数网络可行的更多的广播和往返。实际上,我认为尝试将用于使多核计算机无缝连接到分布式系统的技术应用于分布式系统是一种很好的方式,可以想出可能比其他方式设计的更通用的整洁解决方案。同样,一旦您真正开始努力优化系统,您会注意到计算机内部的带宽也会成为一个限制因素。

处理低带宽通常涉及每次查询或修改共享数据结构的消息类型,并决定何时传送更多数据以便本地交互更快,或者何时传送更少数据以避免糟糕的带宽情况。它通常更进一步到各种类型的复制状态机,其中每个对等点基于复制的更改流更新模型,因为在每次更新之后发送新模型将会占用太多带宽。这方面的例子包括交换提要的RTS游戏。然而,维护每个对等点如何更新其状态以避免去同步的确定性和一致性可能是棘手的,特别是在不同对等点具有不同的语言或软件版本的情况下。您还经常最终实现一个单独的协议来流式传输完整的快照,因为当连接不可行时,从一开始重放事件是不可行的。

试图简化这一点的方法包括RPC库,它使为不同的查询和更新发送大量不同类型的消息变得更容易,而不是提供数据结构、缓存库和压缩。很酷但不太常用的系统包括像Replicant这样的系统,它们可以确保同步状态机代码并更新许多设备上的流,从而使复制状态机变得更容易、更轻松。

一次网络往返不可能是有问题的延迟,或者您需要更好的网络硬件,或者需要解决不同的问题。困难来自于避免以需要过多网络往返的方式实施您的解决方案。这可能导致需要实现在服务器上执行一系列操作的特殊组合消息,而不仅仅是提供较小的原始消息。

具有特别大延迟的Web有很多这种类型的问题,比如加载HTML后只有字体/图像URL,或者REST API需要多个链式调用才能获得下一个所需的ID。已经为这些问题构建了很多东西,比如资源内联、HTTP/2服务器推送和GraphQL。

一个比较酷的通用解决方案是Capn‘n Promise流水线和其他系统,这些系统实质上涉及将一系列步骤传送到另一端(如SQL)执行。这些系统本质上发送在服务器上执行的有限类型的程序。不幸的是,您经常遇到所用语言的限制,比如您无法在没有往返的情况下将Capn‘n Proto结果加1,然后再将其传递给新的呼叫。但是,如果您使您的语言过于强大,您可能会遇到代码问题,因为您要发布的代码会使服务器过载或过大。如果您控制两端,那么为您的用例添加多步骤消息非常容易,但是如果另一端是公司的第三方API,或者甚至是大公司的不同团队所拥有的,那么在这些情况下,他们往往不想在他们的服务器上运行您的程序。我认为在发送代码段的新方法方面有更多的探索空间,同时重用发送的代码以节省带宽并限制其造成破坏的可能性。

另一种可以在数据中心工作的解决方案是使用更好的网络。你可以得到延迟为2us,带宽为100Gbps或更高的网卡,但基本上只有HPC、模拟和金融使用它们。然而,如果您的方法需要O(N)次往返,这些只会减少常量因素,并不能节省您的时间。

一旦您有2+个未同步的事件源,您就会开始担心竞争情况。这可以是多个服务器,也可以只是一个既有用户输入又有到服务器的通道的Web应用程序。总是有一些不常见的排序,比如用户在加载下一个页面之前再次单击“提交”按钮。有时您很幸运,系统的设计意味着这很好,而另一些时候则不是这样,您要么修复它来处理这种情况,要么从被开两次账单的客户那里获得错误报告。异步程度越高,您需要考虑的情况就越多,或者必须使用一种优雅的设计来解决更多的情况,这种设计可以避免糟糕的状态。

根据您的语言/框架的不同,异步还可能需要更改您通常编写代码的方式,从而使一切变得臃肿和丑陋。许多系统过去和现在都要求您在任何地方都使用回调,有时甚至不提供闭包,这会使您的代码变得非常混乱。许多语言在这方面做得更好,比如异步/等待或带有小堆栈的协程(如Go),或者只是使用线程和阻塞I/O。不幸的是,其中一些解决方案引入了函数颜色问题,因为引入异步需要在整个代码库中进行更改。

异步边缘情况是一个相当基本的问题,但是有很多可用的模式来解决不同类型的异步。例如,并发原语(如锁和障碍)、协议设计思想(如幂等)和更时髦的东西(如CRDT)。

通常,当您想要更改协议时,不可能自动升级分布式系统的每个组件。这是从必须全天候运行的服务器群集与将旧版本的网页加载到选项卡中的用户进行通信而运行的。这意味着在一段时间内,您的系统需要使用较新的协议版本,与只知道较旧协议的系统进行通信。这只是一个您需要解决的问题,有两大类常见的解决方案,它们有很多子类型:

通过维护两个实现或将旧处理程序映射到新处理程序,使新软件版本能够同时使用新的和新的协议版本,并与升级的对等点协商使用新版本。

使用免费提供一定程度兼容性的数据结构,然后仅以这些方式升级您的协议。例如,JSON对象中无法识别的字段通常会被忽略,因此可以在识别后用于新功能。迁移通常可以在不中断查询的情况下向数据库表添加新列。然后,您通常会不遗余力地迫使每个更改成为这种类型的兼容。

这两种情况的问题是,第一步通常以代码路径的形式积累技术债务,以处理一旦所有对等点都升级到协议更改之后就永远不会出现的情况(例如,缺少字段)。这通常需要分多个阶段推出,例如引入一个新字段作为可选,在所有地方推出新版本,将该字段更改为必填字段,现在所有客户端都发送该字段,然后再进行一次推出。当我想要更改多个系统使用的协议而又不想弄得一团糟时,我肯定花了很多时间来计划多阶段部署。

对这两种方法都有很多帮助,两种序列化系统都提供了大量兼容的升级路径,如Protobufs,以及用于反序列化/升级旧类型版本的各种模式。

您的数据不仅可能无法到达,而且您的系统可能会接收到可能会对其造成积极损害的数据。系统存在导致发送无效消息的错误,因此不仅在序列化级别,而且在业务逻辑级别,都需要仔细验证输入并返回错误。错误或新负载可能会导致系统发送消息的速度超过其处理速度,从而需要背压和限制。您甚至可能不得不防御那些积极尝试通过发送通常的交易对手永远不会发送的消息来破坏您的系统的攻击者,并智能地寻找边缘案例。

这里也有很多模式,包括速率限制、现场验证逻辑和带内置反压的通道。在安全方面,我们还有加密、证书和模糊等内容。在这里,我们在通用性方面也做得更好,因为我们已经减少了手动模式的流行程度,比如确保我们总是在SQL和HTML中转义内插字符串,而使用更通用的模式,比如?总是应用转义的查询参数和模板系统。

最后,也是最不重要的,所有内容都必须是字节流或字节包。这意味着您需要采用您的语言使其易于操作的漂亮数据结构,并将它们打包成与其内存中表示形式不同的形式,以便在网上发送。幸运的是,除了在极少数地方,简单的序列化/RPC库让这件事变得相当容易,尽管偶尔有些慢。有时,您还可以使用允许您从字节缓冲区中准确挑选出所需部分的方法,而无需将其转换为不同的表示形式,可能是通过将缓冲区指针强制转换为C结构指针(当这甚至接近于安全级别时),或者使用类似Cap‘n Proto的方法来生成访问器。

这可能是我花的时间最少的一次,但是我记得有一次我想要发送一个大型数据结构,但可用的序列化系统只能一次序列化所有数据结构,而不是逐个数据包传输,因为套接字可以接受它,而且我不想长时间阻塞我的服务器,这样会造成尾部延迟。我最终选择了不同的设计,但我也可以编写自定义代码,将我的数据结构分成块,一次发送一点。

我怀疑对这篇文章的许多回复都会是这样的:“如果你只是{做一些不是普遍适用的、耗时的或有自己的问题的事情,那么实际上{一些/所有这些问题}都是微不足道的,可能是我提到的一些事情,如果是这样的话,很可能是使用Erlang},而真正的问题是,其他人在编程方面很糟糕,不像过去的人们”。有很多事情是有帮助的,在了解好的解决方案,选择正确的解决方案,并有效地实施它们方面,有一个技能部分。然而,这些仍然是棘手的问题,人们不得不做出艰难的、真正的权衡,因为我们没有有效地解决它们。也许你会采取不同的权衡,但人们做出这些技术决策是出于真正的原因,我们应该努力降低成本,以及改善我们接受哪些成本的决策。

我做的大多数项目都不能使用Erlang,因为它们要么需要极低的延迟,要么需要与非Erlang生态系统的某些部分集成,要么计算过于密集(是的,我知道NIF)。这意味着只要从一个领域带来解决方案并在另一个领域实现它们,或者使它们更快,就有足够的机会提高生产率!我喜欢看到将Erlang的好处带到更多领域的努力。即使是Erlang也没有解决所有这些问题,我相信有一天可能会解决这些问题。

我认为解决这些问题的最大锤子之一就是从一开始就努力避免编写分布式系统。我这篇文章的目标之一是激励人们尝试开发更通用的解决方案,而不是重复实现特定的模式,但我的另一个目标是试图一次性将所有成本放在你面前,并说,你确定添加独立的联网系统真的会让你的工作变得更容易吗?有时候,分布式系统是不可避免的,比如如果您想要极高的可用性或计算能力,但在其他时候,这是完全可以避免的。要选择特定示例,请执行以下操作:

我认为,如果适合的话,人们应该更愿意尝试将性能敏感的代码作为(可能是多线程的)进程在一台机器上使用快速语言编写,而不是尝试在多台机器上分发速度较慢的实现。我承认这需要时间和精力来学习如何做和优化,但它会在一个更简单的系统中得到回报。特别是,我认为人们应该在可能的情况下更积极地尝试在一台非常大的计算机上使用多线程。我个人发现,在Rust方式下进行多线程编程比在可行的情况下与多进程并行化要容易得多。有些问题(如异步)是相似的,但其他问题(如串行化、延迟和带宽)在很大程度上消失了,除非性能级别比假设的分布式版本高得多。

我认为人们应该更愿意使用CFFI来绑定其他语言的库,而不是将它们放在单独的网络服务中(例如,针对我自己的库的用户,尽管我实际上不知道他们的限制是什么)

除了可用性和并行性之外,人们选择将事物拆分成单独的服务是有原因的。例如,无需与另一个团队(FAST CI)协调,即可使用不同的语言Isolation快速部署更新。我们应该构建更多不涉及独立系统的替代方案,比如通过沙箱而不是微服务(消除“狭窄”、“滞后”和“异步”)来使用自动更新、热重新加载的动态链接库的工具。我非常肯定至少存在一个通过网络推送的热重载dylib更新实例(我希望有链接!)。总线。

..