PAYATHON 2026

如何为自定义 REST API 实现按使用量计费?

支付老李

结论

可以。比较合适的做法是采用 Serverless 架构:用 Amazon API Gateway 接收 REST 请求,由 AWS Lambda 处理业务逻辑,再按需接入 DynamoDB、SES 或其他托管服务。

这类服务主要按请求次数、执行时间和资源用量收费,不需要让 EC2 实例长期运行。没有请求时,API Gateway 和 Lambda 通常不会产生计算费用,但日志、数据库、邮件和域名等配套服务仍可能收费。

典型调用链如下:

CloudFront / Browser
        |
        v
Amazon API Gateway
        |
        v
AWS Lambda
        |
        +--> DynamoDB:保存邀请记录
        |
        +--> Amazon SES:发送邀请邮件

为什么不必常驻 Node.js 或 Sinatra 服务

Node.js 和 Sinatra 是应用框架,并不决定服务如何计费。如果把它们部署在 EC2、传统 VPS 或常驻容器中,进程需要一直运行。即使没有请求,也要支付实例占用时间产生的费用。

邀请 API 通常有这些特点:

  • 请求量不稳定;
  • 单次处理时间较短;
  • 不需要常驻进程;
  • 业务容易拆成独立函数;
  • 可以接受首次调用稍慢。

这些条件适合使用函数计算。收到请求后,Lambda 会启动处理逻辑,执行结束后释放运行环境,因此不必预先购买一台全年运行的服务器。

不过,按使用量计费不等于费用一定为零。账单可能包含:

  • API Gateway 请求和数据传输;
  • Lambda 调用次数、执行时间和分配内存;
  • CloudWatch Logs 日志存储;
  • DynamoDB 读写和存储;
  • SES 邮件发送;
  • 自定义域名、DNS、WAF 或 NAT Gateway;
  • 第三方邮件服务费用。

单价和免费额度会随区域、时间及产品配置变化,应以部署区域的官方价格页面为准。

实现步骤

1. 定义 API

例如创建以下接口:

POST /invites
Content-Type: application/json
Authorization: Bearer <token>

请求体:

{
  "email": "friend@example.com"
}

成功响应:

{
  "inviteId": "b91ce78e-0bd2-4dd8-8ac7-f764cb03ec43",
  "status": "pending"
}

公开 API 至少要验证调用者身份。不能只在前端隐藏接口地址,否则任何人都可能批量调用接口发送垃圾邮件。

2. 编写 Lambda 函数

下面是一个精简的 Node.js 示例,用于校验输入、生成邀请令牌并写入 DynamoDB。示例使用 AWS SDK for JavaScript v3。所选 Lambda 运行时是否预装相关模块,应以对应的运行时说明为准。更稳妥的方式是把依赖放进部署包一起发布。

import crypto from "node:crypto";
import {
  DynamoDBClient,
  PutItemCommand
} from "@aws-sdk/client-dynamodb";

const dynamodb = new DynamoDBClient({});
const tableName = process.env.INVITES_TABLE;

export const handler = async (event) => {
  let body;

  try {
    body = JSON.parse(event.body ?? "{}");
  } catch {
    return response(400, { error: "Invalid JSON body" });
  }

  const email =
    typeof body.email === "string"
      ? body.email.trim().toLowerCase()
      : "";

  if (!isValidEmail(email)) {
    return response(400, { error: "Invalid email address" });
  }

  // userId 应来自经过验证的 JWT 或 API Gateway authorizer,
  // 不应直接相信客户端提交的邀请人身份。
  const inviterId = event.requestContext?.authorizer?.jwt?.claims?.sub;

  if (!inviterId) {
    return response(401, { error: "Unauthorized" });
  }

  const inviteId = crypto.randomUUID();
  const token = crypto.randomBytes(32).toString("base64url");
  const createdAt = new Date().toISOString();

  await dynamodb.send(
    new PutItemCommand({
      TableName: tableName,
      Item: {
        inviteId: { S: inviteId },
        inviterId: { S: inviterId },
        email: { S: email },
        tokenHash: {
          S: crypto.createHash("sha256").update(token).digest("hex")
        },
        status: { S: "pending" },
        createdAt: { S: createdAt }
      },
      ConditionExpression: "attribute_not_exists(inviteId)"
    })
  );

  // 可在此直接调用 SES,也可以将发送任务写入队列后异步处理。
  // 邀请链接中的原始 token 只发送给收件人,数据库仅保存其哈希值。

  return response(201, {
    inviteId,
    status: "pending"
  });
};

function isValidEmail(value) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}

function response(statusCode, body) {
  return {
    statusCode,
    headers: {
      "content-type": "application/json; charset=utf-8"
    },
    body: JSON.stringify(body)
  };
}

在生产环境中,不宜完全依靠正则表达式判断电子邮件地址是否合法。完成基本格式校验后,还应发送确认邮件来验证地址归属。

3. 配置 API Gateway

把 POST /invites 路由连接到 Lambda,并完成以下配置:

  • JWT authorizer、Cognito authorizer 或其他认证方式;
  • 请求体大小限制;
  • CORS,只允许需要访问 API 的站点来源;
  • 节流和请求配额;
  • 统一的错误响应;
  • 必要时启用 AWS WAF;
  • 分开设置生产环境和测试环境。

如果只需要简单的 HTTP 路由,可以对比 API Gateway 的 HTTP API 和 REST API。两者的功能及价格模型不同。是否需要高级鉴权、请求转换、API Key 和使用计划等能力,会影响具体选择。

4. 授予最小权限

Lambda 的执行角色只应获得完成任务所需的权限。例如,只允许它向指定的邀请表写入数据:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:PutItem"
      ],
      "Resource": "arn:aws:dynamodb:REGION:ACCOUNT_ID:table/Invites"
    }
  ]
}

如果函数还要发送邮件,可以单独添加对应的 SES 权限,并限制可使用的资源和操作。不要为了省事授予 dynamodb:* 或 ses:*。

5. 部署并测试

部署后,可以用 curl 测试:

curl -X POST "https://api.example.com/invites" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"email":"friend@example.com"}'

测试至少应覆盖以下情况:

  • 合法邀请;
  • 未登录访问;
  • 无效 JSON;
  • 缺少邮箱或邮箱格式错误;
  • 重复邀请;
  • 超过速率限制;
  • DynamoDB 或邮件服务暂时失败;
  • 同一个请求被重复提交。

重复请求与异步发送

浏览器重试、网络超时或用户重复点击,都可能让邀请接口创建多条记录。可以要求客户端提供幂等键:

Idempotency-Key: 5d673dde-3d3f-480d-8998-c7d29bcd7682

服务端保存这个键,并通过 DynamoDB 条件写入,保证同一个键只执行一次。另一种方式是用“邀请人 ID + 收件邮箱 + 有效期”建立唯一约束。

邮件发送最好与邀请记录的创建分开处理:

POST /invites
      |
      v
保存邀请记录
      |
      v
写入 SQS
      |
      v
邮件发送 Lambda
      |
      v
Amazon SES

邮件服务短暂失败时,队列可以安排重试,API 也不必一直等待。接入队列后,要配置最大重试次数和死信队列,防止永久失败的消息反复执行并持续产生费用。

成本与安全注意事项

防止低流量 API 被恶意刷出高账单

按请求计费可以减少空闲成本,但异常流量也会直接推高账单。建议至少采取以下措施:

  • 设置 API Gateway 节流;
  • 按用户限制邀请频率;
  • 设置每日邀请数量上限;
  • 配置 AWS Budgets 和账单告警;
  • 配置 CloudWatch 指标和异常告警;
  • 必要时使用 CAPTCHA 或 WAF;
  • 设置邮件服务发送额度并处理退信。

预算告警通常只会发送通知,不会自动停止消费。如果需要强制停用服务,应另行设计自动化控制,并评估误停生产服务的风险。

避免隐藏的固定成本

只使用 API Gateway、Lambda 和 DynamoDB 按需模式时,可以很接近“无请求、低成本”。但以下组件可能持续产生费用:

  • NAT Gateway;
  • 预置并发;
  • 长期保留的大量日志;
  • 固定容量数据库;
  • 第三方监控套餐;
  • Route 53 托管区域和自定义域名相关资源。

如果 Lambda 需要访问公网,但没有必须接入私有子网的理由,不要仅因为觉得这样“更安全”就加入 VPC 和 NAT Gateway。是否需要 VPC,应根据数据库、网络隔离和合规要求判断。

冷启动

长时间没有请求后,第一次调用可能出现额外延迟,这就是常说的冷启动。邀请邮件接口通常可以接受这种延迟。如果接口需要稳定且极低的响应延迟,可能要使用预置并发,但这会减少纯按使用量计费带来的成本优势。

其他可选方案

除了 AWS,还可以考虑 Azure Functions、Google Cloud Functions、Google Cloud Run、Cloudflare Workers,以及部分支持 scale-to-zero 的容器平台。

选择平台时,不能只比较“每百万次请求”的价格,还要确认以下事项:

  • 空闲时是否真的能缩容到零;
  • 是否存在最低实例数或固定套餐;
  • 最大执行时间和内存限制;
  • 冷启动表现;
  • 数据库连接方式;
  • 出站流量费用;
  • 自定义域名和日志费用;
  • 邮件发送能力;
  • 是否支持所需区域并满足合规要求。

对于简单的邀请 API,如果现有网站已经使用 AWS,API Gateway、Lambda、DynamoDB 和 SES 通常是集成成本较低,也比较接近按实际使用量计费的组合。

备注:内容仅供参考。