恢复中的亚稳态:带有循环的级联恢复

Lobsters Hottest 新闻

摘要

讨论了级联恢复和反馈循环如何在分布式系统中导致亚稳态故障,阻止正常恢复。

<p><a href="https://lobste.rs/s/hngcku/metastability_recovery_cascading">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/06 04:00

# 带有循环的级联恢复 —— Aleksey Charapko 来源:https://charap.co/metastability-in-recovery-cascading-recovery-with-a-loop/ 我的上一篇关于亚稳态的博客文章讨论了系统和组件之间的交互(https://charap.co/on-metastable-failures-and-interactions-between-systems/),以及它们如何导致亚稳态故障。具体来说,我研究了系统/组件之间的交互,以及由于信号的不明确性,不同系统可能会如何曲解信号 —— 超时可能意味着可以通过重试修复的瞬时故障,但也可能意味着灾难性的过载,此时重试只会让情况更糟。这引出了一个重要认识:系统响应信号所采取的某些常见操作,在某些假设下可能是好的,但在其他假设下则可能是坏的。 上个月,来自Meta的Todd Porter(https://www.linkedin.com/in/todd-porter-584b3231/)、波特兰州立大学的Dave Maier(https://www.pdx.edu/profile/david-maier)和我在SREConn Americas上做了关于恢复中的亚稳态的演讲(https://www.usenix.org/conference/srecon26americas/presentation/porter)。这个演讲讨论了跨系统交互和不明确的假设如何使系统的恢复变得不可能,因为级联放大和反馈循环。 故障可以传播是常见的事实。很多时候,工程努力都投入到系统隔离/划分上,以减少爆炸半径。但如果故障可以传播,那么故障的恢复也可以传播。故障(通常)是可见地传播的 —— 当一个系统开始影响另一个系统时,新受影响的系统上就会响起警报。恢复则以不同的方式传播,而且往往更加隐蔽。即使我们成功隔离了故障,并防止它们直接传播到其他组件,这并不意味着这些故障的恢复不会传播。考虑两个系统:*A* 和 *B*;系统 *A* 遇到问题,但问题仍然局限于 *A*。然而,系统 *B* 通常从 *A* 接收工作,当 *A* 故障时,它发送给 *B* 的工作就减少了。但 *A* 的恢复对 *B* 来说可能是相当出乎意料的 —— *B* 现在开始接收所有的工作,甚至可能更多,因为 *A* 的恢复试图处理积压并将其发送给 *B*。 这些恢复交互形成了一个级联,其中一个系统(我们可称之为“被依赖者”)的恢复会触发其依赖者(系统 *B*)的恢复。在某些情况下,依赖者的恢复可能需要比在常见的非恢复情况下完成相同工作量更多的资源。结果,我们就构建了一个放大的级联,就像雪崩在向下移动时卷起更多的雪一样,整个系统的恢复需要越来越多的工作才能完成。 这本身就是一个问题。虽然不是亚稳态问题,但仍然是一个可能滚雪球般发展成无法恢复系统的重大问题。然而,与雪崩不同,系统恢复也可能有反馈机制,为已经在上游恢复了的系统创造额外工作。而这种向上游创造额外工作的反馈,可能会形成一个故障和恢复的永久循环。 ## 示例 让我们考虑一个例子。想象一个典型的生产者-消费者系统:生产者将一些数据写入共享消息总线(比如 Apache Kafka),消费者获取数据并执行一些昂贵的计算,然后将结果发送给下游的其他服务/组件。在我们的案例中,系统产生时间序列数据,消费者在执行“昂贵计算”之前获取整个时间片(例如,特定数据流的一小时数据)的数据。我用下图说明这个简单的应用: [](https://charap.co/wp-content/uploads/2026/05/cascading_recovery_example-1.png) 但是,生产者端发生了一些事情,导致一些数据丢失,未能按时到达消息总线(下图中第10和第15项)。消费者端不知道有些数据丢失了,因此它遵循常规逻辑:等待特定时间范围内的所有数据停止到达,然后批量处理并执行“昂贵计算”。 [](https://charap.co/wp-content/uploads/2026/05/cascading_recovery_example-2.png) 但是当生产者恢复,丢失的数据最终到达消息总线并传递给消费者时会发生什么?从生产者的角度来看,传递丢失的数据可能并没有多花太多成本 —— 同样的工作量,只是晚了一点。从消息总线的角度来看,也是一样。但对于我们的消费者来说,这是一个问题。 [](https://charap.co/wp-content/uploads/2026/05/cascading_recovery_example-3.png) 消费者应用,尽管从未故障过,却被要求在收到延迟数据后执行恢复操作。延迟的数据属于很久以前完成的批次,因此消费者应用需要重新计算它们。这种批次的重新计算成本很高。远远高于如果及时收到数据,将缺失数据包含在原始批次计算中的成本。然而,消费者的这种成本只是问题的一部分。应用在完成“非常昂贵的计算”后也不会保留旧数据,现在需要重新获取已经收到过一次的旧数据。这就是向上游反馈到消息总线;重新获取数据会给它带来额外的负载。 [](https://charap.co/wp-content/uploads/2026/05/cascading_recovery_example-4.png) 这种由于反馈产生的额外负载可能启动一个循环。如果额外负载很高,消息总线就会变慢,可能无法足够快地接收新数据,或者由于过载和排队,无法按时将数据发送给消费者。这将会在消费者端产生更多需要恢复的批次,而正如我们所记得的,这种恢复会向上游产生反馈到消息总线,使其运行得更慢。 ## 形式化模型 正如我所提到的,恢复会级联,并在甚至可能没有实际故障的系统(只是由于上游故障没有提供工作而未能完成所有常规工作的系统)中引起恢复的涟漪。所以,如果有一个被依赖者故障,就会造成发送给依赖者系统的工作量赤字。这个赤字或积压会随时间积累。当被依赖者恢复时,赤字也必须由被依赖者和依赖者一起恢复。 我们有三种广泛的恢复模式: ## “不匹配” 如果我们看看放大的原因,有很多。我们确定了几种常见的不匹配情况 —— 系统的期望与环境的现实不匹配的情况。 **热路径不匹配:** 当恢复由于时间偏移,不再受益于更快的热数据路径时,就会发生这种情况。例如,在我们的示例应用中,从消息总线重新获取数据可能会命中“冷存储”而不是“热”缓存,因为数据有一段时间没有被使用。 **粒度不匹配:** 依赖系统中的恢复单位与赤字或积压的单位不匹配,因此即使很小的积压也可能需要大量的恢复工作。在我们的示例中,一个缺失的数据项可能只占批次中所有数据的百分之一,却可能导致整个批次的数据被重新获取,整个批次被重新计算 —— 为恢复很小的东西付出很高的代价。 **成本不匹配:** 当系统处理工作时,可能会走不同的执行路径。这些路径可能导致不同的执行成本(从资源角度来看)。如果恢复走的是固有缓慢的路径,就会产生放大。 **一般期望不匹配:** 这有点像包罗万象的东西。但常见的是,我们假设并期望某些行为,结果却发现这些行为在罕见或极端情况下不成立。恢复路径通常较少被使用,因此它们是更罕见的情况,更有可能违背期望和假设。 ## 应对措施 不过,我们可以采取一些措施来应对级联恢复。 **优先采用增量恢复。** 首先,如果我们需要在恢复情况下清理积压,我们应该设计系统使其保持在增量恢复模式。这意味着避免粒度不匹配,并设计为增量、递增的恢复,而不是对更大工作单元进行昂贵的重新计算。 **预算恢复和流整形。** 避免一次性处理所有赤字。快速完成更多工作意味着快速消耗更多资源。将积压恢复分散到更长的时间段或流上,并在有限的“恢复预算”下进行,有助于避免依赖系统中的大负载峰值。 **为恢复而扩容。** 级联恢复某种程度上是可预期的。当被依赖者恢复时,它会将积压发送到下游。提前增加依赖者的容量可以帮助处理突发的工量作。这最适合增量恢复模式,因为恢复工作的成本是已知的。 **充足的端到端资源供应。** 确保资源满足恢复需求,尤其是端到端的!理解正在发生的放大效应并提供充足的资源至关重要。扩容(上一点)会有所帮助,但只有在整体考虑时才行。如果让一个组件变得更强或更高效,而没有让它的依赖者也能处理更多工作,我们就是在为另一种故障做准备。 **避免冷路径,为恢复预热。** 恢复的时间偏移意味着恢复工作可能不是在最佳时间完成。在我们的示例中,时间因素意味着消费者不再拥有批次数据;从消息总线重新获取数据放大了成本,但因为这种重新获取发生在更晚的时间,消息总线可能已将数据从内存缓存移到了存储中,进一步增加了成本。在系统中预热冷数据可能有助于避免一些放大成本。 **为恢复路径设计。** 系统中的快乐路径只有在没有故障发生时才是快乐的。为可恢复性设计系统可能意味着常见情况下的优化程度较低,但具有更好的“性能梯度” —— 系统在常见情况与异常情况下的容量/性能之间的差异。在我们的示例中,为恢复路径设计不仅包括增量恢复,还应最小化其他类型的不匹配,如成本不匹配或热路径不匹配。 ## 模拟 为了更好地理解问题是如何发展的,我在MESSI中模拟了上面的示例。MESSI是一个仍在开发中但已可用的性能模拟工具,旨在研究亚稳态行为。MESSI中的系统表示为逻辑节点和处理器的图,其中逻辑节点有必要的算法和协议并提供工作路由,而处理器模拟所有时间行为,如服务时间和排队。模拟器在离散时间中运行,并通过系统发送工作项来模拟请求及其在组件中的进展。 我们模拟的系统只包含三个逻辑节点:一个用于编码生产者,一个用于简单的消息总线逻辑,第三个用于消费者应用。这些节点通过模拟网络连接。另请注意,某些节点具有起始和结束于同一节点的边;这些自循环边通常表示特定组件的延迟/处理。所有这些结果产生了如下所示的系统图。 生产者创建一个 `ProduceRequestResponse` 请求,将小的时间序列数据项发送到消息总线。我稍微简化了系统,让消费者发起从消息总线的数据传输。消费者定期创建一个 `ConsumerRequestResponse` 请求到消息总线,以获取可用的数据。作为对这个请求的响应,消息总线发送之前未发送过的累积消息。消费者的 `ConsumerRequestResponse` 也可能指定一个时间范围,在这种情况下,消息总线将重新发送该范围内的消息。最后,消费者还创建 `BatchProcessingQItem` 请求,这些请求不发送给任何其他节点;相反,它们用于驱动对数据批次执行“昂贵计算”的模拟。 MsgBus processing cost function 在这个模拟中,我使用了一个多工作队列抽象来模拟消息总线和消费者应用组件上的处理延迟。消息总线上的处理延迟取决于工作类型(接收或发送)、正在发送的模拟数据项数量以及它们的热/冷状态(冷数据项在发送之前需要从存储中获取更多资源)。在这个模型中,我没有模拟实际的存储/IO成本或由于磁盘IO引起的处理中断(但我已经为其他模型/问题做过这些)。类似地,消费者应用模拟了从消息总线接收数据的成本以及执行“昂贵计算”的成本,该成本与接收的数据量成正比。这里的代码片段显示了一个函数,该函数通过从多个分布中提取并聚合成本来定义消息总线上工作的模拟成本。 ### 模拟告诉我们什么? 首先,模拟标准情况 —— 系统在没有数据生产故障的稳定状态下的行为。作为一个度量指标,我关注消费者应用与消息总线的交互(包括延迟和每秒请求数)。其他度量指标也可以,并且可能提供额外的洞察。例如,数据生产的延迟和吞吐量可能更好地显示消息总线过载,而批次创建的延迟可能更好地显示消费者应用过载。不管怎样,为了简洁,我只看消费者应用与消息总线的交互。消费者被配置为每10毫秒向消息总线请求新数据,所以我们期望大约每秒100次交互。下图正好显示了这一点,系统在没有引入生产者端故障的情况下运行。 接下来,我在第10秒标记处引入了一个5秒长的故障。这个故障仅延迟了1%的生产数据,导致它们稍后才到达。在这种情况下(以及这里显示的所有其他模拟中),我让应用构建持续时间为1秒的批次,所以如果这1%的数据到达时间超过了某个配置的等待阈值(一个批次在500毫秒内没有新数据到达就被“密封”以进行处理),那么批次将在没有缺失数据的情况下构建。结果发现,这1%的缺失数据就足以导致灾难性的亚稳态故障。粉色垂直线表示1%故障的开始和结束。在这里,我们清楚地看到消费者应用不再能够向消息总线请求/检索数据,平均数据检索延迟超过500毫秒,远远超过500毫秒的批次密封时间。这造成了更多不完整的批次,这些批次在开始接收延迟数据时需要恢复。 这些结果显示了在我们的示例系统中故障可能多么迅速地发展,即使初始故障率仅为1%的数据缺失。实际上,模拟显示我们很快由于消息总线过载,由延迟数据损坏的批次比初始故障本身造成的还要多。虽然这个测试应用的时间尺度可能与Todd在SREConn上谈论的真实应用不匹配,但它捕获了在真实系统中观察到的非常相似的行为。数据获取高延迟的原因是系统组件的过载。回想一下,故障在第10秒开始,所以到第11.5秒,消费者可能完成了第一个有缺失数据的批次,再经过大约一秒钟,该批次可能开始修复。事实上,在这个模拟中,一个批次可能经历多次修复,使得反馈更加强烈,因为甚至在第一次修复之后可能还有更多的延迟数据到达。所有这些都导致资源迅速耗尽。

相似文章