基于重试协调器的事务重试架构 (Transaction Retry Architecture With Retry Coordinator)

本文档是 StackSaga 框架*事务重试架构 (Transaction Retry Architecture)* 的权威参考指南。 专为集成、部署或运维基于 StackSaga 的微服务的*架构师*、*DevOps 工程师*和*开发人员*编写。

简介 (Introduction)

在分布式微服务架构中,事务通常跨越多个服务和数据库,这使得它们极易受到网络瞬态故障、超时和服务暂时不可用的影响。 StackSaga 框架通过高可靠的*事务重试子系统 (Transaction Retry Subsystem)* 解决了这一严峻挑战,在确保可靠性和最终一致性的同时,杜绝了重复处理或并发冲突的风险。

该子系统能够精准检测处于暂停状态的事务,并结合使用令牌环分区 (Token Ring Partitioning)、RSocket 响应式通信以及时间窗口发布机制,在微服务实例集群之间安全地重新触发这些事务。 其结果是:即使面对瞬态故障也能实现高可用性与系统弹性,同时严格保证事务*恰好执行一次 (Exactly-Once)*。

本文档范围 (Scope of This Document)

本文档专门聚焦于*事务重试子系统 (Transaction Retry Subsystem)*。

涵盖的主题包括:

  • 识别可重试错误与不可重试错误 (Retryable vs Non-Retryable Errors)

  • 为什么仅依靠实例级重试是不够的

  • 编排器部署模式:标准节点 (Standard-Node) 与重试节点 (Retry-Node)

  • 三组件重试生态系统:Orchestrator、Retry-Coordinator-Slave 与 Retry-Coordinator-Master

  • 基于 Murmur3 的令牌环分区机制 (Token Ring Partitioning)

  • 组件间的 RSocket 通信模式

  • 基于时间窗口的令牌发布与冲突规避 (Conflict Avoidance)

  • 故障模式、重连行为与部署考量

  • 多区域部署架构 (Multi-Region Deployments)

  • 适用于超大规模系统的虚拟集群分区 (Virtual Cluster Partitioning)

核心基础概念 (Foundational Concepts)

在深入研究架构之前,必须先明确两个核心概念:什么类型的错误会触发重试,以及为什么不能简单地由发起事务的原始节点来负责重试。

可重试错误与不可重试错误 (Retryable vs Non-Retryable Errors)

StackSaga 将错误划分为两大类别:

不可重试错误 (Non-retryable error) —— 业务逻辑校验失败或永久性错误状态。 此类错误不是瞬态故障,无法通过重试来解决。 典型示例包括参数验证失败、权限认证失败,或任何表明事务本身存在根本性业务冲突的条件。 这类错误在主流程(向前执行)和补偿流程(回滚)中都可能发生。 在主流程中,不可重试错误会触发补偿回滚;如果发生在补偿流程中,该事务将被标记为永久失败 (Permanently Failed)。 在这两种情况下,事务都*不会被重试*。

可重试错误 (Retryable error) —— 瞬态或临时性故障,例如下游微服务暂时不可用、数据库连接超时或瞬时网络分区。 事务将被置于暂停状态 (Paused),并由系统自动重播。 可重试错误在主流程和补偿流程中都有可能发生。 这正是重试子系统发挥核心作用的场景,确保事务在没有重复执行或并发冲突的情况下安全地完成重试。

为什么实例级重试是不充分的 (Why Instance-Level Retrying Is Insufficient)

乍看之下,让最初处理该事务的同一节点负责重试似乎轻而易举。 然而,这种方法存在致命的设计缺陷。

标准微服务节点是*瞬态的 (Ephemeral)* —— 它们的生命周期较短,旨在根据实时流量进行弹性伸缩、动态销毁或按需替换。 这意味着,当重试时机到来时,最初处理该事务的节点可能早已下线或被销毁。 这样一来,该事务将无限期处于暂停状态,没有任何节点能认领并继续执行它。

举个具体的例子:假设有 3 个正在运行的微服务实例,每个实例都因下游网络波动而暂存了一些待重试的事务。 如果依赖每个实例自身的定时调度器来重播各自的事务,而其中一个实例因缩容策略被 Kubernetes 终止下线,那么它原本持有的事务将永远无法被其余实例接管重试。

为什么 StackSaga 不允许实例直接进行本地重试

StackSaga 通过将*事务重试完全与原始发起节点解耦*解决了此问题,其核心正是下文所述的三组件架构。

编排器部署模式 (Orchestrator Deployment Modes)

由于标准的编排器节点具有瞬态性,StackSaga 为编排器服务定义了两种不同的部署模式:

标准节点 (Standard-Node)

常规的编排器微服务(启用了 StackSaga),负责常规业务流量的事务处理与逻辑编排。 标准节点可以根据系统流量自由进行水平扩容或缩容。

重试节点 (Retry-Node)

在处理常规事务的基础之上,额外承担事务重试管理职责的专属节点。 重试节点被设计为比标准节点更加稳定和长寿命 (Long-Lived),以确保在集群其余节点动态伸缩时,重试管理始终平稳可靠。

stacksaga diagram standard node vs retry node

在典型的生产部署中,您可能会配置 5 到 1000 个标准节点来应对高并发流量,同时仅保持 2 个长期稳定的重试节点来专职管理重试任务。 技术上并没有限制重试节点扩缩容的硬性规则——这种区分主要是运维层面的:重试节点就是您选择保持高稳定性的服务节点。

重试是事务处理过程中的*最坏降级路径 (Worst-Case Scenario)*,而非正常主流程。 有关重试发生的频率及其相对于常规流程的处理开销比重,请参阅 Saga 长事务比例分析。

接下来的关键挑战在于:如何在重试节点集群中安全地分配重试职责,避免任意两个节点针对同一事务产生并发竞争冲突? 这正是*令牌环分区 (Token Ring Partitioning)* 所解决的问题。

令牌环分区 (Token Ring Partitioning)

概述 (Overview)

StackSaga 采用 Murmur3 一致性哈希算法 (Murmur3 Consistent Hashing Algorithm) 将每个事务确定性地映射到 64 位令牌环上的唯一位置。 当事务*首次初始化*时(由任意 Orchestrator 发起),系统会计算该令牌值,并与事务记录一同持久化到数据库中,同时记录发起 Orchestrator 的*集群名称 (Cluster Name)* 与*区域 (Region)*。 重试子系统利用持久化存储的令牌、集群和区域信息,在任意时间点确定*具体由哪一个 Orchestrator 实例全权负责*该事务的重试。

完整的令牌空间范围从 \(-2^{63}\) 到 \(2^{63}-1\):

-9,223,372,036,854,775,808  →  9,223,372,036,854,775,807

下图展示了重试节点如何分配互不重叠的令牌环分段。 在此示例中有 4 个重试节点,每个节点各自负责令牌空间的四分之一:

stacksaga diagram token ring partitioning for transaction retrying with retry nodes
  • 哈希空间范围: -9223372036854775808 → 9223372036854775807

  • 总哈希范围大小: 18446744073709551616

  • 每节点分区大小 (4 节点): 4611686018427387904

节点 (Node) 槽位 (Slot) 起始令牌 (Token Range Start) 结束令牌 (Token Range End)

order-service-instance-1

A

-9223372036854775808

-4611686018427387905

order-service-instance-2

B

-4611686018427387904

-1

order-service-instance-3

C

0

4611686018427387903

order-service-instance-4

D

4611686018427387904

9223372036854775807

两级分区机制 (Two-Level Partitioning)

令牌环通过*两级层级*进行切分:

  1. Master → Retry-Coordinator-Slave: Retry-Coordinator-Master 将完整的令牌环均分给所有已注册的 Retry-Coordinator-Slave 节点。

  2. Retry-Coordinator-Slave → Orchestrator: 每个 Retry-Coordinator-Slave 进一步将自身分配到的令牌切片均分给连接到它的各个 Orchestrator 实例。

stacksaga diagram ring partitioning overview
Figure 1. 令牌环分区:节点视角
stacksaga diagram token ring partitioning for transaction retrying
Figure 2. 令牌环分区:分区视角

Master → Retry-Coordinator-Slave 示例 (3 个 Retry-Coordinator-Slave)

从重试协调器节点 (Slave Node) 起始令牌 (Inclusive) 结束令牌 (Inclusive)

Retry-Coordinator-Slave Node 1

-9,223,372,036,854,775,808

-3,074,457,345,618,258,603

Retry-Coordinator-Slave Node 2

-3,074,457,345,618,258,602

3,074,457,345,618,258,602

Retry-Coordinator-Slave Node 3

3,074,457,345,618,258,603

9,223,372,036,854,775,807

每当已注册的 Retry-Coordinator-Slave 数量发生变动时,分区边界将在下一个 30 秒发布周期由系统自动重新计算。

Retry-Coordinator-Slave → Orchestrator 示例 (Slave Node 1 下挂 2 个 Orchestrator)

编排器实例 (Orchestrator Instance) 起始令牌 (Inclusive) 结束令牌 (Inclusive)

Order Service Instance 1

-9,223,372,036,854,775,808

-6,148,914,691,236,517,206

Order Service Instance 2

-6,148,914,691,236,517,205

-3,074,457,345,618,258,603

所有其他的 Retry-Coordinator-Slave 节点对其各自注册的 Orchestrator 实例遵循完全相同的划分逻辑。

并不强制要求多台物理机才能实现令牌环分区。 ring-coordinator-starter 模块可以通过配置属性自由声明为 Master 或 Retry-Coordinator-Slave。 在小型部署中,单个节点可以同时身兼 Master 和 Retry-Coordinator-Slave 两个角色。 在大型生产部署中,通常将这两个角色拆分到不同的独立节点上,以实现故障隔离与水平扩展。

三组件重试架构 (The Three-Component Retry Architecture)

令牌环分区是由三个组件通力协作管理的。 在详细深入各组件之前,先进行整体职责速览:

组件 (Component) 核心职责 (Responsibility)

Retry-Coordinator-Master

领域的权威决策核心。 每 30 秒将完整的令牌环均分给所有活跃的 Retry-Coordinator-Slave。 通过轮询算法 (Round Robin) 指引新上线的 Orchestrator 应该连接哪一个 Retry-Coordinator-Slave。

Retry-Coordinator-Slave

Master 与 Orchestrator 之间的中继枢纽。 接收 Master 下发的令牌切片,并进一步将其细分给连接到它的各个 Orchestrator。 每分钟将专属的子令牌范围下发给对应 Orchestrator。

Orchestrator (Retry-Node)

持有当前分钟内的子令牌范围。 轮询数据库中令牌落入该子范围内的暂停事务并重新执行——绝不会与其他任何实例产生冲突。

每个组件职责边界极其清晰,没有越权侵入。 最终构建出一个支持水平扩展、容忍部分节点故障,并严格保证每个暂停事务*在任意时刻恰好被单一节点执行重试*的高可用系统。

Retry-Coordinator-Master (主重试协调器)

架构定位

Retry-Coordinator-Master 是微服务领域内重试协调的*唯一权威实体 (Single Authority)。 每个业务领域内*有且仅有一个 Master(或者每个虚拟集群一个 Master——参见 通过虚拟集群进行水平扩展)。

Master 完全不感知您的业务逻辑,并且绝不直接访问事务数据库。 它的全部职责就是纯粹的协调调度:切分令牌环、跟踪各个 Retry-Coordinator-Slave 的存活状态,并在新的 Orchestrator 启动时为其指引正确的 Retry-Coordinator-Slave。

它基于 RSocket 构建在*全非阻塞的 Netty 反应式堆栈*之上,这意味着单个 Master 实例即可轻松维持*数千个并发的 Retry-Coordinator-Slave 长连接*——远超绝大多数生产系统的需求。

三大核心职责

职责 1 — 维护 Retry-Coordinator-Slave 注册表。 每个启动的 Retry-Coordinator-Slave 通过持久的 RSocket request-stream 连接到 Master 并注册自身。 Master 维护所有在线连接的 Slave 实时注册表。 当某个 Slave 断开连接(崩溃、重启)时,Master 能够立即察觉并更新注册表。

职责 2 — 每 30 秒发布一次令牌环分区。 在每分钟的第 30 秒,Master 触发定时器。 它根据当前注册的 Slave 列表,将完整的 64 位令牌环均分,并将分配到的范围推送给每个 Slave,打上在*下一个整分钟*生效的时间戳标签。 这 30 秒的提前量为整个集群提供了充足的时间,以便在正式生效前接收并就绪新分配的分区。

职责 3 — 指引新上线 Orchestrator 连接对应 Slave。 当新的 Orchestrator 实例启动时,它向 Master 发送单次 Request-Response 探测请求,询问:“我应该连接哪一个 Retry-Coordinator-Slave?” Master 采用*轮询算法 (Round Robin)* 从可用列表中挑选一个 Slave——将 Orchestrator 均匀打散到各个 Slave 上——并返回该 Slave 的主机名和端口。 在此次交互之后,Orchestrator 不再与 Master 联系;后续的所有通信都直接通过 Slave 进行。

知识边界 (Knows and Does Not Know)

感知的信息 (Knows)
  • 已注册的 Retry-Coordinator-Slave 节点及其连接存活状态。

  • 如何使用 Murmur3 分区算法均分 64 位令牌环。

  • 通过轮询算法为新上线的 Orchestrator 分配对应的 Slave。

*不*感知的信息 (Does not know)
  • 初始寻址完成后的各个单独 Orchestrator 实例细节。

  • 事务数据库及其内容。

  • 具体的业务逻辑或事务结构。

  • 其他微服务领域的 Master——每个 Master 在其所属领域内高度自治隔离。

Master 节点故障处理 (Master Node Failure)

当 Master 节点发生故障宕机时:

  1. 所有已注册的 Retry-Coordinator-Slave 失去与 Master 的长连接。

  2. Retry-Coordinator-Slave 停止接收新的令牌范围更新。 其*最后已知的缓存范围将保留*,但会在下一个分钟边界到达时过期。

  3. Orchestrator 继续使用最后接收到的子范围执行重试,直到该范围自然过期。

  4. 新上线的 Orchestrator 实例由于无法从 Master 获取 Slave 分配,初始化重试连接将失败。

Master 故障*仅影响该领域的重试子系统*。 其他微服务领域的运行完全不受影响。 最重要的是,新事务的常规主执行流程(业务流量)完全不受 Master 存活状态的影响。

对于即使是这种局部故障窗口也无法容忍的超高可用系统,请参阅虚拟集群 (Virtual Clusters)。

Retry-Coordinator-Slave (从重试协调器)

架构定位

Retry-Coordinator-Slave 是一个*专用的基础设施轻量级服务*,通常与您的微服务伴生部署。 它完全不感知任何业务逻辑。 它不触碰事务数据库。 它不执行任何业务事务。

它的核心目的在于充当 Retry-Coordinator-Master 与 Orchestrator 实例之间的*分发网桥 (Distribution Bridge)*:从 Master 接收较大的令牌切片,并将其进一步细分成更小且互不重叠的子范围,分别下发给连接到它的各个 Orchestrator。

四步日常流程 (Four-Step Routine)

步骤 1 — 向 Master 注册。 启动时,Retry-Coordinator-Slave 向 Master 打开一条持久的 RSocket request-stream 连接,宣告自身上线,并静默等待令牌范围更新。

步骤 2 — 接收令牌范围切片。 每 30 秒 Master 发布更新的范围。 Slave 接收其分配到的切片(完整 64 位环的一段连续区间),并获取该切片在哪个自然分钟内生效的元数据。

步骤 3 — 接受 Orchestrator 注册。 当 Orchestrator 启动并连接时,Slave 在其本地注册表中记录该实例。

步骤 4 — 细分并下发。 凭借手中的令牌切片和连接的 Orchestrator 列表,Slave 将切片等额细分——每个 Orchestrator 独占一段互不重叠的子范围——并通过持久流推送到各自对应的 Orchestrator。

此循环每分钟重复一次,使每个连接的 Orchestrator 实时掌握其当前所拥有的重试权限窗口。

为什么需要 Slave 这一中间层?

微服务 Pod(即 Orchestrator)的变动极其频繁——频繁发布部署、高并发自动扩容、夜间自动缩容。 如果由 Master 直接跟踪每个业务 Pod,频繁的注册和注销事件将使 Master 不堪重负,陷入持续全量重新计算令牌环的泥潭。

Retry-Coordinator-Slave 在此起到了关键的*缓冲区 (Buffer)* 作用。 Master 只需要感知极少数且极其稳定的 Slave 节点。 底层应用 Pod 的一切伸缩波动全部由 Slave 在本地静默吸收与消化,由其在 Orchestrator 变动时自行微调子范围分配——完全不会打扰 Master 的稳定性。

知识边界 (Knows and Does Not Know)

感知的信息 (Knows)
  • 自身所注册的 Master 地址(stacksaga.agent.slave.target-master.host / .port)。

  • 当前连接到自身的 Orchestrator 实例。

  • 分配给自身的令牌切片以及如何细分该切片。

*不*感知的信息 (Does not know)
  • 业务逻辑或事务执行图。

  • 事务数据库。

  • 其他 Slave 节点——各个 Slave 之间完全独立无感知。

Slave 节点故障处理 (Slave Node Failure)

当某个 Retry-Coordinator-Slave 崩溃或网络不可达时:

  1. 连接到该 Slave 的所有 Orchestrator 丢失数据流。 它们的子范围失效,这些实例的重试轮询将暂时挂起。

  2. Master 察觉连接断开并执行*惰性再平衡策略 (Lazy Rebalance Strategy)*:

    • 如果丢失的 Slave 不是注册表中的最后一个索引,Master 假定其很快会自愈恢复(在 Kubernetes 环境中尤为常见),*不会立即*重新全量打散分区。

    • Master 继续向其余 Slave 下发缓存的原有分区。

    • 崩溃 Slave 对应的令牌范围暂时*冻结 (Frozen)*——在此故障窗口期内没有 Orchestrator 会认领该范围。

  3. 当该 Slave 重启后,重新连入 Master 并作为*新注册节点*处理。

  4. 在下一个 30 秒发布周期,Master 重新计算并将范围下发给所有当前在线的 Slave(包含重启完成的 Slave)。

在 Slave 故障期间,哈希令牌落入该冻结范围内的事务*暂时不会被重试*,直到该范围重新被节点认领覆盖。 这些事务安全保存在数据库中,状态维持为 FAILED_WITH_RETRYABLE_ERROR,绝不会丢失。

具备重试连接器的编排器 (Orchestrator With Ring-Coordinator connector, Retry-Node)

重试节点 (Retry-Node) 是指启用了 StackSaga 重试环连接器 的 Orchestrator 实例,专门参与分布式事务重试生态系统。 它是在执行常规事务流程之外,额外承担事务重试调度职责的专属节点。 重试节点被设计为比普通标准节点更加稳定和*长寿命 (Long-Lived)*,以确保在集群其他节点弹性伸缩时,重试调度平稳连续。

  • 要将标准节点升级为重试节点,只需在现有 Orchestrator 服务中引入 stacksaga-ring-coordinator-connector 依赖并配置连接参数。

  • 技术上对重试节点的伸缩没有任何限制——区分纯粹是运维维度的:它们就是您团队选择保持高稳定性的微服务实例。

  • 一旦配置为重试节点,Orchestrator 会自动加入重试体系:初始连接 Master 寻址 Slave,随后从被分配的 Slave 接收分配给本节点的令牌子分区,并开始主动拉取并重试落入该分区的所有暂停事务。

它在内部始终并行运行两项核心作业。

两项并发作业 (Two Concurrent Jobs)

作业 1 — 执行常规事务 (标准编排器职责)。 当新的业务请求到达时(例如用户下单),Orchestrator 创建事务,将其分解为各个*跨度 (Spans)* 并依次执行——调用下游服务、更新数据库、发布事件。 如果某个跨度发生不可逆的永久性错误,它将触发*补偿 (Compensation)* 回滚流程,逆向撤销已执行的操作。

作业 2 — 重试暂停的事务 (重试调度职责)。 某些跨度失败并非由于业务冲突,而是由于底层资源瞬时不可用。 StackSaga 不会抛弃这些事务——而是将其标记为暂停状态并持久化到数据库中。 Orchestrator 周期性地检查数据库中的暂停事务并重新执行。 关键在于:根据当前时间窗口持有的令牌子范围,它仅检查并认领自身*全权负责*的那部分事务。 这正是 StackSaga 确保即使有海量运行节点,也绝不会出现两个节点同时重复重试同一事务的核心保障。

知识边界 (Knows and Does Not Know)

感知的信息 (Knows)
  • 自身的业务逻辑与事务跨度定义。

  • 当前由对应 Slave 下发的独占令牌子范围。

  • 事务数据库——直接对事务表进行读写。

*不*感知的信息 (Does not know)
  • 其他 Orchestrator 实例——实例之间完全没有 P2P 对等通信。

  • Retry-Coordinator-Master——在初始启动握手之后,Orchestrator 绝不会再次联系 Master(除非与 Slave 的长连接永久断开需要重新寻址)。

  • 令牌环在底层的全局计算逻辑——它只负责使用分配给自己的范围。

Orchestrator (Retry-Node) 故障处理

当 Orchestrator 实例崩溃并重启时:

  1. 向 Master 发起全新的 Request-Response 单次请求以获取可用 Slave(由于轮询机制,可能会被分配到*不同的 Slave*)。

  2. 与新分配的 Slave 建立持久的 RSocket 流。

  3. 在下一个 30 秒周期接收当前子范围并恢复重试轮询。

在崩溃瞬间正在活跃重试中的任何事务,都将在下一个时间窗口内由接管该令牌范围的 Orchestrator 实例自动重新执行。

注册与令牌分发时序流程 (Registration & Token Distribution Flow)

下方的时序图将这三大组件有机串联在一起,完整展示了从启动注册到活跃重试执行的全生命周期。

registration-sequence

逐步详解 (Step-by-Step Description)

阶段 1 — Retry-Coordinator-Slave 注册

  1. 启动时,每个 Retry-Coordinator-Slave 向 Master 打开一条持久的 RSocket request-stream 长连接。

  2. Master 将该 Slave 记录到其实时注册表中。

  3. Slave 保持连接畅通,被动等待令牌范围更新。

阶段 2 — Orchestrator 注册

  1. 启动时,每个 Orchestrator 向 Master 发送一条单次的 RSocket Request-Response 探测消息,申请分配一个可用的 Slave。

  2. Master 采用*轮询算法 (Round Robin)* 选择一个健康的 Slave,并返回该 Slave 的主机地址和端口号。

    如果同一个 Orchestrator 实例重启,随着轮询指针的前移,它可能会被分配到*不同的* Slave。这确保了在长时间运行中各个 Slave 上的负载保持均衡。
  3. Orchestrator 向其分配到的 Slave 打开持久的 RSocket request-stream 连接,开始监听子范围更新。

阶段 3 — 令牌范围分发

  1. 在每分钟的第 30 秒,Master 触发其内部发布定时器。

  2. Master 将完整的令牌环在所有已注册的 Slave 之间均分,并将带有 validForMinute = T+1 生效标识的范围推送到各个 Slave。

  3. 每个 Slave 将自身负责的范围在注册到它的 Orchestrator 之间均分,并将子范围推送给下游。

  4. 每个 Orchestrator 存储接收到的子范围,并在第 T+1 分钟起始时刻将其无缝激活。

阶段 4 — 重试执行循环

一旦时间窗口进入生效期,每个 Orchestrator 即刻启动其重试轮询循环:

  1. 从事务存储中查询满足以下条件的记录:

    • token 介于 [subRangeStart, subRangeEnd] 闭区间内

    • cluster 严格匹配 Orchestrator 配置的集群标识

    • region 严格匹配 Orchestrator 配置的物理区域

    • status = FAILED_WITH_RETRYABLE_ERROR (因可重试错误而失败)

  2. 针对查询出的每笔事务,重新调用其下一个挂起的待执行跨度 (Span)。

  3. 循环执行直至该时间窗口结束(被下一个周期的子范围平滑取代)。

RSocket 通信模式 (RSocket Communication Patterns)

StackSaga 选用 RSocket 作为所有组件间通信的骨干协议,主要是因为其在非阻塞 Netty 传输层之上提供了出色的持久性、反应式双向数据流支持。

连接链路 (Connection) 交互模型 (Interaction Model) 描述说明 (Description)

Retry-Coordinator-Slave → Master

Request-Stream

持久流订阅。Slave 在 Master 上完成注册并保持长连接通道,用于持续接收令牌范围更新。

Orchestrator → Master

Request-Response

一次性单次寻址。Orchestrator 在启动时向 Master 请求分配一个可用的 Slave 节点。

Orchestrator → Retry-Coordinator-Slave

Request-Stream

持久流订阅。Orchestrator 订阅分配给它的 Slave 并保持长连接通道,用于接收其专属的子令牌范围更新。

令牌时间窗口与冲突规避 (Token Time Window & Conflict Avoidance)

发布周期 (The Publish Cycle)

Retry-Coordinator-Master 运行一个在*每分钟第 30 秒*触发的循环定时器。 在每次触发时,Master:

  1. 根据当前活跃注册的 Slave 重新计算令牌环分区。

  2. 将最新计算出的令牌范围下发给每个 Slave。

  3. 每个 Slave 重新计算其子范围并将更新推送到其注册的各个 Orchestrator。

所发布的令牌范围被明确标记为在*下一个完整自然分钟*(T+1)生效,而非当前分钟。

为什么采用 30 秒的时间偏移量?

在第 30 秒发布并标记在*下一分钟*生效,构建了一个整整 30 秒的缓冲就绪窗口 (30-second Preparation Window)。 该时间窗口充分考虑了各种极端网络延迟:Slave 层层分发开销、Orchestrator 处理开销以及超大型集群中的跨节点网络抖动。

当第 T+1 分钟开始的瞬间,所有 Orchestrator 均已绝对可靠地在本地就绪了最新的子范围,可以立即无缝启动新一轮事务轮询。

时间线示例 (Timeline Example):

  第 T 分钟第 30 秒 → Master 发布令牌分区 (标记 T+1 分钟生效)
  第 T 分钟第 31–59 秒 → Slave 与 Orchestrator 陆续接收并本地缓存新分区
  第 T+1 分钟第 0 秒 → 各 Orchestrator 基于新分区准时启动重试轮询
  第 T+1 分钟第 30 秒 → Master 发布适用于 T+2 分钟的新分区
  ...

冲突规避保证 (Conflict Avoidance)

由于每个 Orchestrator 在*确定的时间窗口*内持有*互不重叠的子范围*,两个 Orchestrator 实例绝不可能在同一时间对同一笔事务声称认领责任。 这在分布式架构层面从根本上消除了事务重试对分布式锁 (Distributed Locking) 的依赖。

多区域部署架构 (Multi-Region Deployment)

对于跨多个地理区域部署的系统,StackSaga 通过 region 属性界定重试归属权。 每个组件——Master、Retry-Coordinator-Slave 以及 Orchestrator——都打上了其所属区域的烙印。

区域隔离的工作原理 (How Region Scoping Works)

当一笔事务创建时,它自动继承发起该事务的 Orchestrator 的区域属性。 该区域值被持久化到数据库的事务记录中。

在重试轮询期间,每个 Orchestrator 在令牌范围过滤之外,将 region 作为强制约束条件:

SELECT *
FROM   transactions
WHERE  token   BETWEEN :subRangeStart AND :subRangeEnd
AND    region  = :region
AND    cluster = :cluster
AND    status  = 'FAILED_WITH_RETRYABLE_ERROR';

这确保了位于 us-central 区域的 Orchestrator 绝不会尝试拉取并重试由 asia-south 区域发起的事务——即使两个区域共享同一个底层分布式数据库(例如全球多活的 Cassandra 集群)。

虚拟集群 (Virtual Clusters)

为什么需要虚拟集群?

在超大规模部署中,随着系统体量的激增,单领域单 Master 架构会面临两大挑战:

  1. 吞吐扩展 (Scale) — 单个 Master 必须维持该领域的所有 Slave 连接。 尽管 Master 基于 RSocket 的反应式非阻塞 Netty 堆栈能够承载数千个长连接,但在极端规模场景下,平台架构师可能希望进一步分流协调负载。

  2. 故障隔离 (Fault Isolation) — 如果该领域的唯一 Master 发生故障,该领域的重试子系统将暂停,直到其自愈恢复。 某些对可用性极其严苛的核心系统要求:部分基础设施故障所波及的重试能力绝不能超过设定的安全比例。

虚拟集群 (Virtual Clusters) 通过将单一物理部署划分为*多个完全独立的 Master-Slave 组*,完美解决了上述问题。每个组在同一物理区域内作为完全独立的重试协调单元运行。

什么是虚拟集群?

虚拟集群是由特定名称标识的 Retry-Coordinator-Master、Retry-Coordinator-Slave 和 Orchestrator 实例集合,与其他组完全物理隔离运行。 组标识通过 stacksaga.instance.cluster 属性进行声明。

共享相同集群名称的三类组件共同构成一个逻辑重试集群:

  • Master 仅接受具有匹配集群名称的 Slave 注册。

  • Slave 仅连接属于自身集群的 Master(stacksaga.agent.slave.target-master.host / .port)。

  • Orchestrator 在初始寻址时仅连接自身集群的 Master,并随后连接该 Master 分配的 Slave。

  • 重试 SQL 查询在令牌范围和区域之外,将集群名称作为强制过滤条件。

运行在同一物理 Kubernetes 集群内的两个虚拟集群是*完全隔离的*——它们不共享任何状态、不共享任何网络连接,重试职责也完全独立。

部署拓扑 (Deployment Topologies)

多区域单集群 (Multi-Region Single-Cluster)

最基础的多区域方案——每个物理区域配置一个虚拟集群。 这是地理分布式部署的基准标准。

多区域多虚拟集群 (Multi-Region Multi-Virtual-Cluster)

为了实现极致的扩展性与故障隔离,每个物理区域内部署*多个虚拟集群*。 如果任何单个 Master 发生故障,仅仅波及属于该特定虚拟集群的事务比例——同一区域内的其余虚拟集群继续完全正常运作。

跨虚拟集群的事务归属权 (Transaction Ownership Across Virtual Clusters)

事务永久绑定到创建它的 Orchestrator 所在的区域和虚拟集群。 其他任何虚拟集群——即使位于相同的物理区域内——都绝不会尝试接管重试。

重试查询语句始终强制包含 region 和 cluster:

SELECT *
FROM   transactions
WHERE  token   BETWEEN :subRangeStart AND :subRangeEnd
AND    region  = 'us-central'        -- 物理区域
AND    cluster = 'us-central-c1'     -- 虚拟集群
AND    status  = 'FAILED_WITH_RETRYABLE_ERROR';

这种设计尤其契合 Apache Cassandra 等分布式数据库,在 Cassandra 中可以将 region 和 cluster 作为复合分区键 (Partition Key) 的一部分,从而使数据库能够将查询精确路由到特定的数据节点,完全规避全表扫描。

专业术语表 (Glossary)

术语 (Term) 术语定义 (Definition)

跨度 (Span)

分布式事务中单一且可独立追踪的工作执行单元。一个完整的事务由一个或多个按序执行的跨度组成。

令牌 (Token)

从事务 ID 经 Murmur3 哈希算法计算得到的 64 位整数。用于将事务确定性地映射给特定的重试责任人。

令牌环 (Token Ring)

切分为互不重叠连续区间的完整 64 位整数哈希空间。StackSaga 采用 Murmur3Partitioner 模型,原理与 Apache Cassandra 的令牌环完全一致。

令牌范围 (Token Range)

令牌环上的连续子集区间,在特定的时间窗口内由 Master 授予特定的 Slave 或 Orchestrator 独占负责。

时间窗口 (Time Window)

一个自然分钟的时间周期,在此期间 Orchestrator 对其分配的子令牌范围拥有排他性的重试所有权。

发布周期 (Publish Cycle)

每分钟第 30 秒触发的周期性事件,Master 在此期间重新计算并分发令牌范围。

补偿回滚 (Compensation)

当事务主执行流程遇到不可重试错误时触发的反向回滚撤销序列。

Murmur3 分区器 (Murmur3Partitioner)

一种采用 MurmurHash3 算法的一致性哈希方案,用于在令牌环上均匀分布哈希键。

物理区域 (Region)

物理机房或云部署边界(例如 default、us-central、asia-south)。通过 stacksaga.instance.region 声明,记录于事务中(编码为 SagaUUID 中的 12 位代码),并作为强制的重试过滤条件。参见 SagaRegionResolver。

虚拟集群 (Virtual Cluster)

命名逻辑组,包含完全物理隔离的 Master、Slave 和 Orchestrator 实例。通过 stacksaga.instance.cluster 声明,支持在同一物理区域内划分多个独立的重试单元。

RSocket

一种支持多种交互模式(request-response、Request-Response、request-stream 和 channel)的二进制应用层协议。StackSaga 采用其中的 request-stream 和 Request-Response。

惰性再平衡 (Lazy Rebalance)

Master 的弹性策略:在 Slave 意外断开时推迟令牌环的重新切分计算直到下一个预定发布周期,避免对临时网络波动做出过激的瞬时再平衡反应。

标准节点 (Standard-Node)

负责常规业务事务处理的瞬态 Orchestrator 实例,可根据业务流量自由水平伸缩。

重试节点 (Retry-Node)

长期稳定运行的 Orchestrator 实例,同时承担常规事务处理与事务重试管理两项职责。

事务重播调用的模块交互架构 (StackSaga Module Interaction for Transaction Re-Invocation)

下图从高层架构视角展示了 StackSaga 各模块之间如何协作完成事务的重新调用。 该图经过了适当简化,仅突出与事务重播直接相关的核心模块,略去了部分底层实现细节。

stacksaga diagram module interaction for invocation
  1. stacksaga-ring-coordinator (Slave) 通过持久的 RSocket 流向 Orchestrator 上的 stacksaga-ring-coordinator-connector 模块发布分配给它的令牌子范围。

  2. 对应 stacksaga-{database}-support 数据库实现中的 ReInvokeTaskManager 任务管理器基于分配到的令牌子范围长轮询事件存储,查找需要被重新调用的事务。

  3. 符合重试条件的事务被移交给对应 stacksaga-{impl}-support 实现模块中的 TransactionReInvokeManager。

  4. TransactionReInvokeManager 通过 stacksaga-{database}-support 提供的 event-store service 请求历史事件数据,将事务快照 (Snapshot) 重建到发生故障的断点状态。

  5. 事务快照重建完毕后,TransactionReInvokeManager 将快照发送给同模块内的 ExecutionManager (执行管理器)。

  6. 最终,引擎像处理常规流程一样重新执行该事务。

虽然架构图中未直接画出 stacksaga-env-{impl}-support,但该模块专门负责提供具体部署环境的元数据解析。

重试环协调器部署拓扑 (Ring-Coordinator Deployment Topologies)

根据系统的规模和性能 SLA 要求,重试环协调器支持多种灵活的部署拓扑结构:

内嵌式部署拓扑 (Embedded Ring-Coordinator Deployment)

在此拓扑中,Ring-Coordinator 的 Master 和 Slave 均内嵌在同一个物理服务实例内部运行。 非常适合中小型规模的部署。即便如此,由于底层基于 RSocket 非阻塞 Netty 反应式架构,单实例依然能轻松应对海量的 Orchestrator 实例。

内嵌共享模式 (Shared Mode Of Embedded Ring-Coordinator Deployment)

在此模式下,单个服务实例运行一组内嵌共享的 Ring-Coordinator Master 和 Slave,为同一物理区域内的多个微服务领域提供统一服务。 这种模式资源利用率最高,但各个领域之间存在一定的耦合度。 例如在 us-central 区域内有三个微服务领域(订单 Order、支付 Payment、库存 Inventory),系统部署单个共享服务实例 (Shared-Service),内嵌的 Master 和 Slave 同时为这三个业务领域提供服务。

架构图如下:

stacksaga diagram embedded ring coordinator shared mode

内嵌独立专用模式 (Dedicated Mode Of Embedded Ring-Coordinator Deployment)

在此模式下,每个微服务领域在自身的服务实例内部独立运行其专属的内嵌 Master 和 Slave。 这提供了极高的隔离性与故障容忍度,因为每个领域都拥有完全私有的协调器。 例如在 us-central 区域内有三个微服务领域(Order、Payment、Inventory),系统部署三个独立的服务实例(Order-Service、Payment-Service、Inventory-Service),各自内嵌专属的 Master 和 Slave。

架构图如下:

stacksaga diagram embedded ring coordinator dedicated mode

内嵌混合模式 (Hybrid Mode Of Embedded Ring-Coordinator Deployment)

在此模式下,部分核心微服务领域拥有自身专属的内嵌协调器实例,而其余领域则共享一组公共内嵌协调器。 这在运维部署中提供了极佳的灵活性:对隔离度要求高的核心业务独享资源,其余业务共享资源降低成本。 例如在 us-central 区域内,订单领域在 Order-Service 中独享内嵌协调器,而支付和库存领域则共享 Shared-Service 中的内嵌协调器。

架构图如下:

stacksaga diagram embedded ring coordinator hybrid mode

集群化部署拓扑 (Clustered Ring-Coordinator Deployment)

在此拓扑中,Master 和 Slave 实例作为独立的微服务分别独立部署,以实现更高的伸缩性与故障隔离。 该拓扑同样进一步细分为三种模式:

集群化共享模式 (Shared Mode Of Clustered Ring-Coordinator Deployment)

这是一种性价比更高的部署拓扑:在同一物理区域内,由一个统一的 Ring-Coordinator 集群(一个 Master 和多个 Slave)为多个微服务领域提供共享服务。 例如在 us-central 区域内,单个共享的协调器集群 (Shared-RC-Cluster) 同时服务于订单、支付和库存三个领域。

架构图如下:

stacksaga diagram clustered ring coordinator shared mode

集群化独立专用模式 (Dedicated Mode Of Clustered Ring-Coordinator Deployment)

这是解耦最为彻底的顶级生产拓扑:每个微服务领域在同一物理区域内都拥有完全独立的专属 Ring-Coordinator 集群。 这提供了最高等级的业务隔离与容灾能力。 例如在 us-central 区域内,订单、支付和库存三个领域各自拥有完全独立的集群(Order-RC-Cluster、Payment-RC-Cluster、Inventory-RC-Cluster),各自配备独立的 Master 和 Slave 节点。

架构图如下:

stacksaga diagram clustered ring coordinator dedicated mode

集群化混合模式 (Hybrid Mode Of Clustered Ring-Coordinator Deployment)

这是一种兼顾隔离性与资源成本的灵活拓扑:某些核心领域拥有专属集群,其余领域共享公共集群。 例如,高频核心的订单领域独享 Order-RC-Cluster,而支付与库存领域则共享 Shared-RC-Cluster。

架构图如下:

stacksaga diagram clustered ring coordinator hybrid mode

Kubernetes StatefulSet 集群化部署

在云原生容器化的 Kubernetes 环境中,官方强烈推荐将 Clustered Ring-Coordinator 作为 StatefulSet 运行。

StatefulSet 为每个副本提供了稳定的网络标识和确定性的递增序号索引(coordinator-0、coordinator-1、coordinator-2 等)。 StackSaga 原生利用这一特性实现了基于索引的自动化角色识别(stacksaga.coordinator.deploy.index-based=true):

  • 索引为 0 的副本(coordinator-0)自动作为 Master 节点引导启动。

  • 所有索引 > 0 的副本(coordinator-1、coordinator-2 等)自动作为 Slave 节点引导启动。

这一精妙设计使运维团队只需维护单一的 StatefulSet 清单和 ConfigMap 即可搞定整个协调器集群,免除了为 Master 和 Slave 分别编写并维护多套部署配置的繁琐开销。

禁用事务 (Disabled Transactions) 与终止事务 (Terminated Transactions)

禁用事务 (Disabled Transactions)

如果某些事务在事务生命周期 (Transaction Lifetime) 内经过多次重试后仍然超时过期,这些事务将被标记为*禁用事务 (Disabled Transactions)*。

导致事务未能完成并最终过期的原因有哪些?

  1. 在事务执行期间某些执行器 (Executors) 发生了变更。
    例如,假设某个事务在服务版本 1.0.0 中发起,由于目标服务不可用而在重试队列中保持了一段时间;在此期间如果部署了新版本,而新版本中删除了该执行器,那么该事务将无法被执行。 即使该事务被传递去重试,由于执行器不存在,事务也无法执行。 即使此时未找到执行器,系统仍会一次又一次地重新调度该事务进行重试,直到达到事务生命周期的终点。 生命周期结束后,该事务将被标记为禁用事务。

    即使未找到执行器仍重新调度事务的原因是,开发和运维团队能够识别问题并在生命周期结束前进行修复。 发生此类错误时,StackSaga 框架会在 StackSaga Trace Window (链路追踪窗口) 中发出问题通知。
  2. 未找到目标服务主机 (Target Service Host)。
    如果目标主机持续未找到,直到超出生命周期,该事务在生命周期结束后将被标记为禁用事务。

    此时系统不会通过 StackSaga Trace Window 发出警告,因为底层资源暂时不可用属于框架允许并处理的正常瞬态故障。

终止事务 (Terminated Transactions)

如果事务在回滚补偿 (Revert) 过程中失败,该事务将被标记为*终止事务 (Terminated Transactions)*。因为在 StackSaga 中,回滚补偿流程除抛出 RetryableException (可重试异常) 之外不允许失败。 终止事务与禁用事务的区别在于:终止事务会在未耗尽生命周期时就立即停止重试。

如何恢复禁用事务与终止事务?

禁用事务和终止事务的根本问题应由团队人工排查和定位。 修复缺陷并准备好重新执行这些事务后,您可以恢复事务以进行重试。

系统提供了 2 个端点用于处理禁用事务和终止事务:

  1. 获取禁用事务与终止事务 (Fetching the Disabled Transactions and Terminated Transactions)。
    您可以通过此端点拉取禁用事务和终止事务,用于分析错误和定位问题。

  2. 恢复禁用事务与终止事务 (Restoring the Disabled Transactions and Terminated Transactions)。
    您可以通过此端点延长事务的生命周期,从而恢复并重新触发这些事务的重试。

不同实现方式 (Implementation) 下的端点可能有所不同,请根据您所使用的具体实现查找对应的端点。