来源:https://www.shadaj.me/writing/distributed-programming-stalled
在过去十年中,我们见证了分布式系统在技术上的巨大进步,但编写分布式系统的方式却鲜有根本改进。尽管我们有时能将分布式细节抽象掉(例如 Spark、Redis 等),但开发者依然在并发、容错和版本管理等问题上遇到挑战。
很多人(以及初创公司)都在致力于这个领域,但几乎所有的工作焦点都集中在开发工具上,以帮助分析使用传统(顺序)编程语言编写的分布式系统。像 Jepsen 和 Antithesis 这样的工具,已经将验证正确性和容错能力的技术水平推向了新的高度,但这些工具无法与那些原生展现分布式系统基本概念的编程模型相匹配。我们已经从 Rust 看到了这一点,Rust 所提供的内存安全保证,远比 C++ 配合 AddressSanitizer 所能提供的丰富得多。
如果你上网查询,会发现有大量框架可供编写分布式代码。在这篇博客中,我将论证这些框架仅仅是在三个固定底层范式——外部分布、静态位置和任意位置——之上进行权宜之计和加糖处理。而我们依然缺失一个对分布式系统来说真正 原生 的编程模型。接下来,我们将逐一探讨这些范式,然后反思一个真正分布式编程模型所欠缺的关键要素。
外部分布
外部分布 架构是绝大多数“分布式”系统的典型表现。在这种模型中,软件以 顺序逻辑 编写,并在一个具有 顺序语义 的状态管理系统上运行:
- 无状态服务结合分布式数据库(Aurora DSQL、Cockroach)
- 利用传播式 CRDT 状态的服务(Ditto、ElectricSQL、Redis Enterprise)
这可能会让人感到意外。CRDT 经常被宣传为分布式系统的灵丹妙药,但另一种观点认为,它们仅仅是用来加速分布式事务。运行在 CRDT 之上的软件仍然是顺序执行的。[↩] - 工作流和步骤函数
这种架构编写软件非常简单,因为底层的分布式细节并未直接暴露给开发者222。至少这是理论上的情况。通常,默认并非串行化(而是采用快照隔离),因此并发错误有时会暴露出来。[↩]
尽管这种架构产出的确是一个分布式 系统,但我们并没有获得一个分布式 编程模型。
由于几乎无需显式考虑容错或并发错误(除了确保选用了 CRDT 正确的一致性级别),开发者自然倾向于这种方案,因为它以简明的顺序语义屏蔽了分布式的复杂性。但这显然是有代价的:性能和扩展性都会受到影响。
对所有操作进行串行化等同于在“模拟”一个非分布式系统,只不过需要昂贵的协调协议。数据库在这种系统中形成了单点故障;你只能希望 us-east-1 不会宕机,或者切换到像 Cockroach 那样的多写入系统,但后者也自带性能上的问题。许多应用在低规模下可以容忍这种局限,但你绝不会愿意这么实现一个计数器。
静态位置
静态位置 架构是编写分布式代码的经典方式。你将多个单元组合在一起——每个单元都是以 本地(单机) 代码编写,并通过异步网络调用与其他机器进行通信:
- 使用 API 调用进行通信的服务(可能采用 async/await,如 gRPC、REST)
- Actor 模型(如 Akka、Ray、Orleans)
- 通过轮询和推送共享 pub/sub 的服务(如 Kafka)
这种架构赋予了我们极大的底层控制权。我们编写的是一堆含有网络调用的顺序、单机代码,这对于性能和容错非常有利,因为我们可以控制代码在何时何地运行。
但网络单元之间的边界 僵硬且不透明。开发者必须单向地决定如何拆分应用。这些决策对正确性有深远影响:重试和消息排序由发送端控制,而接收端却常常一无所知。此外,编程语言与工具对这些单元的组合了解有限,经常无法实现跳转到定义,且服务间的序列化不匹配问题也可能悄然出现。
最重要的是,这种分布式系统设计方法根本上消除了语义共置与模块化。在顺序代码中,连续发生的操作在文本上是连续排列的,而函数调用能封装整个算法;但在静态位置架构中,开发者被迫将代码以机器为边界进行模块化,而非按照语义进行组织。在这样的架构中,根本无法将一个分布式算法封装为一个统一、完整的语义单元。
尽管静态位置架构为开发者提供了系统的最低级控制,但实际上,没有分布式系统专家的支持,要健壮地实现这类系统是极其困难的。这里存在着实现与执行之间的根本矛盾:静态位置软件以单机代码形式编写,但系统正确性要求我们必须从整体上考虑整机集群的行为。构建这类系统的团队往往对并发错误和失败充满恐惧,最终使得一大堆遗留代码因过于关键而不敢轻易修改。
任意位置
任意位置 架构是大多数“现代”分布式系统方法的基础。这种架构让你可以编写看似在单机上运行的代码,但在运行时,软件会动态地在多台机器间执行333。即使 Actor 框架支持迁移,其本质上也不算数,因为开发者仍必须明确界定 Actor 的边界并指定消息传递发生的位置[↩]。
- 分布式 SQL 引擎
- MapReduce 框架(如 Hadoop、Spark)
- 流处理系统(如 Flink、Spark Streaming、Storm)
- 持久执行系统(如 Temporal、DBOS、Azure Durable Functions)
这些架构优雅地处理了语义共置的问题,因为它们在语言/API 中没有显式的网络边界来划分代码。然而,这种简化是以牺牲控制力为代价的。由于允许运行时决定代码如何分布,我们失去了有关如何扩展应用、故障域分布以及数据何时通过网络传输的决策权。
同外部分布模型一样,任意位置架构通常也伴随着性能损失。持久执行系统通常在每一步之间将状态快照到持久存储中444,尽管在处理纯确定性函数时可以进行一些优化[↩]。流处理系统可能会动态持久化数据,并在各步骤之间引入异步处理;SQL 用户则只能任由查询优化器安排,最多只能对分布式决策提供“提示”。
我们往往需要对逻辑在各个位置的分布有底层控制以保证性能和正确性。设想一下如何实现两阶段提交协议:这种协议中,领导者需要明确定义广播提案,而工作者则需要对提案作出确认。为了正确实现这样的协议,必须显式地将特定逻辑分配给相应角色,因为法定人数的确定依赖于单一领导者,而每个工作者都必须以原子方式决定接受还是拒绝提案。在任意位置架构中,实现这样一个协议几乎不可能,而不引入额外的网络和协调开销。
必备的 LLM 部分
如果你一直在关注“agentic” LLM 的动态,可能会疑问:“在我的软件由 LLM 编写的世界中,这些问题还有意义吗?”如果静态位置模型足够丰富,可以表达所有分布式系统,那编程过程中的痛苦似乎就不那么重要了。
但我认为,LLM 实际上是一个很好的论据,说明我们确实需要一个新的编程模型。这些模型在处理散布在大量文本中的上下文相关信息时,通常表现得力不从心555。参见大海捞针测试;更别说分布式系统的问题了[↩]。LLM 最擅长处理语义相关信息共置的场景。
静态位置模型迫使我们将语义相关的分布式逻辑拆分到不同模块中。LLM 目前在推理单个机器上的正确性方面已有不足,更别提将多个单机程序正确组合起来了。此外,LLM 进行决策是按顺序进行的;而将分布式逻辑拆分到多个网络模块中,本质上就与 AI 模型的结构相悖。
如果能够设计出一种保留“语义局部性”的编程模型,LLM 的表现将大为改观。设想一种编程模型,可以将跨多台机器的代码共置,那么所有分布式算法相关的逻辑都将整齐地排列在一起,LLM 就能以直线式逻辑生成分布式代码,从而大幅降低难度。
另一个关键问题是正确性。LLM 并非总是准确无误,最好的策略是结合能够自动发现错误的工具666。Lean 就是一个很好的例子,多个团队(包括 Google 及 Deepseek)已在实践中广泛应用这一理念[↩]。顺序模型无法推断分布式执行可能导致的问题,而一个足够丰富的分布式编程模型则可以揭示由于网络延迟和故障所引发的问题(可以想象成一个专为分布式系统设计的借用检查器)。
我们能从这些系统中学到什么?
尽管以上讨论的各种编程模型各自存在不少局限,但它们同时展示了原生分布式系统编程模型应具备的一些理想特性。我们能从每种模型中吸取哪些经验?
这里我将略过外部分布模型,因为如前所述,其并非真正意义上的分布式。对于那些能够容忍这种模型的性能及语义限制的应用,外部分布是一个不错的选择;但对于通用的分布式编程模型,我们不能将网络和并发完全隐藏在开发者视野之外。
静态位置模型似乎是一个合适的起点,因为它至少能够表达出我们可能希望实现的所有类型的分布式系统,尽管这种编程模型在帮助我们推理分布式问题上力有不逮。而我们从任意位置模型中所缺失的两点分别是:
- 将分布于多台机器的逻辑放在同一个函数中顺次编写
- 将诸如消息乱序、重试以及跨网络边界的序列化格式等分布式行为的语义信息暴露出来
而每一点都有其对应的另一面,这是我们不愿失去的:
- 显式控制逻辑在各个机器上的放置,并能够执行本地、原子化的计算
- 在不迫使语言绑定全局协调与恢复协议的前提下,提供丰富的容错保证和网络语义选项
是时候推出一个原生的编程模型了 —— 类似于为分布式系统打造的 Rust —— 来应对上述所有挑战。
感谢 Tyler Hou、Joe Hellerstein 与 Ramnivas Laddad 对本文的反馈!
- CRDT 经常被宣传为分布式系统的灵丹妙药,但另一种观点认为,它们仅仅是用来加速分布式事务。运行在 CRDT 之上的软件依然是顺序执行的。[↩]
- 至少这是理论上的设想。默认情况下,并非采用串行化(而是快照隔离),因此有时会暴露出并发错误。[↩]
- 即使 Actor 框架支持状态迁移,其本质也不能完全忽略开发者必须显式定义 Actor 边界以及消息传递位置的事实。[↩]
- 对于纯确定性函数的步骤,可做一些优化处理。[↩]
- 参见大海捞针测试;分布式系统的推理更具挑战性。[↩]
- Lean 是这方面的优秀实践。多个团队(包括 Google 与 Deepseek)已在此领域展开应用研究。[↩]