尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多租户SaaS架构:隔离模型选择与租户生命周期管理

多租户SaaS架构:隔离模型选择与租户生命周期管理 简介以AWS为基础构建SaaS平台架构的方案讲解PPT适合云架构师、技术决策者以及正在向SaaS模式转型的独立软件供应商ISV参考可帮助理解从传统软件交付到多租户云服务的整体演进路径。压缩包内共有1个PPT演示文稿大小约1.21MB以图示和要点形式系统呈现SaaS缘起、AWS平台优势、身份与租户隔离、三种多租户模式Silo、Bridge、Pool的优劣对比、应用分层隔离、监控与计费、DevOps流程以及大数据、物联网、人工智能等扩展方向。已有143人学习内容覆盖从基础概念到落地实践的多个层面既可作为方案选型时的对比依据也能用于内部技术分享或项目初期的架构梳理。通过这份PPT读者可以快速获得AWS SaaS平台的关键设计思路尤其能在身份模型、数据隔离和运维监控等核心环节上少走弯路是一份兼顾广度与深度的方案参考。1. 从共享池到租户感知SaaS 架构为什么不能一步到位新租户接入时控制面应该在几分钟内把他放进哪个环境很多刚从传统 ISV 转过来的团队第一反应是给每个租户复制一套 VPC 加数据库三个月后账单翻倍运维却开始按租户排队。云上资源确实可以按需创建但真正决定 SaaS 服务密度和交付效率的不是虚拟机规格而是租户上下文tenant context这条贯穿身份、数据、计费和监控的主线。下面这张架构拆解来自一份 AWS 上的 SaaS 平台设计主线可以概括为用 AWS 的多账号能力搭底座用「控制面 数据面」分离处理租户生命周期再在应用层把租户 ID 灌进每一个请求。适合正在做 SaaS 化的架构师、云上负责人以及从单体软件向多租户迁移的团队。看完你能直接带回去用的是一套可操作的隔离选型表和租户迁移路径。2. 多租户隔离模型与 SaaS IDSilo、Bridge、Pool 的取舍2.1 三种隔离模型的边界与适用场景多租户的第一道选择题是资源拓扑。AWS 上最常见的做法是先把租户按隔离强度分成三档Silo、Bridge、Pool。Silo 是每个租户一套完整环境网络、计算、存储全部独立合规性最好租户之间完全没有爆炸半径代价是资源利用率低版本升级要跑多遍监控和计费也得各自聚合。Pool 是共享基础设施所有租户跑在同一套应用和数据库上用 tenant_id 做逻辑隔离密度和成本最优但一个租户的慢查询、超量调用都可能影响整池更重要的是只要底层实例故障池内所有租户同时不可用这就是架构上常说的 All or Nothing。Bridge 介于两者之间例如共享数据库集群但每个租户独立 Schema隔离性略弱于 Silo密度又不如 Pool。选择不能拍脑袋。给一张实际选型时用的对照表按「隔离维度、资源密度、漏洞影响、适用租户」四个角度来定隔离模型数据层资源密度租户间影响典型适用Silo每租户独立实例/独立 VPC低无影响爆炸半径仅限单租户合规要求高、资源消耗大、愿意付高价的头部租户Bridge共享集群独立 Schema中底层故障仍可能互相影响逻辑 Layer 隔离中型租户对数据隔离有较高要求Pool共享表tenant_id 过滤高明显慢 SQL、热点 Key 都可能跨租户扩散中小租户、试用客户、长尾流量我一般会建议平台同时支持三种模式而不是只锁定一个。因为租户本身会成长一个从 Pool 起步的 SAAS 客户三年后可能因为合规审计要求必须升级到独立环境。控制面里把「隔离模式」做成租户属性后续才有迁移空间。2.2 数据层隔离实例级、Schema 级还是共享表数据隔离选型通常发生在数据库层。AWS 上三档对应关系是每个租户一个 RDS/Aurora 实例、共享实例但每个租户独立 Schema、共享库共享表按 tenant_id 路由。每租户一个实例的问题不用多说成本和运维都线性增长共享 Schema 是密度和隔离的折中但要注意连接数上限PostgreSQL 的 max_connections 很容易被租户数量打满共享表则必须保证所有 SQL 都强制带 tenant_id一旦某条更新语句漏掉过滤条件就是跨租户数据事故。我的经验是不要指望开发人员自觉而是让筛选条件变成代码层强制。例如在 ORM 层注入租户过滤器# 所有数据库操作前强制注入租户条件防止漏写 where tenant_id def get_tenant_scoped_session(tenant_id: str): session Session() event.listens_for(session, before_flush) def _inject_tenant_filter(session, flush_context, instances): for obj in session.new | session.dirty: if hasattr(obj, tenant_id): obj.tenant_id tenant_id return session # 这里用 SQLAlchemy 事件监听核心动作是写入新对象或修改对象时自动补 tenant_id这段代码的逻辑是业务代码只管正常读写不再手动拼接租户条件before_flush在事务提交前把当前租户 ID 强制写到新增和变更对象上。它的局限在于只覆盖写入查询路径仍需要在基类里加tenant_id current_tenant的过滤器否则全表扫描一样会泄露数据。2.3 身份模型用户 ID 租户 ID 才是完整的 SaaS ID隔离的另一个关键维度是身份。单租户系统里一个用户 ID 就能定位身份但 SaaS 里同一个邮箱可能存在于十个不同租户。所以身份不能只看「你是谁」还要看「你在哪个租户上下文里操作」。AWS 的身份边界由 Cognito User Pool、IAM 策略和 MFA 组合完成但应用层更需要维护一个复合标识SaaS ID 用户 ID 租户 ID。这里给一个身份校验的最小实现目的是把「登录用户」和「当前访问的租户」绑定避免跨租户访问def build_identity(id_token: str, request_host: str) - dict: claims decode_jwt(id_token) # 使用 IdP 的公钥验签 token_tenant_id claims.get(custom:tenant_id) # IdP 中预置的租户属性 host_tenant_id resolve_tenant_by_domain(request_host) # 根据访问域名反查租户注册表 if not token_tenant_id or token_tenant_id ! host_tenant_id: raise PermissionError(租户 ID 与访问域名不一致拒绝建立会话) return { user_id: claims[sub], # 用户在 IdP 中的唯一标识 tenant_id: token_tenant_id, # 租户上下文后续所有 API 都要带 role: claims.get(custom:role, member) }这里有几个参数说明custom:tenant_id是 Cognito 中自定义属性不能只存邮箱resolve_tenant_by_domain是从控制面读取租户注册表这里不查数据库而是走缓存否则每次登录都会打一次 DB。校验失败直接抛异常不能降级放行。更稳妥的做法是在 MFA 通过之后再执行这一步否则多因素认证还没有完成租户上下文就先泄露了。2.4 什么场景不要直接上 PoolPool 模式再诱人有三类场景我会直接反对。第一类是数据合规要求强隔离的租户例如医疗、金融客户要么用 Silo要么用支持行级安全策略的数据库方案共享表里用 tenant_id 过滤在合规审计面前不够硬第二类是重度计算型租户他们可能跑批处理任务把整池 CPU 占满影响其他租户第三类是状态很重的单体应用改造隔离成本往往高于多部署一套环境。提示Pool 模式下租户间影响不可避免建议把「熔断」设计在控制面单租户资源使用超限时优先拒绝该租户的新请求而不是让整池进入保护模式。3. 租户生命周期管理从 Pool 到 Silo 的资源流动与 DevOps3.1 默认进 Pool按业务等级动态升级隔离SaaS 平台要服务的第一条原则是快速交付。新租户注册后直接为其创建独立环境通常是最慢的路径要等 VPC、数据库、SSL 证书全部就绪。更常见的设计是新租户默认被打入 Pool用一个租户上下文记录其配置和数据路由只有当业务等级发生变化例如从试用升级为战略客户或者客户提出合规隔离要求才触发「池化租户迁移到独立环境」的流程。这个设计的本质是SaaS 平台必须把「隔离模式」做成租户的一个属性而非创建时的静态决定。控制面维护一张租户资源映射表记录每个租户当前处于 Pool、Bridge 还是 Silo下游监控和计费都通过这张表聚合。3.2 用基础设施即代码描述租户拓扑隔离模式动态化的前提是基础设施可编程。如果每一次租户迁移都靠人工在控制台点鼠标运维根本跟不上。推荐的做法是用 Terraform 或 CloudFormation 管理租户资源把租户列表作为变量输入同一份代码定义 Pool 租户和 Silo 租户的差异。下面是一个简化示例# 池化租户不单独建资源只登记租户上下文 module pool_tenant { source ./modules/tenant_registry for_each var.pool_tenants tenant_id each.key data_model shared_table # 数据层使用共享表仅写入配置 } # 独立租户创建完整环境 module silo_tenant { source ./modules/tenant_silo for_each var.silo_tenants tenant_id each.key vpc_cidr each.value.vpc_cidr database_engine aurora-postgresql deletion_protection true # 防止误删租户数据库 }pool_tenants和silo_tenants是控制面维护的两个 map 变量业务部门通过内部系统变更租户等级后CI 自动更新这两个变量并执行terraform apply。这段代码的价值在于租户从 Pool 升级到 Silo不是新建一套流程而是把租户 ID 从var.pool_tenants移到var.silo_tenantsTerraform 会自动创建新环境。注意这里for_each不能去掉否则所有租户会互相覆盖资源定义。3.3 部署流水线多租户与多环境反复交叉SaaS 场景下的 DevOps 和单租户软件有本质区别单租户发布只要保证一个实例的兼容性多租户发布要同时兼容池里的所有租户、跨版本数据、以及部分租户可能还在旧版本 API 上运行。PPT 里的那条流水线从 Commit、Unit Test、System Test、QA、Staging 到 Prod每一层都不只是代码验证还要跑租户配置迁移脚本。这里补一步通用实践发布前用影子表验证数据迁移把生产租户数据脱敏后导入新版本 Schema跑一遍冒烟用例。部署策略上有两种做法滚动发布和蓝绿发布。Pool 模式的租户量大滚动发布更常见但需要精细化控制批次Silo 模式下每个租户是一套独立环境可以按租户灰度先升级内部测试租户再升级付费租户。具体到 AWS应用层走 ECS/Fargate 的滚动部署数据库结构变更用 Aurora 的蓝绿部署特性避免一次 DDL 锁表影响池内全部租户。CI 阶段还要增加一个租户上下文校验环节确认所有迁移脚本都带 tenant_id 条件避免把生产库的全局更新带入版本。3.4 池化租户迁移到独立环境的切换要点租户从 Pool 提升到 Silo最怕的是数据不一致。迁移的基本路径是先对池内租户的数据做快照或逻辑导出然后在新建的 Silo 实例里导入验证行数和关键字段后把控制面中的租户路由切换到新环境最后销毁原池中的数据副本。切换点必须串行否则会出现同一租户的数据同时在两个环境写入。切换时 AWS 上最顺手的工具是 RDS 快照或 Aurora 的克隆功能。Aurora 克隆能在一两分钟内创建出可读写的独立实例对源库 IO 影响极小比逻辑导出快得多。迁移步骤大致是# 1. 对当前租户所在的 Aurora 集群创建克隆用于数据核对 aws rds restore-db-cluster-to-point-in-time \ --source-db-cluster-identifier saas-main-cluster \ --restore-type copy-on-write \ --db-cluster-identifier tenant-93194942-clone \ --use-latest-restorable-time # 2. 确认新实例可访问后将路由切换为租户独立端点 aws rds modify-db-cluster \ --db-cluster-identifier tenant-93194942 \ --new-db-cluster-identifier tenant-93194942-silo参数解释copy-on-write是 Aurora 克隆的关键选项它不复制实际数据文件而是从源集群按页延迟复制成本远低于物理快照use-latest-restorable-time取最近可恢复时间点降低增量数据丢失窗口。第二步的modify-db-cluster实际不会改实例名这里要配合 DNS 层更新租户路由表。整个切换期间要保证源库不再接受写入所以顺序应该是控制面将租户状态置为migrating网关拒绝该租户写请求等数据对齐后再切回active。很多事故都源于顺序反了先切流量再迁移数据结果丢了一条关键订单。4. 租户级运维CloudWatch、Config、CloudTrail 与成本归属4.1 用标签把租户维度注入运维链路多租户运维的最大难点不是告警数量而是告警无法定位到具体租户。AWS 的 CloudWatch、AWS Config 和 CloudTrail 都是全局视角默认不会自动区分租户。解决办法是把「租户上下文」做成资源标签并在日志和告警里带出。最常见的一组标签是标签键取值示例作用tenant-idt-93194942租户维度资源聚合tenant-tierpool/silo区分隔离模式决定告警级别envprod/staging环境隔离cost-centerfinance/operations内部成本归口CloudWatch 告警里每条指标都要带上 tenant-id 标签否则一个 CPU 飙升告警打过来至少要花半小时翻资源归属。AWS Config 的规则同样可以按标签范围启用例如只对tenant-tiersilo的资源强制开启删除保护Pool 租户不必重复检查。CloudTrail 对 SaaS 平台的独特价值在于审计当租户投诉数据异常时可以通过 CloudTrail 查询谁在什么时间调用了哪个 API配合user_id tenant_id的组合判断是否越权。4.2 用成本分配标签拆分多租户账单SaaS 按量计费的前提是知道每个租户消耗了多少资源。AWS 的 Cost Explorer 成本拆分依赖「成本分配标签」需要先在账单控制台激活标签然后才能按租户聚合。最简单的开源查询方式是直接调 AWS CLIaws ce get-cost-and-usage \ --time-period Start2025-10-01,End2025-10-31 \ --granularity DAILY \ --filter {Dimensions: {Key: RECORD_TYPE, Values: [Usage]}} \ --metrics UnblendedCost \ --group-by TypeTAG,Keytenant-id命令说明--group-by指定按标签键tenant-id分组输出结果就是每个租户每天的原始费用RECORD_TYPEUsage过滤掉税务和折扣类的非资源记录UnblendedCost是按使用量计费的标准成本口径。这里有个常见误区如果资源没打标签它们会归入一条tenant-id$unknown组必须把这个组单独设告警否则未打标的资源会悄悄流失成本归属。租户计费统一视图通常是在控制面里维护一张汇总表每晚把 Cost Explorer 的数据同步到内部数据库再按租户、按 API 调用量、按存储量展示。PPT 里提到的详细计费报告就是这张表的数据来源配合 AWS Marketplace Metering可以把 SaaS 订阅费用也并入同一条计费链路。API 级计费不能只看成本要加上应用层的计量数据例如每次请求的调用次数、消息数、数据量用明细 Billing Report 对齐。提示激活成本分配标签后历史数据不会追溯建议上线第一天就建立强制打标规范并把打标检查放进 CI 流程未通过的不允许部署。4.3 监控数据如何在租户维度做关联CloudWatch、Config、CloudTrail 三个服务要配合使用才能形成完整的租户运维链。CloudWatch 负责性能数据例如某租户在 Pool 里的 P95 延迟升高Config 负责配置漂移例如某租户的 S3 桶被改成公共读权限CloudTrail 负责操作审计例如是谁在什么时间删除了该租户的 VPC 流日志。三个服务在租户维度关联的方法是统一以 tenant-id 为线索把 CloudWatch 的维度标签、Config 的资源标签、CloudTrail 的userIdentity中的租户扩展字段格式化为同一个字段名输出到集中日志平台。PPT 里出现的 Splunk、Sumologic、Kibana 都是集中日志侧的消费端真正的标准化在产生端完成。否则三个系统各存各的租户出问题时对不上时间线追查效率极低。5. 域名解析与租户上下文传递把多租户落到请求链路5.1 子域名、路径还是自定义域名租户从浏览器输入地址到 API 返回数据第一跳就是域名解析。多租户场景下域名不仅要解析到服务端还要让服务端知道「当前是哪个租户」。三种常见模式的差异值得提前定好模式示例优点需要注意的问题子域名t12345.saas.example.comHost 头直接还原租户无需额外参数每个租户需要通配符证书DNS 条目多自定义域名app.customer-a.com客户品牌感强集成体验好需要 CNAME 绑定和网关层域名映射表路径前缀saas.example.com/t12345证书配置最简单租户信息暴露在 URL 中且容易漏传给第三方链接自定义域名的实现逻辑通常是租户在控制面添加域名系统生成一个 CNAME 记录目标到 SaaS 厂商的接入网关同时在网关里记录「域名 → 租户 ID」的映射。之后用户访问app.customer-a.com时DNS 解析到接入层接入层查映射表得到租户 ID再注入请求头向下游传递。这里 Route 53 只负责解析真正的租户识别发生在应用网关。AWS 上如果使用 API Gateway 的自定义域名需要把 ACM 证书建在 us-east-1即使服务部署在其他区域这是 CloudFront 前置的硬性要求。5.2 用 Lambda Authorizer 绑定租户上下文域名解析出租户之后不能直接信任 Host 头还要验证身份与租户是否匹配。比较推荐的做法是用 Lambda Authorizer 统一做一次「域名 Token」的双重校验因为它在进入业务 API 之前拦截隔离策略可以集中管理。示例代码如下def lambda_handler(event, context): host event[requestContext][domainName] # 自定义域名或子域名 tenant_id get_tenant_from_domain(host) # 查询控制面中的租户域名映射 token event[headers].get(Authorization, ) claims verify_jwt(token) # 使用 IdP 公钥验签 if not claims or claims.get(custom:tenant_id) ! tenant_id: raise Exception(Unauthorized) return { principalId: f{tenant_id}:{claims[sub]}, policyDocument: { Version: 2012-10-17, Statement: [{ Action: execute-api:Invoke, Resource: farn:aws:execute-api:*:*:*/*/*/{tenant_id}/*, Effect: Allow }] }, context: { tenantId: tenant_id, userId: claims[sub], role: claims.get(custom:role, member) } }get_tenant_from_domain建议走 DDB 或 ElastiCache不要在鉴权层查关系型数据库否则每个请求都增加一次 DB 查询custom:tenant_id放入 JWT 后如果用户改绑租户旧 Token 最长要等过期时间才会失效因此 Token 过期时间不应超过 15 分钟policyDocument中 Resource 的{tenant_id}是一种资源约束确保该用户只能调用自己租户的 API 路径。最后的context对象会自动传递给下游 Lambda 和 HTTP 集成业务代码直接从event.requestContext.authorizer读取租户信息不必再解析一次 JWT。这样整个链路从域名解析到业务响应租户上下文一路跟到底日志里也能统一按tenantId聚合。本文还有配套的精品资源点击获取
返回列表