正在加载,请稍候…

Serverless Architecture: Patterns, Pitfalls, and Best Practices

Build robust serverless applications. Learn function composition, cold start mitigation, stateless design, cost optimization, and when serverless is the right choice.

Serverless Architecture Patterns

When to Use Serverless

Good fit:
  - Event-driven workloads (image processing, notifications)
  - Unpredictable traffic (variable load)
  - Batch processing jobs
  - Webhooks and integrations
  - Low-traffic APIs

Poor fit:
  - Long-running processes (>15 min)
  - High-traffic APIs (cost at scale)
  - Stateful applications
  - Latency-sensitive (cold starts)
  - GPU workloads

Function Composition Patterns

Event-Driven Pipeline

// S3 -> Lambda -> SQS -> Lambda -> DynamoDB
// Each function does one thing

// Image uploaded to S3
export const onImageUpload: S3Handler = async (event) => {
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name;
    const key = record.s3.object.key;

    // Send to processing queue (decouple)
    await sqs.sendMessage({
      QueueUrl: process.env.PROCESSING_QUEUE_URL!,
      MessageBody: JSON.stringify({ bucket, key }),
    }).promise();
  }
};

// Process from SQS
export const processImage: SQSHandler = async (event) => {
  for (const record of event.Records) {
    const { bucket, key } = JSON.parse(record.body);
    await resizeAndOptimize(bucket, key);
    await storeMetadata(bucket, key);
  }
};

Saga with Step Functions

{
  "Comment": "Order processing saga",
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:validateOrder",
      "Next": "ChargePayment",
      "Catch": [{ "ErrorEquals": ["ValidationError"], "Next": "OrderFailed" }]
    },
    "ChargePayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:chargePayment",
      "Next": "ReserveInventory",
      "Catch": [{ "ErrorEquals": ["PaymentFailed"], "Next": "OrderFailed" }]
    },
    "ReserveInventory": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123:function:reserveInventory",
      "Next": "OrderComplete"
    },
    "OrderComplete": { "Type": "Succeed" },
    "OrderFailed": { "Type": "Fail" }
  }
}

Cold Start Mitigation

// 1. Provisioned Concurrency (AWS)
// In serverless.yml:
// provisionedConcurrency: 5  # Always warm instances

// 2. Scheduled warmup
export const warmup: ScheduledHandler = async () => {
  // This function runs every 5 minutes to keep Lambda warm
  return { statusCode: 200, body: 'warmed' };
};

// 3. Minimize package size
// Use esbuild bundling, tree-shake unused modules
// Avoid importing entire AWS SDK - use v3 modular clients:
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// Not: import AWS from 'aws-sdk';

// 4. Keep initialization outside handler
const client = new DynamoDBClient({ region: 'us-east-1' }); // Reused across invocations
export const handler = async (event: APIGatewayEvent) => { /* ... */ };

Stateless Design Patterns

// Problem: Storing state in-memory (lost on cold start)
const cache = new Map<string, User>();  // Bad! Lost between invocations

// Solution 1: External cache
async function getUser(id: string): Promise<User> {
  return redis.getOrSet(`user:${id}`, () => db.findUser(id), 300);
}

// Solution 2: Pass state through events
export const handler = async (event: { userId: string; sessionToken: string }) => {
  // All needed state comes from the event
  const user = await validateToken(event.sessionToken);
  return processRequest(user, event);
};

// Solution 3: DynamoDB for distributed state
const session = await dynamodb.getItem({ TableName: 'sessions', Key: { id: { S: sessionId } } });

Cost Optimization

// Use ARM64 (Graviton) for ~20% cost savings
// serverless.yml: architecture: arm64

// Right-size memory (more memory = more CPU + cost)
// Profile actual memory usage:
export const handler = async () => {
  const used = process.memoryUsage().heapUsed / 1024 / 1024;
  console.log(`Memory used: ${Math.round(used)}MB`);
  // Set Lambda memory to ~1.5x actual usage
};

// Batch SQS messages to reduce invocations
// SQS trigger with batch size 10:
export const batchHandler: SQSHandler = async (event) => {
  await Promise.all(event.Records.map(r => processRecord(JSON.parse(r.body))));
  // 1 invocation processes 10 messages instead of 10 invocations
};

Serverless shines for event-driven, variable workloads but requires rethinking traditional architectures.