005
用中文读科技新闻
旨在帮你快速挑选感兴趣的文章来读。

过去十年,我们在分布式系统领域取得了巨大进步,但编程方式却鲜有根本性改进。 虽然我们有时可以抽象出分布(Spark、Redis 等),但开发者仍然在并发、容错和版本控制等方面面临挑战。
许多人(和初创公司)都在致力于解决这些问题。但几乎所有人都专注于工具,以帮助分析用经典(顺序)编程语言编写的分布式系统。像 Jepsen 和 Antithesis 这样的工具已经推进了验证正确性和容错性的先进水平,但工具无法与原生呈现基本概念的编程模型相提并论。我们已经在 Rust 中看到了这一点,它提供的内存安全保证远比使用 AddressSanitizer 的 C++ 丰富。
如果你在网上搜索,会发现有很多用于编写分布式代码的框架。在这篇博文中,我将论证它们仅仅是在三种固定的底层范式上提供的创可贴和糖衣:外部分布、静态位置和任意位置。我们仍然缺少一个原生于分布式系统的编程模型。我们将探讨这些范式,然后反思真正分布式编程模型所缺少的要素。
外部分布 架构是绝大多数"分布式"系统的样子。在这种模型中,软件被编写为针对具有顺序语义的状态管理系统运行的顺序逻辑:
-
• 使用分布式数据库的无状态服务(Aurora DSQL、Cockroach)
-
• 使用流言 CRDT 状态的服务(Ditto、ElectricSQL、Redis Enterprise)¹
- • ¹ 这可能令人惊讶。CRDT 通常被宣传为所有分布式系统的灵丹妙药,但另一种观点是它们只是加速了分布式事务。在 CRDT 上运行的软件仍然是顺序的。
-
• 工作流和步骤函数
这些架构很容易编写软件,因为底层分布对开发者是不可见的²。
* ² 至少这是设想。序列化通常不是默认设置(快照隔离是),因此有时会暴露并发错误。
虽然这种架构产生了一个分布式系统,但我们没有一个分布式编程模型。
几乎不需要考虑容错或并发错误(除了确保选择正确的 CRDT 一致性级别)。因此,开发者选择这种方案的原因显而易见,因为它将分布式混乱隐藏在干净的顺序语义之下。但这带来了明显的代价:性能和可扩展性。
将所有内容序列化相当于模拟一个非分布式系统,但带有昂贵的协调协议。数据库构成了系统中的单点故障;你要么希望 us-east-1 不会宕机,要么切换到像 Cockroach 这样的多写入器系统,但后者会带来自身的性能影响。许多应用程序的规模足够小,可以容忍这一点,但你不会想用这种方式实现一个计数器。
静态位置 架构是编写分布式代码的经典方式。你组合多个单元,每个单元都编写为使用异步网络调用与其他机器通信的本地(单机)代码:
-
• 使用 API 调用通信的服务,可能使用 async / await(gRPC、REST)
-
• Actor(Akka、Ray、Orleans)
-
• 轮询和推送共享发布/订阅的服务(Kafka)
这些架构为我们提供了完整的、低级别的控制。我们编写的是带有网络调用的一堆顺序的单机软件。这对于性能和容错性非常有用,因为我们可以控制在何处以及何时运行什么。
但网络单元之间的边界是僵化且不透明的。开发者必须对如何分解应用程序做出单向决策。这些决策对正确性有很大影响;重试和消息排序由发送者控制,接收者未知。此外,语言和工具对单元的组合方式了解有限。跳转到定义通常不可用,并且服务之间的序列化不匹配很容易悄然出现。
最重要的是,这种分布式系统方法从根本上消除了语义协同定位和模块化。在顺序代码中,一个接一个发生的事情在文本上一个接一个地放置,函数调用封装了整个算法。但是,使用静态位置架构,开发者被迫在机器边界而不是语义边界上模块化代码。在这些架构中,根本无法将分布式算法封装为单个统一的语义单元。
尽管静态位置架构为开发者提供了对其系统的大多数低级别控制,但在实践中,如果没有分布式系统专业知识,它们很难稳健地实现。实现和执行之间存在根本性的不匹配:静态位置软件被编写为单机代码,但系统的正确性需要将整个机器集群作为一个整体进行推理。构建此类系统的团队通常生活在对并发错误和故障的恐惧中,导致大量遗留代码过于关键而无法触及。
任意位置 架构是大多数"现代"分布式系统方法的基础。这些架构通过允许我们编写代码(就像它在单台机器上运行一样)来简化分布式系统,但在运行时,软件会在多台机器上动态执行³。
* ³ Actor 框架并不真正算数,即使它们支持迁移,因为开发者仍然必须显式定义 Actor 的边界并指定消息传递发生的位置。
-
• 分布式 SQL 引擎
-
• MapReduce 框架(Hadoop、Spark)
-
• 流处理(Flink、Spark Streaming、Storm)
-
• 持久执行(Temporal、DBOS、Azure Durable Functions)
这些架构优雅地处理了协同定位问题,因为语言/API 中没有显式的网络边界来分割你的代码。但这种简单性带来了巨大的代价:控制。通过让运行时决定如何分布代码,我们失去了对应用程序如何扩展、故障域位于何处以及何时通过网络发送数据做出决策的能力。
就像外部分布模型一样,任意位置架构通常会带来性能成本。持久执行系统通常会在每个步骤之间将其状态快照到持久存储⁴。
* ⁴ 当步骤是纯粹的确定性函数时,有一些优化。
流处理系统可能会动态持久化数据,并且可以自由地在步骤之间引入异步性。SQL 用户受查询优化器的支配,他们最多只能给出关于分布决策的"提示"。
为了性能和正确性,我们通常需要对单个逻辑的放置进行低级别控制。考虑实现两阶段提交。此协议对广播提议的领导者和确认提议的工作者具有显式的、不对称的角色。为了正确实现这样的协议,我们需要将特定的逻辑显式分配给这些角色,因为必须在单个领导者上确定仲裁,并且每个工作者必须原子地决定接受或拒绝提议。在不引入不必要的网络和协调开销的情况下,根本不可能在任意位置架构中实现这样的协议。
强制性的 LLM 部分
如果你一直在关注"智能体"LLM 领域,你可能会想:"在我的软件由 LLM 编写的世界中,这些问题是否相关?" 如果静态位置模型足够丰富以表达所有分布式系统,那么即使编程起来很痛苦,又有什么关系呢!
我认为 LLM 实际上是我们需要新编程模型的一个很好的论据。这些模型在上下文相关信息分散在大量文本中的情况下,会遇到著名的困难⁵。
* ⁵ 参见"大海捞针"测试;对分布式系统进行推理甚至更难。
当语义相关信息协同定位时,LLM 表现最佳。
静态位置模型迫使我们将语义连接的分布式逻辑分散到多个模块中。LLM 在单台机器上的正确性方面还不够出色;将多个单机程序组合在一起以正确协同工作,远远超出了它们的能力。此外,LLM 顺序地做出决策;将分布式逻辑分散到多个联网模块中,对 AI 模型的结构本身提出了固有的挑战。
LLM 在保留"语义局部性"的编程模型中会表现得更好。在一个假设的编程模型中,跨越多台机器的代码可以协同定位,这个问题就变得微不足道了。分布式算法的所有相关逻辑都将彼此相邻,并且 LLM 可以以直线方式生成分布式逻辑。
另一个难题是正确性。LLM 会犯错误,我们最好的选择是将它们与可以自动找到错误的工具结合起来⁶。
* ⁶ Lean 是这方面的一个很好的例子。包括 Google 和 Deepseek 在内的团队已经使用它一段时间了。
顺序模型无法推断分布式执行可能导致的问题。但是,一个足够丰富的分布式编程模型可以揭示由网络延迟和故障引起的问题(想想分布式系统的借用检查器)。
我们可以从这些系统中获得什么?
尽管我们讨论的编程模型各有局限性,但它们也展示了分布式系统的原生编程模型应该支持的理想特性。我们能从每个模型中学到什么?
我将跳过外部分布,正如我们讨论的那样,它并不完全是分布式的。对于可以容忍此模型的性能和语义限制的应用程序,这是可行的方法。但是,对于通用的分布式编程模型,我们不能将网络和并发对开发者隐藏起来。
静态位置模型似乎是正确的起点,因为它至少能够表达我们可能想要实现的所有类型的分布式系统,即使编程模型在推理分布方面对我们没有太大帮助。我们缺少任意位置模型提供的两件事:
-
• 在单个函数中编写跨越多台机器的逻辑
-
• 揭示分布式行为(如消息重新排序、重试和跨网络边界的序列化格式)的语义信息
这些要点中的每一个都有一个对偶,我们不想放弃:
-
• 对机器上逻辑的放置进行显式控制,并具有执行本地原子计算的能力
-
• 提供丰富的容错保证和网络语义,而不会让语言将我们锁定在全球协调和恢复协议中。
是时候推出一个原生编程模型了------如果你愿意,可以称之为分布式系统的 Rust------它能够解决所有这些问题。