news 2026/10/4 1:56:07

基于AWS的SaaS平台架构:多租户隔离、计费与弹性伸缩实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于AWS的SaaS平台架构:多租户隔离、计费与弹性伸缩实战

简介:这是一份面向独立软件供应商(ISV)架构师、技术负责人与云计算从业者的解决方案型PPT,围绕基于AWS构建SaaS平台展开,帮助读者理解从传统软件向SaaS转型的动因与落地路径。内容系统梳理了AWS在按需付费、弹性扩展、多租户共享基础设施等方面的优势,并深入讲解身份管理、租户隔离、应用分层隔离、管理监控、测量计费、DevOps敏捷交付等核心模块,还延伸至大数据、物联网与人工智能服务平台架构。资源包内含1个pptx文件,约1.21MB,以图文并茂的幻灯片形式呈现,便于直接用于方案汇报或内部培训。目前已有143人学习下载,适合需要快速建立SaaS架构全局认知、对照AWS服务选型与多租户设计思路的中高级技术人员参考。

1. 从一份 PPT 标题说起:AWS 上的 SaaS 平台架构到底在解决什么

如果你手里也有一份叫「基于 AWS 的 SaaS 平台架构」的 PPT,大概率不是要讲 AWS 有多少服务,而是要回答一个更硬的问题:一套多租户系统,怎么在 AWS 上做到租户隔离、按量计费、弹性伸缩,还能让运维不被半夜的告警电话拖垮。SaaS 和普通 Web 系统最大的区别不在功能,而在「一套代码服务 N 个客户」这件事本身带来的架构约束——数据怎么隔离、资源怎么分配、某个租户流量暴涨时会不会拖垮其他人。

这份 PPT 标题里的三个词,其实对应三层决策:AWS 是基础设施底座,SaaS 是业务交付模式,平台架构是把两者接起来的那套规则。它适合正在从单租户往多租户迁移的团队,也适合已经上了云但租户一多就开始出问题的团队。接下来我不讲 PPT 怎么做,而是把这份标题背后的技术方案拆开,讲清楚每一层该怎么落地、参数怎么定、哪里最容易翻车。

2. 多租户隔离模型:三种池化策略怎么选

2.1 从「一套代码服务 N 个客户」倒推隔离粒度

多租户的核心矛盾是成本和隔离的权衡。隔离越彻底,单位成本越高,但故障爆炸半径越小。业界通常把隔离模型分成三种池化策略,这不是 AWS 独有的概念,但在 AWS 上每种策略对应的服务组合差别很大。

Siloh(独立池):每个租户一套独立资源,独立数据库、独立计算实例。隔离性最强,但租户上百之后运维成本指数上升。适合金融、医疗这类合规要求极高的场景。

Pool(共享池):所有租户共享同一套计算和存储,靠 tenant_id 字段做逻辑隔离。成本最低,但一个租户的慢查询可能拖垮整个数据库。适合中小客户为主、对隔离不敏感的 SaaS。

Bridge(混合池):大部分租户共享,大客户或高合规租户单独隔离。这是目前最主流的做法,也是我在实际项目里最常推荐的起点。

选型时不要一上来就追求最强隔离。我见过太多团队为了「安全」给每个租户开独立 RDS,结果租户到 50 个的时候,光数据库实例费用就吃掉了全部毛利。常见做法是:先用 Pool 模型快速验证业务,等出现第一个愿意为隔离付溢价的大客户时,再引入 Bridge 模型。

2.2 在 AWS 上落地三种模型的资源映射

把上面的策略翻译成 AWS 的具体服务,大致是这样一张映射表:

池化策略计算隔离数据隔离典型 AWS 组合
Siloh每租户独立 ECS/EKS 命名空间每租户独立 RDS 实例ECS Cluster + RDS Instance per Tenant
Pool共享 ECS Service共享 RDS,tenant_id 行级隔离ECS Service + RDS + 应用层过滤
Bridge共享为主,大租户独立共享库 + 大租户独立库ECS + RDS + 动态路由层

Pool 模型下,数据隔离靠应用层保证,这里最容易出问题。我一般会在数据访问层强制注入 tenant_id 条件,而不是靠开发人员自觉。下面是一个简化的 Python 数据访问封装示例:

# tenant_aware_db.py # 核心思路:所有查询强制拼接 tenant_id,杜绝跨租户读取 import psycopg2 from contextvars import ContextVar # 用 ContextVar 存当前请求的租户 ID,避免层层传参 current_tenant = ContextVar("current_tenant") class TenantAwareDB: def __init__(self, dsn): self.conn = psycopg2.connect(dsn) def query(self, sql, params=None): tenant_id = current_tenant.get() # 强制在 WHERE 里注入 tenant_id,业务 SQL 不允许自带 WHERE 条件绕过 safe_sql = f"SELECT * FROM ({sql}) AS t WHERE t.tenant_id = %s" with self.conn.cursor() as cur: cur.execute(safe_sql, (params or []) + [tenant_id]) return cur.fetchall()

这段代码的关键在于把 tenant_id 的注入放在框架层而不是业务层。参数说明:current_tenant用 ContextVar 而不是全局变量,是因为在异步框架下全局变量会被并发请求污染;safe_sql用子查询包一层,是为了防止业务 SQL 里的WHERE 1=1之类写法绕过租户过滤。失败时先看 ContextVar 有没有在请求入口正确 set,这是最常见的翻车点。

2.3 隔离模型选型时最容易忽略的计费维度

选隔离模型时,大多数人只看技术隔离性,忽略了计费维度。SaaS 的计费方式直接决定隔离模型能不能成立。按席位计费的 SaaS,Pool 模型最划算,因为每个租户的资源消耗相对可预测。按用量计费的 SaaS,如果用量波动大,Pool 模型下某个租户突然跑批处理,会直接影响其他租户的体验,这时候 Bridge 模型更稳。

在 AWS 上做用量计费,通常会把 CloudWatch 的指标和 Cost Explorer 的标签结合起来。给每个租户的资源打上tenant_id标签,然后用 Cost Explorer 按标签聚合成本。这一步不做,后面根本算不清每个租户是赚是亏。我一般会在资源创建时就强制打标签,用 AWS Config 规则检查没有标签的资源并告警。

3. 用 AWS 原生服务搭出可计费的多租户底座

3.1 租户识别与请求路由的最小实现

多租户系统的入口第一件事是识别租户。常见做法有三种:子域名(tenant1.example.com)、请求头(X-Tenant-Id)、JWT 声明。子域名对用户最友好,也最容易被 CDN 和 WAF 处理。在 AWS 上,我一般用 Route 53 泛域名解析 + ALB 的 host-based routing 来做。

下面是一个 ALB 监听规则的概念配置,用 AWS CLI 表达:

# 创建基于 host header 的 ALB 监听规则 # 把 tenant1.example.com 的流量转发到 tenant1 目标组 aws elbv2 create-rule \ --listener-arn arn:aws:elasticloadbalancing:region:account:listener/app/my-alb/xxx \ --priority 10 \ --conditions Field=host-header,Values=tenant1.example.com \ --actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:region:account:targetgroup/tenant1-tg/xxx

参数说明:priority数字越小优先级越高,泛域名规则要放在最后;conditions里用 host-header 而不是 path-pattern,是因为子域名方案对应用代码零侵入。失败时先确认 Route 53 的泛域名证书有没有覆盖*.example.com,证书不匹配会导致 ALB 直接拒绝连接,这个坑我踩过不止一次。

请求到达应用后,需要把租户上下文透传到下游。我一般会在 API Gateway 或 ALB 层就把 tenant_id 解析出来,塞进请求头,应用层只负责读取。这样做的原因是:如果让每个微服务自己去解析子域名,一旦路由规则变了,要改的地方太多。

3.2 数据层隔离:RDS 行级安全与 Schema 隔离的取舍

数据层是多租户架构里最敏感的部分。Pool 模型下,PostgreSQL 的行级安全策略(RLS)是一个被低估的能力。它能在数据库层面强制隔离,即使应用层漏了 tenant_id 条件,数据库也会返回空结果。

-- 启用行级安全,强制按 tenant_id 隔离 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 = 'tenant-uuid-here';

参数说明:current_setting('app.current_tenant')是会话级变量,每个连接建立后必须设置,连接池复用时尤其要注意重置,否则会出现租户 A 的连接被租户 B 复用导致数据串号。这是 RLS 方案里最隐蔽的坑。解决方式是在连接归还连接池前执行RESET app.current_tenant,或者用连接池的初始化 SQL 强制设置。

Schema 隔离是另一种选择:每个租户一个 schema,表结构相同但数据完全分开。它的好处是备份和恢复可以按租户做,坏处是租户上千之后 schema 数量爆炸,迁移脚本要跑 N 遍。我一般建议租户数在 100 以内用 Schema 隔离,超过 100 用 RLS。

3.3 计算层弹性:ECS 与 Lambda 在多租户下的分工

计算层要回答的问题是:租户的请求跑在哪里。ECS 适合长驻服务,Lambda 适合突发和低频任务。在多租户场景下,我一般这样分工:API 服务用 ECS,因为需要稳定的连接池和较低的冷启动延迟;异步任务和定时批处理用 Lambda,按租户维度触发。

Lambda 在多租户下有一个容易忽略的问题:并发限制是账号级的,不是租户级的。某个租户突然触发大量 Lambda,会吃掉整个账号的并发配额,导致其他租户的任务排队。解决办法是给每个租户设置预留并发(reserved concurrency),或者用 SQS 做队列缓冲,按租户维度限流。

# lambda_tenant_limiter.py # 用 SQS 消息属性做租户级限流,避免单租户打满账号并发 import boto3 import os sqs = boto3.client("sqs") def handler(event, context): tenant_id = event["tenant_id"] # 每个租户一个队列,队列长度即积压量 queue_url = os.environ[f"QUEUE_{tenant_id}"] sqs.send_message( QueueUrl=queue_url, MessageBody=event["payload"], MessageGroupId=tenant_id # FIFO 队列保证同租户顺序 ) return {"status": "queued", "tenant": tenant_id}

参数说明:MessageGroupId只在 FIFO 队列里有效,用来保证同一租户的消息顺序;QUEUE_{tenant_id}这种环境变量命名方式在租户多的时候不好维护,生产环境建议用 DynamoDB 存租户到队列的映射。失败时先看 SQS 的ApproximateNumberOfMessages指标,积压量持续上涨说明下游消费能力不足,需要扩容消费者。

4. 计费与用量采集:把资源消耗翻译成账单

4.1 用量事件采集的三种粒度

SaaS 计费的基础是用量数据。采集粒度决定了计费的灵活性和实现复杂度。常见三种粒度:请求级(每次 API 调用记一条)、会话级(一次用户会话记一条)、聚合级(每小时汇总一次)。请求级最精确但存储成本高,聚合级成本低但丢失细节。

我一般用「请求级采集 + 小时级聚合」的组合:原始事件写入 Kinesis Firehose,自动落到 S3,然后用 Athena 做小时级聚合。这样既保留了原始数据用于对账,又不会让计费查询扫全量数据。

-- Athena 查询:按租户和小时聚合 API 调用次数 SELECT tenant_id, date_trunc('hour', event_time) AS hour_bucket, COUNT(*) AS api_calls, SUM(response_bytes) AS total_bytes FROM usage_events WHERE event_time >= TIMESTAMP '2024-01-01 00:00:00' GROUP BY tenant_id, date_trunc('hour', event_time);

参数说明:date_trunc('hour', ...)是 PostgreSQL 和 Athena 都支持的函数,按小时对齐;response_bytes用来做流量计费。这个查询在数据量大时会很慢,建议在 S3 上按日期分区,Athena 查询时自动裁剪分区。

4.2 把 CloudWatch 指标映射到租户账单

AWS 原生服务的用量数据在 CloudWatch 里,但 CloudWatch 的维度默认没有 tenant_id。要按租户计费,必须在资源创建时打标签,然后用 Cost Explorer 的标签聚合。这里有一个时间差问题:Cost Explorer 的数据有 24 小时延迟,不适合实时计费展示。

实时计费我一般用 CloudWatch 的自定义指标:应用层在每次请求后往 CloudWatch 打一个带 tenant_id 维度的指标,然后用 CloudWatch 的 GetMetricData API 做实时聚合。成本比 Cost Explorer 高,但延迟低。

数据源延迟精度适用场景
Cost Explorer24h账单级月度对账
CloudWatch 自定义指标1min请求级实时用量展示
Kinesis + S3 + Athena5min事件级计费明细与审计

选哪种取决于业务对实时性的要求。如果只是月度出账,Cost Explorer 足够;如果要在控制台展示「本月已用多少」,必须用自定义指标。

5. 避坑与排查:多租户 SaaS 上线后最常炸的五个地方

5.1 连接池复用导致租户数据串号

现象:租户 A 登录后看到租户 B 的订单列表,刷新后又恢复正常,偶发且难复现。

原因:应用用了连接池,租户 A 的请求设置了app.current_tenant,连接归还池后没有重置,租户 B 的请求复用了这个连接,读到了 A 的租户上下文。

解决:在连接归还池前强制执行RESET app.current_tenant,或者用连接池的init语句在每次取出连接时重新设置。PgBouncer 的 transaction 模式下尤其要注意,因为连接在事务结束后就归还了。

5.2 Lambda 并发被单租户打满

现象:某个租户跑了一次批量导入,其他租户的异步任务全部超时。

原因:Lambda 并发是账号级配额,单租户的突发流量吃掉了全部并发。

解决:给每个租户的 Lambda 函数设置预留并发,或者用 SQS 做缓冲并按租户限流。预留并发会占用账号总配额,租户多的时候要算好总量。

5.3 RLS 策略被表 owner 绕过

现象:行级安全策略明明启用了,但某些查询还是能读到其他租户的数据。

原因:PostgreSQL 里表 owner 默认绕过 RLS,除非显式设置FORCE ROW LEVEL SECURITY。

解决:执行ALTER TABLE orders FORCE ROW LEVEL SECURITY;,并且应用连接用的数据库用户不能是表 owner。

5.4 标签漏打导致计费数据缺失

现象:月度账单里有一部分资源成本无法归属到任何租户。

原因:资源创建时没有强制打 tenant_id 标签,或者标签拼写不一致(tenant_idvstenantId)。

解决:用 AWS Config 规则检查必填标签,用 Service Control Policy 阻止无标签资源创建。标签键统一用下划线命名,避免大小写混用。

5.5 跨租户的缓存键冲突

现象:租户 A 的配置更新后,租户 B 的配置也跟着变了。

原因:Redis 缓存键没有带 tenant_id 前缀,不同租户的相同业务键互相覆盖。

解决:所有缓存键强制加tenant:{tenant_id}:前缀,在缓存封装层统一处理,不依赖开发人员手动拼。

6. 进阶技巧:用租户分层做成本与体验的平衡

租户分层是多租户 SaaS 从「能用」到「赚钱」的关键一步。不是所有租户都值得同样的资源投入。我一般把租户分成三层:免费层、标准层、企业层。免费层用 Pool 模型,共享资源,限制并发和存储;标准层用 Pool 模型但给更高的配额;企业层用 Bridge 模型,独立数据库或独立计算资源。

分层的实现不需要改架构,只需要在路由层加一个租户等级判断。下面是一个简化的路由决策逻辑:

# tenant_router.py # 根据租户等级决定请求走共享资源还是独立资源 TIER_ROUTING = { "free": {"cluster": "shared", "db": "shared", "rate_limit": 100}, "standard": {"cluster": "shared", "db": "shared", "rate_limit": 1000}, "enterprise": {"cluster": "dedicated", "db": "dedicated", "rate_limit": 10000}, } def route_request(tenant_id, tenant_tier): config = TIER_ROUTING.get(tenant_tier, TIER_ROUTING["free"]) # 企业层租户走独立集群,其他走共享 if config["cluster"] == "dedicated": return get_dedicated_endpoint(tenant_id) return get_shared_endpoint()

参数说明:rate_limit是每分钟请求数上限,用 API Gateway 的 Usage Plan 实现;dedicated集群的 endpoint 存在 DynamoDB 里,按 tenant_id 查询。这个方案的好处是分层逻辑集中在一处,新增租户等级只需要改配置表。

验证分层是否生效,我一般会做两件事:一是用企业层租户的账号跑压测,确认共享层租户的延迟不受影响;二是看 Cost Explorer 里企业层租户的成本是否被正确归属。如果企业层租户的成本还是混在共享资源里,说明标签或独立资源创建流程有问题。

最后说一个我自己的习惯:每次架构评审,我都会问「如果现在最大的租户明天流量翻十倍,系统哪里先炸」。这个问题能逼着团队去想隔离边界和限流策略,而不是等到真炸了再救火。多租户架构没有银弹,只有一层层的权衡和兜底。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 1:55:51

告别Charles证书地狱:用Frida Hook直取App网络请求与响应

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:54:23

图表开发实战总结| 7 条“黄金法则”与“性能铁律”

今天,不谈论某个具体 API 怎么用。我想分享一些更宏观的、能决定你项目成败的“法则”。这是我从大量图表应用中总结出的 7 条黄金法则,这是基于多年使用 Highcharts可视化图表 高阶开发的实战指南。法则一:【工程化瘦身铁律】按需导入功能模…

作者头像 李华
网站建设 2026/10/4 1:51:51

告别位置偏差:MingLi-Bench 无固定点选项打乱算法原理深析

告别位置偏差:MingLi-Bench 无固定点选项打乱算法原理深析 【免费下载链接】MingLi-Bench A benchmark for evaluating LLMs on Chinese traditional fortune telling — Bazi (八字) and Ziwei Doushu (紫微斗数). 项目地址: https://gitcode.com/gh_mirrors/mi/…

作者头像 李华