mcpField
S
serverless-development

serverless-development

github.com/travisjneuman云与 DevOps106127 次安装1 天前更新
代码仓库

关于 serverless-development

面向构建 API、事件处理器和定时任务的开发者,提供无服务器应用设计与运维指南。内容涵盖平台模式、延迟、数据库连接、部署、监控和成本优化。

主要功能

  1. 01AWS Lambda 与 SAM 模式
  2. 02Vercel 和 Cloudflare 函数模式
  3. 03减少冷启动
  4. 04数据库连接池管理
  5. 05部署与成本指导

适用场景

  1. 01构建无服务器 API 和 Webhook
  2. 02处理后台任务和定时任务
  3. 03降低无服务器端点延迟
  4. 04管理并发场景下的数据库访问

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/travisjneuman/.claude --skill serverless-development

安装后在 Claude Code 中输入/serverless-development确认已加载。

详细介绍

无服务器开发

概述

此技能涵盖在无服务器计算平台上构建应用,这类平台将基础设施管理抽象化。内容涉及 AWS Lambda(Node.js、Python、Go 运行时)、Vercel Edge Functions 和 Serverless Functions、Cloudflare Workers、冷启动优化、无服务器设计模式(扇出、步骤函数、事件驱动)、数据库连接管理、使用 SAM/SST/Serverless Framework 部署、监控和成本优化。

在构建 API、Webhook、后台处理、定时任务,或任何适合自动扩缩、按使用量付费且无需管理基础设施的工作负载时,请使用此技能。


核心原则

  1. 按无状态设计 - 每次函数调用都是独立的。切勿在不同调用之间将状态存储在全局变量中(但可以复用连接)。使用外部状态存储(数据库、缓存、队列)进行持久化。
  2. 尽量减少冷启动 - 冷启动会在首次调用时增加 100 毫秒至 10 秒的延迟。保持程序包精简,使用轻量级运行时,并考虑为对延迟敏感的路径启用预置并发。
  3. 连接管理至关重要 - 无服务器函数可能会生成数千个并发实例。如果每个实例都各自打开数据库连接,就会耗尽连接池。使用连接池(RDS Proxy、Prisma Data Proxy、PgBouncer)。
  4. 按调用付费的思维方式 - 你需要为执行时间和内存付费。优化热点路径,选择合适的内存大小(更多内存 = 更多 CPU = 可能更快 = 有时更便宜),并尽可能批量处理操作。
  5. 接受限制 - 函数超时、负载大小限制和并发限制是特性,不是缺陷。围绕这些限制进行设计:使用队列处理长时间任务,使用 S3 存储大型负载,并使用步骤函数实现复杂编排。

关键模式

模式 1:使用 TypeScript 的 AWS Lambda

适用场景: 在 AWS 上构建后端 API、事件处理和定时任务。

实现:

// handler.ts - Lambda handler with proper typing
import {
  APIGatewayProxyEventV2,
  APIGatewayProxyResultV2,
  SQSEvent,
  ScheduledEvent,
  Context,
} from "aws-lambda";

// Reusable across invocations (connection reuse)
let dbConnection: PrismaClient | null = null;

function getDb(): PrismaClient {
  if (!dbConnection) {
    dbConnection = new PrismaClient({
      datasources: {
        db: { url: process.env.DATABASE_URL },
      },
    });
  }
  return dbConnection;
}

// API Gateway handler
export async function apiHandler(
  event: APIGatewayProxyEventV2,
  context: Context
): Promise<APIGatewayProxyResultV2> {
  // Don't wait for event loop to drain (for connection reuse)
  context.callbackWaitsForEmptyEventLoop = false;

  try {
    const method = event.requestContext.http.method;
    const path = event.rawPath;

    if (method === "GET" && path === "/api/users") {
      const db = getDb();
      const users = await db.user.findMany({ take: 50 });
      return response(200, { users });
    }

    if (method === "POST" && path === "/api/users") {
      const body = JSON.parse(event.body ?? "{}");
      const db = getDb();
      const user = await db.user.create({ data: body });
      return response(201, { user });
    }

    return response(404, { error: "Not found" });
  } catch (error) {
    console.error("Handler error:", error);
    return response(500, { error: "Internal server error" });
  }
}

// SQS event handler (batch processing)
export async function sqsHandler(event: SQSEvent): Promise<{ batchItemFailures: Array<{ itemIdentifier: string }> }> {
  const failures: string[] = [];

  for (const record of event.Records) {
    try {
      const body = JSON.parse(record.body);
      await processMessage(body);
    } catch (error) {
      console.error(`Failed to process message ${record.messageId}:`, error);
      failures.push(record.messageId);
    }
  }

  // Partial batch failure reporting (only retry failed messages)
  return {
    batchItemFailures: failures.map((id) => ({ itemIdentifier: id })),
  };
}

// Scheduled event handler (cron)
export async function cronHandler(event: ScheduledEvent): Promise<void> {
  console.log("Running scheduled task at:", event.time);
  const db = getDb();

  // Clean up expired sessions
  const deleted = await db.session.deleteMany({
    where: { expiresAt: { lt: new Date() } },
  });

  console.log(`Cleaned up ${deleted.count} expired sessions`);
}

function response(statusCode: number, body: Record<string, unknown>): APIGatewayProxyResultV2 {
  return {
    statusCode,
    headers: {
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": process.env.ALLOWED_ORIGIN ?? "*",
    },
    body: JSON.stringify(body),
  };
}
# template.yaml - AWS SAM template
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Runtime: nodejs20.x
    Timeout: 30
    MemorySize: 256
    Environment:
      Variables:
        DATABASE_URL: !Ref DatabaseUrl
        NODE_OPTIONS: "--enable-source-maps"

Resources:
  ApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: dist/handler.apiHandler
      Events:
        Api:
          Type: HttpApi
          Properties:
            Path: /api/{proxy+}
            Method: ANY
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2022
        Sourcemap: true
        EntryPoints:
          - src/handler.ts

  QueueProcessor:
    Type: AWS::Serverless::Function
    Properties:
      Handler: dist/handler.sqsHandler
      Events:
        Queue:
          Type: SQS
          Properties:
            Queue: !GetAtt ProcessingQueue.Arn
            BatchSize: 10
            FunctionResponseTypes:
              - ReportBatchItemFailures

  ScheduledCleanup:
    Type: AWS::Serverless::Function
    Properties:
      Handler: dist/handler.cronHandler
      Events:
        Schedule:
          Type: Schedule
          Properties:
            Schedule: rate(1 hour)

  ProcessingQueue:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 180
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt DLQ.Arn
        maxReceiveCount: 3

  DLQ:
    Type: AWS::SQS::Queue

原因: AWS Lambda 配合 SAM 可提供基础设施即代码、本地测试(sam local invoke)和自动部署。通过模块级变量复用连接,可以避免每次调用都重新连接造成的冷启动开销。SQS 的部分批次失败报告可确保只重试失败的消息。


模式 2:Vercel Edge Functions

适用场景: 需要在全球范围内靠近用户运行的低延迟 API 路由和中间件。

实现:

// app/api/geo/route.ts - Edge Function
export const runtime = "edge"; // Run on Vercel Edge Network

export async function GET(request: Request) {
  // Access geo information (available at the edge)
  const country = request.headers.get("x-vercel-ip-country") ?? "US";
  const city = request.headers.get("x-vercel-ip-city") ?? "Unknown";

  // Edge-compatible KV store
  const cachedData = await kv.get(`content:${country}`);
  if (cachedData) {
    return Response.json(cachedData);
  }

  // Lightweight processing at the edge
  const content = await getLocalizedContent(country);
  await kv.set(`content:${country}`, content, { ex: 3600 });

  return Response.json(content, {
    headers: {
      "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
    },
  });
}
// middleware.ts - Edge Middleware for auth, redirects, A/B testing
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export const config = {
  matcher: ["/dashboard/:path*", "/api/:path*"],
};

export function middleware(request: NextRequest) {
  // Auth check at the edge (fast, runs before serverless function)
  const token = request.cookies.get("session_token");

  if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  // A/B test assignment at the edge
  const abCookie = request.cookies.get("ab-pricing");
  if (!abCookie && request.nextUrl.pathname === "/pricing") {
    const variant = Math.random() < 0.5 ? "control" : "new-layout";
    const response = NextResponse.next();
    response.cookies.set("ab-pricing", variant, { maxAge: 60 * 60 * 24 * 30 });

    // Rewrite to variant-specific page
    if (variant === "new-layout") {
      return NextResponse.rewrite(new URL("/pricing-v2", request.url));
    }
    return response;
  }

  return NextResponse.next();
}

原因: Edge Functions 在全球 30 多个位置运行,冷启动时间低于 1 毫秒(使用 V8 隔离环境,而非容器)。它们非常适合身份验证检查、地理路由、A/B 测试,以及能从靠近用户的位置获得响应优势的 API。代价是运行时受限(不支持 Node.js API 和原生模块,内存也更少)。


模式 3:冷启动优化

适用场景: 对延迟敏感的端点无法接受冷启动时间时。

实现:

// 1. Minimize bundle size with esbuild tree-shaking
// esbuild.config.mjs
import { build } from "esbuild";

await build({
  entryPoints: ["src/handler.ts"],
  bundle: true,
  minify: true,
  platform: "node",
  target: "node20",
  outdir: "dist",
  external: [
    // Don't bundle the AWS SDK (already available in Lambda)
    "@aws-sdk/*",
  ],
  // Tree-shake unused code
  treeShaking: true,
  // Source maps for debugging
  sourcemap: true,
});
// 2. Lazy-load heavy dependencies
let _sharp: typeof import("sharp") | null = null;

async function getSharp() {
  if (!_sharp) {
    _sharp = await import("sharp");
  }
  return _sharp;
}

// Only load sharp when actually processing images
export async function imageHandler(event: APIGatewayProxyEventV2) {
  if (event.rawPath.includes("/resize")) {
    const sharp = await getSharp();
    // Use sharp...
  }
}
// 3. Provisioned concurrency for critical paths
// In SAM template:
// Properties:
//   ProvisionedConcurrencyConfig:
//     ProvisionedConcurrentExecutions: 5

// 4. Keep-alive with scheduled warming (cheaper than provisioned concurrency)
// CloudWatch rule: rate(5 minutes) -> Lambda with warming event
export async function handler(event: unknown) {
  // Check if this is a warming invocation
  if ((event as Record<string, unknown>).source === "serverless-warming") {
    console.log("Warming invocation, returning early");
    return { statusCode: 200, body: "warm" };
  }

  // Normal handler logic...
}

原因: 冷启动是无服务器环境中主要的延迟问题。程序包越小,初始化越快。延迟加载会将沉重的导入开销推迟到实际需要时。对于关键路径,预置并发可以彻底消除冷启动(但需要付费)。通过预热调用,可以让函数实例保持热状态,同时无需为预置并发付费。


模式 4:数据库连接池

适用场景: 任何连接关系型数据库的无服务器函数。

实现:

// prisma/schema.prisma - Use Prisma Accelerate or Data Proxy for serverless
datasource db {
  provider = "postgresql"
  // Direct connection for migrations
  url      = env("DATABASE_URL")
  // Pooled connection for serverless runtime
  directUrl = env("DIRECT_DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}
// For AWS Lambda: Use RDS Proxy
// DATABASE_URL = "postgresql://user:[email protected]:5432/mydb"

// For Vercel/Cloudflare: Use Neon serverless driver or Prisma Accelerate
import { Pool, neonConfig } from "@neondatabase/serverless";
import { PrismaNeon } from "@prisma/adapter-neon";
import { PrismaClient } from "@prisma/client";
import ws from "ws";

// Required for Node.js environments
neonConfig.webSocketConstructor = ws;

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaNeon(pool);
const prisma = new PrismaClient({ adapter });

// Connection reuse pattern
let client: PrismaClient | null = null;

export function getClient(): PrismaClient {
  if (!client) {
    client = new PrismaClient({
      log: process.env.NODE_ENV === "development" ? ["query"] : [],
    });
  }
  return client;
}

原因: 每次 Lambda 调用都可能创建新的数据库连接。数千次并发调用会耗尽数据库连接上限(托管数据库通常为 100 至 500)。连接池(RDS Proxy、PgBouncer、Neon serverless、Prisma Accelerate)可将许多无服务器连接复用到更少的数据库连接上。


成本优化参考

策略节省幅度权衡
合理配置内存20-40%需要进行基准测试
ARM64(Graviton)20%兼容性问题较小
预置并发(相较于过度配置的服务)相较于始终运行可节省 50-70%扩容时延迟较高
预留并发防止成本失控限制吞吐量
批处理(SQS 批次大小)50-80%延迟较高
分层存储(S3 生命周期)30-60%冷存储层的访问延迟较高

反模式

反模式不好的原因更好的做法
每次调用都打开数据库连接耗尽连接池在模块级别复用连接,并使用 RDS Proxy
打包整个 node_modules冷启动时间过长(5-10 秒)使用 esbuild 进行 Tree Shaking,并将 AWS SDK 外置
在全局变量中存储状态调用之间状态会丢失(不可靠)使用 DynamoDB、Redis 或 S3 存储状态
为 API 处理程序设置 15 分钟超时让用户等 15 分钟?API 设置为 30 秒;长任务使用 Step Functions
不限制并发成本失控,下游过载为每个函数设置预留并发
同步扇出速度慢,有超时风险使用 SQS/SNS 扇出,并异步处理
使用没有结构的 console.log无法在 CloudWatch 中搜索使用带关联 ID 的结构化 JSON 日志

检查清单

  • 已尽可能减小打包体积(使用 esbuild Tree Shaking,并将 AWS SDK 外置)
  • 已对数据库连接进行连接池管理(RDS Proxy、Neon、Prisma Accelerate)
  • 已测量冷启动时间,且对当前使用场景而言可以接受
  • 已为延迟敏感路径配置预置并发
  • 已对内存大小进行基准测试并合理配置
  • 已启用 SQS 部分批次失败报告
  • 已为异步事件源配置死信队列
  • 已设置预留并发,以防止成本失控
  • 已使用包含请求 ID/关联 ID 的结构化日志
  • 已合理设置超时时间(API 为 30 秒,批处理可更长)
  • 所有配置均使用环境变量(没有硬编码值)
  • 已选择 ARM64/Graviton 运行时以节省成本

相关资源

  • 技能: event-driven-architecture(SQS/SNS 模式)、monitoring-observability(Lambda 跟踪)
  • 技能: performance-engineering(冷启动对延迟的影响)
  • 规则: docs/reference/stacks/python.md(Python Lambda 模式)

评论

支持 Markdown