首页 / 资讯中心 / 文章详情

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

基于AWS的SaaS平台架构:多租户隔离、计费与弹性伸缩实战 ★ FEATURED ARTICLE
简介这是一份面向独立软件供应商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 TenantPool共享 ECS Service共享 RDStenant_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, paramsNone): tenant_id current_tenant.get() # 强制在 WHERE 里注入 tenant_id业务 SQL 不允许自带 WHERE 条件绕过 safe_sql fSELECT * 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 11之类写法绕过租户过滤。失败时先看 ContextVar 有没有在请求入口正确 set这是最常见的翻车点。2.3 隔离模型选型时最容易忽略的计费维度选隔离模型时大多数人只看技术隔离性忽略了计费维度。SaaS 的计费方式直接决定隔离模型能不能成立。按席位计费的 SaaSPool 模型最划算因为每个租户的资源消耗相对可预测。按用量计费的 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 Fieldhost-header,Valuestenant1.example.com \ --actions Typeforward,TargetGroupArnarn: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[fQUEUE_{tenant_id}] sqs.send_message( QueueUrlqueue_url, MessageBodyevent[payload], MessageGroupIdtenant_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 里企业层租户的成本是否被正确归属。如果企业层租户的成本还是混在共享资源里说明标签或独立资源创建流程有问题。最后说一个我自己的习惯每次架构评审我都会问「如果现在最大的租户明天流量翻十倍系统哪里先炸」。这个问题能逼着团队去想隔离边界和限流策略而不是等到真炸了再救火。多租户架构没有银弹只有一层层的权衡和兜底。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站