AWS us-east-1 宕机教会我关于构建弹性系统的经验

发布: (2025年12月15日 GMT+8 03:45)
6 min read
原文: Dev.to

Source: Dev.to

AWS us‑east-1 将再次宕机。当它宕机时,你的系统能否存活?
上个周末,我构建了一个能够在此类宕机中生存的系统。

在 Surfline 工作八年,构建订阅基础设施——通过 Stripe、Apple 和 Google Play 处理支付——我认识到真正的问题不是 云提供商是否会失败,而是 当它失败时你的架构会如何退化

我花了四个小时实现了三个可靠性模式,这些模式来源于 AWS Builders’ Library、Google SRE 实践以及 Stripe 的工程博客。以下是关键要点。

为什么弹性很重要

当 AWS 发生事故时,常见的故障模式包括:

  • Lambda 函数超时
  • DynamoDB 调用失败或被限流
  • SQS 队列积压

对大多数应用来说,用户只会看到错误页面并稍后重试。支付系统则不同:

  • 一次失败的扣费实际上可能已经成功。
  • 重试可能导致对客户的双重扣费。
  • 大量重试的“惊群效应”会使故障进一步蔓延。

因此我们需要能够在部分失败时仍然保持金钱和信任不受损失的模式。

模式 1:带抖动的重试

AWS Builders’ Library 中关于 Timeouts, retries, and backoff with jitter 的文章改变了我对重试逻辑的看法。没有抖动时,所有客户端会在完全相同的间隔重试,形成同步波,冲击正在恢复的服务。

// Full jitter formula from AWS Builders' Library
const calculateDelay = (attempt: number): number => {
  const exponentialDelay = Math.min(
    MAX_DELAY,
    INITIAL_DELAY * Math.pow(2, attempt)
  );
  // Full jitter: random value between 0 and exponential delay
  return Math.random() * exponentialDelay;
};

结果: 在负载测试中,成功率从约 70 % 提升到 >99 %,因为抖动将重试负载均匀分布在时间轴上,而不是产生尖峰。

适用场景

  • Lambda 在限流时重试 DynamoDB
  • ECS 任务通过 NAT 网关调用外部 API
  • Step Functions 对服务集成使用重试策略

模式 2:有界队列 + 工作池

单独的有界队列 并不能 限制并发处理。在一次测试中,我将队列容量设为 100,发送 200 个请求,原本预期会有约 100 个被拒绝——结果却是零,因为 Node.js 处理请求的速度快于它们的积累。

// What you actually need: queue + worker pool
class BoundedQueue {
  private queue: Request[] = [];
  private readonly capacity = 100;

  enqueue(request: Request): boolean {
    if (this.queue.length >= this.capacity) {
      return false; // HTTP 429 – fail fast
    }
    this.queue.push(request);
    return true;
  }
}
class WorkerPool {
  private activeWorkers = 0;
  private readonly maxWorkers = 10; // THIS controls throughput

  async process(queue: BoundedQueue) {
    while (this.activeWorkers < this.maxWorkers && queueHasWork()) {
      this.activeWorkers++;
      // process a single request...
      this.activeWorkers--;
    }
  }
}

幂等性处理

class IdempotencyHandler {
  private inFlight = new Set<string>();
  private cache = new Map<string, { response: any; ttl: number }>();

  async process(idempotencyKey: string, operation: () => Promise<any>) {
    // Check cache first
    const cached = this.cache.get(idempotencyKey);
    if (cached) return cached.response;

    // Detect concurrent duplicates
    if (this.inFlight.has(idempotencyKey)) {
      throw new ConflictError('Request already in progress');
    }

    this.inFlight.add(idempotencyKey);
    try {
      const response = await operation();
      // Only cache successes
      if (response.success) {
        this.cache.set(idempotencyKey, {
          response,
          ttl: Date.now() + 24 * 60 * 60 * 1000,
        });
      }
      return response;
    } finally {
      this.inFlight.delete(idempotencyKey);
    }
  }
}

存储选项

  • DynamoDB:使用带 TTL 的条件写入实现自动清理
  • Lambda Powertools:内置的幂等性工具,基于 DynamoDB
  • Step Functions:通过执行名称实现原生幂等性
// DynamoDB idempotency pattern
await dynamodb.put({
  TableName: 'IdempotencyStore',
  Item: {
    idempotencyKey: key,
    response: result,
    ttl: Math.floor(Date.now() / 1000) + 86400 // 24 h
  },
  ConditionExpression: 'attribute_not_exists(idempotencyKey)'
});

综合应用

下面是一个在 AWS 上实现弹性支付处理流水线的高层架构示意图:

┌─────────────────────────────────────────────────────────────┐
│                     API Gateway (Rate Limiting)            │
└─────────────────────┬───────────────────────────────────────┘

┌─────────────────────▼───────────────────────────────────────┐
│                      SQS Queue (Bounded Buffer)            │
└─────────────────────┬───────────────────────────────────────┘

┌─────────────────────▼───────────────────────────────────────┐
│          Lambda (Reserved Concurrency = 10) – Worker Pool   │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ 1. Check DynamoDB idempotency store                     ││
│  │ 2. Process payment with retry + jitter                  ││
│  │ 3. Store result in DynamoDB                             ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────┬───────────────────────────────────────┘

┌─────────────────────▼───────────────────────────────────────┐
│                 DynamoDB Tables                            │
│  - IdempotencyStore (TTL)                                   │
│  - ProcessingResults                                        │
└─────────────────────────────────────────────────────────────┘

接下来要做的事

resilient‑relay 仓库中包含完整实现。计划中的改进:

  • 对失败支付的死信队列处理
  • 用于 RED(Rate, Errors, Duration)可观测性的 CloudWatch 指标
  • 多区域故障转移模式

当 us‑east‑1 再次宕机——它一定会——你的系统应该是“优雅降级”,而不是“灾难性崩溃”。

AWS Builders’ Library 的存在,是因为亚马逊在运营 AWS 本身时已经吸取了这些教训。仅仅抖动那篇文章就值得你花时间阅读。

行动号召

你在 AWS 架构中实现了哪些可靠性模式?我很想听听哪些在生产环境中奏效——或是哪些惨败的经验。

Back to Blog

相关文章

阅读更多 »