关于 serverless-development
面向构建 API、事件处理器和定时任务的开发者,提供无服务器应用设计与运维指南。内容涵盖平台模式、延迟、数据库连接、部署、监控和成本优化。
主要功能
- 01AWS Lambda 与 SAM 模式
- 02Vercel 和 Cloudflare 函数模式
- 03减少冷启动
- 04数据库连接池管理
- 05部署与成本指导
适用场景
- 01构建无服务器 API 和 Webhook
- 02处理后台任务和定时任务
- 03降低无服务器端点延迟
- 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、后台处理、定时任务,或任何适合自动扩缩、按使用量付费且无需管理基础设施的工作负载时,请使用此技能。
核心原则
- 按无状态设计 - 每次函数调用都是独立的。切勿在不同调用之间将状态存储在全局变量中(但可以复用连接)。使用外部状态存储(数据库、缓存、队列)进行持久化。
- 尽量减少冷启动 - 冷启动会在首次调用时增加 100 毫秒至 10 秒的延迟。保持程序包精简,使用轻量级运行时,并考虑为对延迟敏感的路径启用预置并发。
- 连接管理至关重要 - 无服务器函数可能会生成数千个并发实例。如果每个实例都各自打开数据库连接,就会耗尽连接池。使用连接池(RDS Proxy、Prisma Data Proxy、PgBouncer)。
- 按调用付费的思维方式 - 你需要为执行时间和内存付费。优化热点路径,选择合适的内存大小(更多内存 = 更多 CPU = 可能更快 = 有时更便宜),并尽可能批量处理操作。
- 接受限制 - 函数超时、负载大小限制和并发限制是特性,不是缺陷。围绕这些限制进行设计:使用队列处理长时间任务,使用 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 模式)