
简介这份PPT资料面向正在或计划将传统软件转型为SaaS的独立软件供应商架构师、技术负责人及云计算学习者系统梳理了基于AWS构建SaaS平台的整体架构思路与关键技术选型。内容围绕为何选择SaaS、为何AWS是理想平台展开深入讲解身份管理中的用户ID与租户ID映射、MFA与IAM策略多租户的Silo、Bridge、Pool三种模式对比以及应用分层隔离、网络与数据隔离等核心议题。同时覆盖CloudWatch、CloudTrail、S3等管理监控工具测量计费与租户统一视图并延伸至DevOps敏捷交付、大数据分析、物联网平台与人工智能服务引擎等场景。资源包共1个pptx文件约1.21MB以图文架构图与要点清单呈现便于快速理解多租户设计与隔离策略的取舍。目前已有143人学习适合需要搭建SaaS架构或评估AWS技术栈的读者参考。1. 从一份 PPT 标题说起AWS 上的 SaaS 平台架构到底在解决什么问题如果你手里也有一份叫「基于 AWS 的 SaaS 平台架构」的方案要落地大概率不是要从零写一份 PPT而是要把这套架构真正跑起来。SaaS 和传统单租户系统最大的区别在于一套代码、一套基础设施要同时服务成百上千个客户每个客户的数据要隔离、用量要计量、套餐要能升降级、账单要能算清楚。AWS 提供的托管服务能把这些脏活累活接过去一大半但前提是你得知道哪些服务组合起来才不踩坑。这篇内容面向的是正在做 SaaS 架构选型或迁移的工程师尤其是中小团队里那个「既要画架构图又要写 Terraform」的人。我会按「先想清楚租户模型 → 再落地基础设施 → 然后处理计费与隔离 → 最后排坑」的顺序讲每一章都给出可以直接抄的配置和代码。读完你应该能判断自己的 SaaS 产品适不适合上 AWS 多租户架构以及第一步该动哪里。2. 租户模型先定死三种隔离级别怎么选在写任何 AWS 资源之前必须先回答一个问题你的租户数据放在哪一层隔离。这个决定会直接影响后面所有基础设施的形态改起来代价极大。常见做法是三种共享数据库共享表加 tenant_id 字段、共享数据库独立 schema、独立数据库实例。选哪种不取决于技术偏好取决于你的客户画像和合规要求。2.1 三种模型的成本与隔离对比隔离级别数据存储方式单租户月成本量级适用场景迁移难度共享表同一张表加 tenant_id极低小微客户、免费套餐低独立 schema同库不同 schema低中型客户、有数据导出需求中独立实例每租户一个 RDS高金融、医疗、强合规高我一般建议起步阶段用共享表加 tenant_id但在数据访问层强制注入租户过滤条件不要指望每个开发者自觉写 WHERE。等出现第一个愿意为隔离付溢价的客户再针对他单独升级到独立 schema 或独立实例。这种「混合隔离」策略在 AWS 上很好实现因为 RDS 和 Aurora 都支持在同一集群里创建多个数据库。2.2 用行级安全策略兜住租户过滤光靠应用层写 WHERE tenant_id ? 迟早会翻车一个漏写的查询就可能把 A 客户的数据返回给 B 客户。PostgreSQL 的行级安全策略RLS是最后一道防线Aurora PostgreSQL 原生支持。-- 开启表的行级安全 ALTER TABLE orders ENABLE ROW LEVEL SECURITY; -- 创建策略只允许访问当前会话租户的数据 CREATE POLICY tenant_isolation ON orders USING (tenant_id current_setting(app.current_tenant)::uuid); -- 应用连接后必须设置当前租户 -- SET app.current_tenant 租户UUID;逻辑说明current_setting(app.current_tenant)从会话变量里读取租户 ID应用每次获取数据库连接后立刻执行 SET 命令。参数说明tenant_id字段类型要和会话变量转换后的类型一致否则策略不生效。注意连接池场景下归还连接前必须 RESET 这个变量否则下一个请求可能继承上一个租户的上下文这是血泪经验里最常见的一种串数据事故。2.3 租户上下文在应用层的传递方式应用层需要把租户 ID 从请求入口一路传到数据库会话。以 Spring Boot 为例常见做法是用 ThreadLocal 存租户上下文在拦截器里从 JWT 或子域名解析出租户标识。public class TenantContext { private static final ThreadLocalString CURRENT new ThreadLocal(); public static void set(String tenantId) { CURRENT.set(tenantId); } public static String get() { return CURRENT.get(); } public static void clear() { CURRENT.remove(); } }逻辑说明拦截器在请求进入时调用 set请求结束时必须调用 clear否则线程池复用会导致租户串号。参数说明租户标识建议用 UUID 而不是自增 ID避免被猜测。这套机制和后面要讲的 IAM 权限、计费计量是打通的——计量服务也需要知道当前请求属于哪个租户才能把用量记到正确的账上。3. 在 AWS 上把多租户基础设施搭起来租户模型定了之后接下来是把计算、存储、网络这些资源用 AWS 服务拼出来。SaaS 平台和普通 Web 应用在基础设施上的核心差异是你需要一套自动化的租户开通流程新客户注册后几分钟内就能用上独立或半独立的环境而不是靠运维手动建资源。这一章给出可复现的搭建路径。3.1 用 CDK 定义一套可复用的租户基础设施AWS CDK 比 Terraform 更适合 SaaS 场景因为你可以用编程语言写循环为每个租户生成一套资源栈。下面是一个最小化的租户基础设施定义包含一个 Aurora Serverless v2 集群和一个 ECS 服务。import * as cdk from aws-cdk-lib; import * as rds from aws-cdk-lib/aws-rds; import * as ecs from aws-cdk-lib/aws-ecs; import { Construct } from constructs; export class TenantStack extends cdk.Stack { constructor(scope: Construct, id: string, props: { tenantId: string }) { super(scope, id); // 每个租户一个独立的 Aurora Serverless v2 集群 const cluster new rds.DatabaseCluster(this, TenantDB, { engine: rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_15_4, }), serverlessV2MinCapacity: 0.5, serverlessV2MaxCapacity: 4, writer: rds.ClusterInstance.serverlessV2(writer), defaultDatabaseName: app, }); // 租户 ID 作为标签方便成本分摊 cdk.Tags.of(cluster).add(tenant, props.tenantId); } }逻辑说明每个租户一个 StackStack 名里带租户 ID资源打上 tenant 标签。参数说明serverlessV2MinCapacity设为 0.5 ACU 是成本底线空闲时几乎不花钱maxCapacity按租户套餐等级调整免费版给 2企业版给 16。注意 Aurora Serverless v2 的冷启动比 v1 快很多但首次连接仍有几百毫秒延迟对延迟敏感的场景要在连接池里保持最小连接数。3.2 租户开通流水线从注册到可用新租户注册后需要一个自动化流程把基础设施创建出来。常见做法是用 Step Functions 编排先调 CDK 部署租户栈再初始化数据库 schema最后把租户信息写入控制面数据库。# 触发租户开通的 Step Functions 执行 aws stepfunctions start-execution \ --state-machine-arn arn:aws:states:us-east-1:123456789012:stateMachine:TenantProvisioning \ --input {tenantId:t-abc123,plan:pro,region:us-east-1}逻辑说明Step Functions 的输入里带租户 ID、套餐等级和目标区域。参数说明plan字段决定后续资源规格region决定数据驻留位置。整个流程通常 3 到 5 分钟完成期间租户状态是 provisioning前端要轮询状态接口。这里有个坑CDK 部署本身是异步的Step Functions 里要用 wait 状态轮询 CloudFormation 栈状态不能直接假设部署完成。3.3 控制面与数据面的分离SaaS 架构里必须把控制面租户管理、计费、配置和数据面业务数据处理分开。控制面用一张全局的 DynamoDB 表存租户元数据数据面按租户隔离。这样做的好处是控制面故障不影响已有租户的业务运行。import boto3 dynamodb boto3.resource(dynamodb) table dynamodb.Table(TenantRegistry) def get_tenant_config(tenant_id): response table.get_item(Key{tenant_id: tenant_id}) item response.get(Item) if not item: raise ValueError(fTenant {tenant_id} not found) return { db_endpoint: item[db_endpoint], plan: item[plan], region: item[region], status: item[status] }逻辑说明控制面表用 tenant_id 做主键存数据库连接信息、套餐、状态。参数说明db_endpoint是租户专属的 Aurora 端点应用启动时从控制面拉取并缓存。注意这张表是全局单点要做好备份和跨区域复制否则控制面挂了新租户无法开通已有租户虽然能继续用但无法变更配置。4. 计费与计量SaaS 的钱袋子怎么管SaaS 平台和普通项目的本质区别之一就是必须能算清楚每个租户用了多少、该收多少钱。AWS 本身按资源用量计费但你需要把这层成本映射到租户维度再加上自己的加价逻辑。这一章讲计量数据的采集、聚合和账单生成。4.1 用量事件采集从 API 网关到 Kinesis每次租户调用 API 都应该产生一条用量事件包含租户 ID、接口名、时间戳、消耗的计量单位比如 API 调用次数、处理的数据量。这些事件先写入 Kinesis Data Streams再批量落盘。import json import boto3 from datetime import datetime kinesis boto3.client(kinesis) def emit_usage_event(tenant_id, api_name, units): event { tenant_id: tenant_id, api_name: api_name, units: units, timestamp: datetime.utcnow().isoformat() } kinesis.put_record( StreamNameusage-events, Datajson.dumps(event), PartitionKeytenant_id # 同一租户的事件进同一分片 )逻辑说明PartitionKey 用 tenant_id 保证同一租户的事件有序方便后续按租户聚合。参数说明units是计量单位可以是调用次数、token 数、存储字节数按你的计费维度定义。注意 Kinesis 单分片写入上限是 1MB/s 或 1000 条/s租户量大时要提前算好分片数否则会触发 ProvisionedThroughputExceededException。4.2 用量聚合Lambda 按小时汇总Kinesis 里的原始事件需要聚合成小时或天粒度的用量记录才能用于出账。常见做法是用 Lambda 消费 Kinesis按租户和接口维度做窗口聚合写入 Timestream 或 DynamoDB。def lambda_handler(event, context): aggregated {} for record in event[Records]: payload json.loads(record[kinesis][data]) key (payload[tenant_id], payload[api_name]) aggregated[key] aggregated.get(key, 0) payload[units] # 写入聚合结果表 for (tenant_id, api_name), total in aggregated.items(): table.update_item( Key{tenant_id: tenant_id, api_name: api_name}, UpdateExpressionADD total_units :val, ExpressionAttributeValues{:val: total} )逻辑说明Lambda 每次触发处理一批 Kinesis 记录先在内存里聚合再写库减少写入次数。参数说明total_units是累加字段用 DynamoDB 的 ADD 操作保证原子性。注意 Lambda 的批大小和窗口时间要匹配批太小聚合效果差批太大延迟高一般设 100 条或 60 秒。4.3 套餐与费用策略的映射计量数据有了接下来要映射到套餐。SaaS 套餐通常分免费版、专业版、企业版每个版本有配额和单价。这张映射表建议放在控制面数据库里方便运营调整而不需要改代码。套餐API 调用配额超出单价存储配额支持租户数免费版1 万次/月不可超出1 GB不限专业版100 万次/月0.01 元/次50 GB不限企业版不限0.005 元/次500 GB按合同逻辑说明配额检查在 API 网关的 Lambda 授权器里做每次请求前查当前用量是否超限。参数说明超出单价按阶梯递减鼓励大客户多用。注意免费版「不可超出」意味着超限直接拒绝请求要在响应里明确告诉用户升级套餐否则体验很差。5. 避坑与排查多租户 SaaS 上 AWS 最容易翻车的地方这一章记录的是我在实际项目里踩过的坑每一条都按「现象 → 原因 → 解决」写。有些坑看起来是 AWS 的问题其实是架构设计时没想清楚。5.1 连接池串租户A 客户看到 B 客户的数据现象压测时偶发数据错乱A 租户的请求返回了 B 租户的订单。原因应用用了 HikariCP 连接池租户上下文存在 ThreadLocal 里但连接归还时没有 RESET 数据库会话变量下一个请求复用了同一个连接继承了上一个租户的app.current_tenant。解决在连接池的 connectionInitSql 里强制 RESET或者在每次获取连接后重新 SET。更稳妥的做法是用独立的数据库用户每个租户一个角色从连接层面隔离。5.2 Aurora Serverless v2 冷启动导致首请求超时现象新租户开通后第一次访问特别慢有时直接 504。原因Aurora Serverless v2 虽然比 v1 快但从 0.5 ACU 扩容到更高容量仍需几秒加上应用连接池首次建连叠加起来超过网关超时。解决把serverlessV2MinCapacity设到 1 而不是 0.5或者在租户开通流程里加一步预热主动发几个查询把容量拉起来。对延迟敏感的业务直接上 Provisioned 实例更省心。5.3 Kinesis 分片热键导致计量数据延迟现象某个大租户的用量数据延迟几小时才出现在账单里。原因Kinesis 按 PartitionKey 分片这个大租户的调用量太大单个分片写入达到上限数据被限流后重试。解决PartitionKey 改成 tenant_id 随机后缀把热点打散到多个分片。代价是同一租户的事件不再有序但计量场景对顺序不敏感可以接受。5.4 CDK 部署租户栈时触发 CloudFormation 配额现象开到第 200 个租户时新租户开通失败报 CloudFormation 栈数量超限。原因AWS 默认每个账户每个区域最多 2000 个 CloudFormation 栈每个租户一个栈很快就用完了。解决改用 CDK 的嵌套栈或者直接用 SDK 调 API 创建资源不走 CloudFormation。或者把多个小租户合并到一个栈里用参数区分。5.5 跨区域数据驻留合规检查缺失现象欧洲客户投诉数据被存到了美国区域。原因租户开通时 region 参数没做校验默认用了 us-east-1。解决在控制面加一层合规检查根据租户所在国家强制指定区域欧洲客户只能选 eu-west-1 或 eu-central-1。这个检查要在 Step Functions 的第一步做不通过直接拒绝开通。6. 进阶技巧用标签和 Cost Explorer 做租户级成本核算前面讲的都是怎么把平台搭起来、怎么算钱。但还有一个更实际的问题你怎么知道每个租户到底花了你多少 AWS 成本如果不知道定价就是拍脑袋。这一章讲一个我常用的技巧用资源标签加 Cost Explorer API 做租户级成本分摊。核心思路很简单所有为租户创建的资源都打上tenant标签然后在每月出账后调 Cost Explorer 的GetCostAndUsage接口按标签维度拉取成本数据再和你的计费系统对账。import boto3 from datetime import datetime, timedelta ce boto3.client(ce) def get_tenant_cost(tenant_id, start_date, end_date): response ce.get_cost_and_usage( TimePeriod{Start: start_date, End: end_date}, GranularityMONTHLY, Metrics[BlendedCost], Filter{ Tags: { Key: tenant, Values: [tenant_id] } }, GroupBy[{Type: DIMENSION, Key: SERVICE}] ) return response[ResultsByTime]逻辑说明Filter按 tenant 标签过滤GroupBy按服务维度拆分这样你能看到每个租户在 EC2、RDS、S3 上分别花了多少。参数说明Granularity用 MONTHLY 出账用日常监控可以用 DAILY。注意 Cost Explorer 的数据有 24 小时延迟且标签激活需要时间新打的标签不会立刻出现在成本报告里。拿到成本数据后和你的计费系统做对比。如果某个租户的 AWS 成本已经超过他付的订阅费要么涨价要么优化架构。我一般会设一个告警当租户的 AWS 成本超过其套餐收入的 70% 时触发提醒运营介入。这个比例不是固定的基础设施占比高的业务可以放宽到 80%纯软件业务应该控制在 50% 以下。还有一个细节共享资源比如控制面的 DynamoDB、API 网关的成本没法直接归到某个租户需要按用量比例分摊。我的做法是单独建一个shared标签月底按各租户的 API 调用量占比分摊这部分成本。虽然不精确但比不算强。最后说一个我自己的习惯每次给租户开通资源时除了打 tenant 标签还会在资源描述里写上开通时间和套餐版本。这样半年后回头看能快速判断哪些租户是早期低价套餐、现在成本已经倒挂该谈续约涨价了。这个习惯帮我避免了好几次「客户越用越多、我们越亏越多」的局面。希望帮到你。本文还有配套的精品资源点击获取