如果这个博客有一个统一的主题,那就是 分布式系统 充满 了 权衡取舍 。 具体来说,对于分布式消息传递, 您不能有 exactly-once delivery 。然而,消息传递的权衡并不仅限于交付语义。我想谈谈我的意思,并解释为什么许多开发人员在构建分布式应用程序时经常有错误的心态。
自然的趋势是构建分布式系统,就好像它们根本不是分布式的一样——假设数据一致性、可靠的消息传递和可预测性。这更容易推理,但它也公然误导。
消息传递(以及一般的分布式系统)中唯一可以保证的是,迟早,您的保证会失效。 如果你认为这些保证是公理化的,那么建立在它们之上的一切都会变得不可靠。根据情况的不同,这可能会从轻微烦人到完全灾难性的不等。
我最近看到了 Apcera 首席执行官 Derek Collison 关于这个话题的 评论 ,引起了我的共鸣:
在确实要求某种形式保证的系统上,最好查看保证真正用完的级别。尤其是关于持久性、exactly once 交付语义等。我职业生涯的大部分时间都花在了设计和构建具有这些保证的消息传递系统上,并反过来开发了许多利用其中一些特性的系统。对我来说,我发现依赖这些保证在分布式系统设计中是一个糟糕的模式……
您应该知道您的系统在达到临界点时的行为,但不太明显的是,提供这些类型的强保证通常非常昂贵。我们愿意支付什么价格,我们的保证保持在什么水平,当它们放弃时会发生什么?从这个意义上说,“保证”实际上与 SLA 没有什么不同,但更强的保证允许更强的假设。
这听起来很模糊,让我们看一个具体的例子。对于消息传递,我们通常关心传递的可靠性。在一个完美的世界中,消息传递将得到保证且恰好一次。当然,我已经详细讨论了为什么这是不可能的,所以让我们把自己锚定在现实中。我们可以看看 TCP/IP 是如何工作的。
IP 是一种不可靠的传输系统,它运行在不可靠的网络基础设施上。数据包可以按顺序传送、乱序传送或根本不传送。没有确认,因此发件人无法知道他们发送的内容是否已收到。 TCP 通过有效地使传输有状态并添加控制层来建立在 IP 之上。通过增加复杂性和性能成本,我们在不可靠的堆栈上实现了可靠的交付。
这里的关键要点是我们从一些原始的东西开始,比如将位从 A 点移动到 B 点,并在抽象层上构建更强大的保证。这些抽象几乎总是有代价的,无论是否有形,这就是为什么将成本推高到上面的层很重要。如果不是每个用例都需要可靠的交付,为什么要把成本强加给每个人?
恰好一次交付是分布式消息传递的圣杯,而保证交付是独角兽。具有讽刺意味的是,即使它们可以实现,您也可能 不想要它们 。这些类型的强有力的保证需要 昂贵的基础设施 ,这些基础设施执行昂贵的协调,需要昂贵的管理。但是,所有这些昂贵的东西最终能给你带来什么?
一个关键问题是消息传递和消息处理之间存在巨大差异。当然,TCP 或多或少可以确保您的数据包是否已送达,但实际上这有什么用呢?发件人如何知道其消息已成功处理或收件人已完成其需要做的事情?真正知道的唯一方法是接收方发送业务级确认。低级传输协议不知道应用程序语义,所以唯一的方法就是 向上 。如果我们假设任何保证最终都会失效,我们就必须 在业务层面 考虑到这一点。引用一篇 相关文章的 话,“如果可靠性在业务层面很重要,那么就在业务层面做到这一点。”重要的是不要将传输协议与业务交易协议混为一谈。
这就是为什么像 Akka 这样的系统不提供保证交付的概念——因为“保证交付”到底是什么意思?这是否意味着消息已交给传输层?这是否意味着远程机器收到了消息?这是否意味着邮件已在收件人的邮箱中排队?这是否意味着收件人已开始处理它?这是否意味着收件人已完成处理?这些东西中的每一个都有一组非常不同的要求、约束和成本。另外,消息被“处理”意味着什么?这取决于业务环境。因此,底层基础设施做出这些决定通常没有意义,因为这些决定通常会显着影响上面的层。
通过仅提供基本保证,那些不需要更严格保证的用例无需支付实施成本;总是可以在基本保证之上添加更严格的保证,但不可能追溯主动删除保证以获得更多性能。
分布式计算本质上是异步的,而网络本质上是不可靠的,因此最好接受这种异步性而不是建立在有漏洞的抽象之上。 与其隐藏这些不便,不如将它们明确化并迫使用户围绕它们进行设计。 您最终得到的是一个更健壮、更可靠且通常性能更高的系统。这种权衡在 Huang 等人的论文“ 复制消息系统中的精确一次语义 ”中得到了强调。在研究exactly-once delivery的问题时:
因此,以服务器为中心的算法无法实现恰好一次语义。相反,我们将努力实现更弱的正确性概念。
通过放宽我们的要求,我们最终得到了一个性能开销和复杂性较低的解决方案。为什么要费心去追求不可能的事情?您为可能不如您想象的可靠且性能不佳的东西支付了巨额溢价。在许多情况下,最好让钟摆向另一个方向摆动。
网络不可靠,这意味着消息传递永远无法真正得到保证——它只能 尽力而为 。 两将问题 表明,事实证明,两个远程进程 不可能 安全地就一个决定达成一致。同样, FLP 不可能的结果 表明,在异步环境中,可靠的故障检测是不可能的。也就是说,没有办法判断进程是否已崩溃或只是需要很长时间才能响应。因此,如果一个进程有可能崩溃,那么一组进程就 不可能 达成一致。
如果不能保证消息传递并且不可能达成共识,那么消息排序真的那么重要吗?有些用例可能实际上需要它,但我怀疑,这通常是人为的约束。网络不可靠、流程有问题以及分布式通信是异步的这一事实使得可靠、有序的交付出奇地昂贵。但是TCP不就解决了这个问题吗?在传输级别,是的,但这只能让您达到我一直试图展示的程度。
因此,您使用 TCP 并通过单个线程处理消息。大多数时候, 它只是有效 。但是在重负载下会发生什么?消息传递失败时会发生什么?当您需要扩展时会发生什么?如果您正在对消息进行排队,或者您有一个死信队列,或者您有网络分区或崩溃恢复模型,您可能会遇到重复、丢弃或乱序的消息。即使基础架构提供有序交付,这些问题也可能会在应用程序级别表现出来。
如果你是分布式的,忘记排序并开始考虑可交换性。忘记保证交付并开始考虑幂等性。 停止考虑消息传递平台,开始考虑消息传递模式和业务语义。与完全有序和“有保证”的系统相比,可交换和幂等的模式将远不那么脆弱且更有效。这就是 CRDT 在分布式空间中变得越来越流行的原因。 当您无法编写假定消息完全到达的代码时,切勿编写假定消息将按顺序到达的代码。
最后,仔细考虑业务案例以及您的实际需求。您能否在不依赖昂贵且有漏洞的抽象或欺骗性保证的情况下满足他们?如果不能,当这些保证失效时会发生什么?这与了解不满足 SLA 时发生的情况非常相似。性能和复杂性的权衡是否值得?运营和业务管理费用如何?以我的经验,直面分布式系统的复杂性要比对它们视而不见更好。迟早,他们会露出丑陋的脑袋。