Gunnar Morling

Gunnar Morling

关于软件工程的随机思考

The Synchrony Budget

发布于 2025 年 3 月 18 日

更新于 3 月 27 日:这篇文章正在 Hacker News 上被讨论

在构建分布式服务系统时,我认为一个非常有价值的概念是所谓的 synchrony budget(同步预算):服务应尽可能减少其向其他服务发出的同步请求的数量。

这背后的原因有两方面:同步调用成本高昂。同步请求越多,处理自身服务的入站请求所需的时间就越长;用户不喜欢等待,如果事情花费太长时间,他们可能会决定转向其他地方。其次,同步请求会影响服务的可用性,因为所有被调用的服务都必须启动并运行,服务才能工作。以同步方式依赖的服务越多,服务的可用性就越低。

同步调用是帮助确保一致性的工具,但从设计上来说,它们会阻塞进程直到完成。从这个意义上讲,synchrony budget 的概念不是关于一个可以花费的实际预算,而是关于有意识地实现服务之间的通信流:尽可能异步,必要时同步。

让我们通过一个例子使事情更具体一些。考虑一个电子商务网站,用户可以在该网站上下达采购订单。当收到订单时,订单录入服务需要与几个其他服务进行交互才能处理该订单:

让我们从最后一个开始,发货服务。下订单的客户是否关心发货服务何时收到通知?一点也不。因此,从订单录入请求处理程序中同步通知发货服务将浪费我们的 synchrony budget 。这不仅会导致入站请求花费比实际需要更长的时间,而且还会导致订单录入请求在发货服务不可用时失败,例如由于维护、网络分裂或某种其他类型的故障。此外,我们不需要将发货服务的任何响应报告回发出入站订单放置请求的客户端。这使得此调用成为异步执行的完美候选者,例如,通过让订单服务向一个 Kafka topic 发送消息,然后该消息被发货服务消费。这样,订单服务请求不会因等待来自发货服务的响应而变慢,发货服务的停机也不会影响订单服务的可用性。它只会再次启动时处理来自 Kafka topic 的任何待处理消息。通常,每当一个服务只需要通知另一个服务发生了某些事情时,默认使用异步通信就很有意义。

以类似的精神,如果任何已更改的数据应从 OLTP 数据存储传播到 OLAP 系统,则应异步完成。根据定义,针对后者发出的分析查询不需要即时了解 OLTP 系统中发生的每个数据更改。因此,向 OLAP 存储发送同步请求将是又一个不必要地花费 synchrony budget 的好例子。

现在,如果我们的消息传递基础设施(例如 Kafka)无法访问怎么办?我们是否回到了原点?我们可以设想为此类情况提供某种缓冲手段,例如将要发送的消息存储在某些本地状态存储中,并在与 Kafka 的连接恢复后将其发送出去。幸运的是,我们不必在此重新发明轮子:outbox pattern 是一种成熟的方法,用于通过服务的数据存储传送传出消息,并且在事务上与同时需要完成的任何其他数据更改保持一致。用于基于日志的变更数据捕获 (CDC) 的工具,例如 Debezium,可用于以低开销和高性能从 outbox 表中提取消息。这样,服务处理传入请求所需的唯一有状态资源就是它自己的数据库。

接下来让我们看一下与库存服务的通信。当订单服务处理传入请求时,它将需要有关指定商品是否以所需数量可用的信息。这与用于与发货服务通信的通知语义不同,因为我们需要来自库存服务的数据才能处理传入请求。那么在这种情况下我们应该进行同步调用吗?当然这可能是一个选择,但它会再次消耗我们的 synchrony budget:这将对我们的响应时间产生影响,并且如果库存服务不可用我们应该怎么做?应该使传入请求失败吗?但由于一些内部技术问题而不接受客户请求听起来并不是那么理想。

反转通信流可能是一种出路:库存服务可以发布库存变更的提要,每当有库存更新时,都会向 Kafka 推送一条消息。订单服务可以订阅此提要,并在其自己的本地数据存储中实现此数据的视图。这样,在处理订单请求时,服务之间不需要同步调用,这只能通过查询订单服务的数据库来完成。库存服务的变更提要可以再次通过 outbox pattern 实现;另一种选择是使用 CDC 来捕获库存数据库中实际业务表中的更改,然后利用流处理,例如使用 Apache Flink,来为该数据流建立 稳定的数据契约。这样,像订单服务这样的消费者就可以免受发货服务数据模型的任何潜在破坏性更改的影响,并且流处理器可以处理非规范化关系表,以便为消费者提供完全上下文相关的事件。

当然,这里存在一个权衡:由于对订单服务库存数据视图的更新是异步发生的,因此我们可能会遇到这种情况,即该视图已过时,并且接受了对商品的请求,而实际上该商品不再有库存。实际上,Debezium 和 Kafka 可以以亚秒级的端到端延迟传播数据更改,因此在正常运行期间,发生错误的时间窗口将非常小。但是,退后一步并从业务角度看待事物也有帮助:现实从一开始就不是事务性的。我记得几年前的一个生日聚会,我的一位朋友当时正在值班,并且在仓库中扔掉一架花后不得不修补电子商务应用程序的库存表。换句话说,企业在任何情况下都需要有应对这种情况的手段。很可能,在极少数情况下,我们会向客户发送一张 10 美元的优惠券作为道歉,因为我们接受了没有库存的商品的订单,而不是花费我们的 synchrony budget 并为此流程建立同步调用流程。

现在,让我们看一下与支付服务的通信。根据具体情况,这实际上可能是一个同步调用合理的情况。例如,在构建航班预订系统时,您确实希望 100% 确定客户的信用卡可以成功扣款,然后再确认预订请求。显然不可能复制世界上所有信用卡和银行帐户的数据,因此也无法反转调用流。支付处理器 API 的构建考虑了极高的可用性是有原因的。这就是 synchrony budget 的概念:尽可能异步地实现服务间调用,以便在绝对需要时有空间进行同步调用。也就是说,对于电子商务应用程序,默认情况下对支付服务进行同步调用实际上可能是可行的,但在发生故障时会回退到异步处理。由于出售合同通常仅在商品发货时才被接受,因此如果付款在异步处理路径上失败,您仍然有空间取消订单。

最后,以下是我们的与订单服务相关的数据流的整体解决方案的样子,应用 synchrony budget 的思维模式:

Synchronous and Asynchronous Data Flows in an E-Commerce System © 2019 - 2025 Gunnar Morling | 根据 Creative Commons BY-SA 4.0 获得许可