架构 (IT)系统架构师

你如何为分布式预订系统设计一个容错的 saga 协调模式,以便在独立子域之间补偿长时间运行的事务,同时确保在重复请求场景中具有幂等性?

用 Hintsage AI 助手通过面试

答案

使用 TemporalNetflix Conductor 实现一个集中式 Saga Orchestrator,在 PostgreSQL 中维护耐用的工作流状态,并通过 gRPC 通信与域服务交互。该模式要求将幂等性密钥存储在具有匹配业务约束的 TTL 窗口的 Redis Cluster 中,同时 Apache Kafka 作为审计日志和补偿触发器的事件骨干。每个 saga 步骤必须包括补偿事务,使用 Saga State Machine 模式执行反向操作,并在 etcdZooKeeper 中跟踪明确的状态(PENDING、SUCCEEDED、COMPENSATING、COMPENSATED)以进行集群协调。

┌─────────────────┐     ┌──────────────┐     ┌─────────────────┐
│   API 网关      │────▶│   Temporal   │────▶│   库存         │
└─────────────────┘     │  Orchestrator│     │   服务          │
                        └──────────────┘     └─────────────────┘
                               │                        │
                               ▼                        ▼
                        ┌──────────────┐          ┌─────────────────┐
                        │  PostgreSQL  │          │   PostgreSQL    │
                        │  状态存储   │          │   (补偿逻辑) │
                        └──────────────┘          └─────────────────┘

生活中的情况

一个全球酒店预订平台在协调房间预订、支付处理和积分更新时遇到了级联故障,涉及三个不同区域的 Kubernetes 集群。其遗留实现使用 Two-Phase Commit (2PC) 通过 REST API,导致在支付网关出现延迟高于 10 秒的高峰流量时的大规模死锁。

团队评估了使用 Amazon EventBridgeChoreography-Based Saga,每个服务将域事件发布到共享总线。这种方法消除了单点故障,减少了 40% 的基础设施成本。然而,它引入了严重的可观察性挑战,因为确定复杂的多房间预订是否成功需要查询跨越十七个微服务的日志。隐式依赖使得执行一致的超时策略变得不可能,调试生产问题变成了跨越多个 AWS CloudWatch 仪表板的法医学练习。

他们原型化了一个使用自定义 Node.js 协调器的 Orchestrated Saga,该协调器部署在 AWS ECS 上。这使得业务逻辑集中化,简化了通过统一的 Grafana 仪表板进行监控。不幸的是,初始实现仅将工作流状态存储在内存中,导致在部署期间协调器重启时发生灾难性的数据丢失。三十个事务进入未知状态,导致手动数据库调和耗时三天,并导致双重收费客户的显著收入损失。

所选择的解决方案部署 Temporal 作为工作流引擎,并使用 Cassandra 进行持久化,确保在 Pod 重启和 AZ 故障之间的状态耐久性。架构使用 Protobuf 方案进行协调器和域服务之间的类型安全通信,使用 Redis Sentinel 管理幂等性密钥。当支付服务在 us-east-1 发生区域性故障时,saga 自动触发补偿工作流,在 200 毫秒内释放房间保留并原子性撤销积分。

该系统现在每日处理 50,000 个复杂预订,具有 99.99% 的一致性保证,并且在网络分区期间没有手动干预。故障检测的平均时间 (MTTD) 从 45 分钟下降到 8 秒,而补偿延迟在 p99 下保持在 500 毫秒以内。

候选人常常忽视的问题

当补偿事务本身失败时,你如何处理部分补偿失败,可能会导致系统处于不一致状态?

使用 Event Sourcing 实现 Compensation Audit Log,每个尝试的补偿作为不可变事件记录在 Apache Kafka 中,保留无限期。系统必须区分需自动重试的瞬态基础设施故障和需要人工干预的业务逻辑违规。对于瞬态问题,使用 RabbitMQAmazon SQS 中的 Dead Letter Queues (DLQ),在服务恢复后进行补偿处理,带有颠簸以防止雷鸣效应。对于业务规则违规,例如尝试退款已经结算的交易,saga 进入 'COMPENSATION_FAILED' 状态,触发 PagerDuty 警报,同时应用 CQRS 模式通过命令模型冻结聚合根。始终设计补偿为幂等的,使用数据库唯一约束或 Redis SETNX 操作,以确保重试不会产生副作用。

关于时间耦合和回答“当前事务状态是什么”查询的能力,编排和编排之间的根本架构差异是什么?

编排 遵循 反应宣言,创建时间解耦,服务对事件作出反应,而不知道上游或下游参与者,但牺牲了在没有建立复杂的 Distributed Tracing 的情况下查询 saga 状态的能力,使用 JaegerAWS X-Ray。状态由事件日志生成,要求 CQRS 读取模型投影来回答“预订是否完成”的问题。编排 在协调器和工作者之间引入显式时间耦合,因为协调器必须可用以触发下一步,但在其状态存储(PostgreSQL/CockroachDB)中提供了单一的真实来源。这允许立即状态查询,但会创建网络依赖性。关键见解是编排要求在每个消费者中实现状态机,而编排集中化了这种复杂性;对于需要强审计性和合规性 (PCI-DSS) 的系统,尽管存在耦合成本,编排仍然是首选。

在使用至少一次交付语义的消息代理中,在 Kafka 消费者重新平衡或 Kubernetes Pod 重启期间,如何防止重复的 saga 执行?

使用 RedisMemcached 实现 Idempotent Consumer 模式,以存储处理过的消息 ID,去重窗口与您的 Recovery Point Objective (RPO) 匹配,通常为 24-48 小时,适用于金融系统。当 saga 协调器接收命令时,通过对相关 ID 与业务密钥(客户 ID + 预订参考)进行哈希生成确定性幂等性密钥,然后再执行任何副作用。每个域服务必须根据其 Idempotency Store 验证此密钥,该存储作为具有复合键唯一约束的 PostgreSQL 表实现,或使用 Redis 中的 Bloom Filters 进行内存效率较高的负面查找。对于长时间运行的 saga,使用 Saga State Machines 通过 etcd 版本向量实现乐观锁定,以在分布式节点之间处理确切一次的处理语义。这防止在部署期间消费者组重新平衡或触发 Kubernetes livenessProbe 重启的情况下双重预订场景。