ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SaaS架构设计实战:多租户隔离、计费与弹性伸缩落地指南

SaaS架构设计实战:多租户隔离、计费与弹性伸缩落地指南 简介这份《SaaS架构设计》PDF文档面向希望系统掌握SaaS架构原理与实践的开发者、架构师及技术学习者围绕如何构建高效、可扩展的多租户系统展开。内容从SaaS成熟度模型四级分级切入依次讲解RUP「41」视图模式场景、逻辑、开发、过程、物理视图、MDA模型驱动架构、系统级与程序级安全性设计并深入剖析独立数据库、共享数据库隔离数据架构、共享数据库共享数据架构三种多租户存储方案同时覆盖数据库索引优化、缓存、日志记录与加密算法等性能优化手段以及速率、并发数、吞吐量、响应时间等云计算网络性能测试指标。资源包为1个PDF文件约967KB结构紧凑、知识点密集适合作为架构设计参考手册或学习笔记。目前已有197人学习可帮助读者快速建立从需求分析、系统设计到性能优化的完整SaaS架构认知框架。1. 从一份 SaaS 架构设计文档说起多租户到底难在哪很多团队第一次做 SaaS都会先画一张“用户 → 服务 → 数据库”的架构图觉得把单体拆成微服务、把数据库换成云数据库SaaS 就成了。真上线才发现问题根本不在服务拆得够不够细而在“一套系统怎么同时服务几百上千个互不可见的租户”。这就是 SaaS 架构设计要解决的核心命题多租户隔离、按租户计费、弹性伸缩、灰度发布四件事互相牵制任何一件没设计好后面都要推倒重来。这份《SaaS架构设计.pdf》标题里的“”我理解成“叠加”——不是单纯讲 SaaS而是把多租户、可观测、成本控制这些能力叠加到一套架构里。它适合两类人一类是正在把内部系统改造成对外售卖产品的后端负责人一类是已经上线但被“大客户要独立库、小客户要便宜”逼到墙角的架构师。下面我按自己踩过坑的顺序把选型理由、落地步骤和参数配置讲清楚新手能照着搭最小可跑版本熟手能直接对边界条件。2. 多租户数据隔离三种模式怎么选、怎么落地2.1 共享库共享表、共享库独立表、独立库的取舍多租户隔离的第一道分叉是数据放哪。常见做法有三种共享库共享表每张业务表加tenant_id、共享库独立 schema、独立数据库。选型不看技术先进程度看三个硬指标租户规模、合规要求、单租户数据量。共享库共享表最省成本一台数据库能扛几千个小租户但一个慢查询会拖垮所有人且大客户往往不接受“我的数据和你隔壁公司在一张表里”。共享库独立 schema 是折中隔离性比共享表好备份恢复可以按 schema 做但连接池和迁移脚本会变复杂。独立库隔离最彻底适合金融、医疗这类有合规审计要求的客户代价是运维成本随租户数线性上涨。我的经验是默认走共享库共享表给 Top 5% 的大客户预留独立库能力。架构上不要写死而是在数据访问层做一层路由让隔离模式成为配置项。2.2 用 tenant_id 做行级隔离的最小实现下面是一个基于 Python SQLAlchemy 的租户路由最小实现核心思路是请求进来先解析租户再把租户上下文绑定到当前会话所有查询自动带上tenant_id过滤。# tenant_context.py import contextvars from sqlalchemy import event from sqlalchemy.orm import Session # 用 contextvar 保存当前请求的租户 ID避免线程间串号 current_tenant contextvars.ContextVar(current_tenant, defaultNone) def set_tenant(tenant_id: str): current_tenant.set(tenant_id) def get_tenant(): tid current_tenant.get() if tid is None: raise RuntimeError(租户上下文未设置拒绝执行查询) return tid # 给所有查询自动追加 tenant_id 过滤 event.listens_for(Session, do_orm_execute) def _add_tenant_filter(execute_state): if execute_state.is_select and not execute_state.is_column_load: tid current_tenant.get() if tid: execute_state.statement execute_state.statement.filter_by(tenant_idtid)逻辑说明contextvars在异步框架FastAPI、Sanic里比threading.local更安全因为一个线程可能处理多个协程。do_orm_execute事件在每次 ORM 查询前触发自动注入tenant_id条件业务代码里就不用每个查询都手写过滤减少漏写导致越权的风险。参数说明tenant_id建议用字符串而非自增整数方便后续从共享表迁移到独立库时保持 ID 不变filter_by要求所有业务表都有tenant_id列并建索引索引顺序建议(tenant_id, created_at)因为租户内查询通常按时间排序。提示这套自动过滤只对 ORM 查询生效手写原生 SQL 的地方必须自己加条件建议在代码审查里把裸 SQL 列为重点检查项。2.3 独立库租户的连接路由与迁移当某个租户升级到独立库数据访问层要能按租户切换连接。常见做法是维护一张租户路由表记录tenant_id → db_dsn连接池按 DSN 缓存。# db_router.py from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker _engines {} def get_session(tenant_id: str, dsn: str): # 按 DSN 缓存引擎避免每个请求都新建连接池 if dsn not in _engines: _engines[dsn] create_engine(dsn, pool_size5, max_overflow10, pool_pre_pingTrue) SessionLocal sessionmaker(bind_engines[dsn]) return SessionLocal()逻辑说明pool_pre_pingTrue很关键独立库可能因为网络抖动断连没有这个参数会拿到失效连接报错。pool_size不要设太大独立库租户数量多时总连接数 租户数 × pool_size容易打满数据库的max_connections。参数说明小租户共享库的pool_size可以设 1020独立库租户建议 5 以内迁移时用双写 校验的方式先写共享库再写独立库比对一致后切读流量最后停双写。这个迁移脚本我一般会单独写一个校验任务按tenant_id分批比对行数和关键字段的 checksum。3. 计费与配额把用量采集做成可审计的流水3.1 计量点设计API 调用、存储、 seats 怎么统计SaaS 计费的前提是“可计量”。常见计量维度有三类API 调用次数、存储用量、活跃席位seats。API 调用最好在网关层埋点因为网关是所有流量的必经之路存储用量适合每天凌晨跑一次统计任务按租户汇总对象存储和数据库的实际占用seats 则依赖用户表的活跃状态通常按“过去 30 天有登录”来算。计量数据不能只存一个汇总值要存成流水因为客户会质疑“为什么这个月比上个月贵”。流水表建议包含tenant_id、metric_type、quantity、occurred_at、request_id其中request_id用于和网关日志对账。3.2 用 Redis 做实时配额扣减与防超卖配额扣减要在高并发下保证不超卖常见做法是用 Redis 的原子操作。下面是一个按租户 计量项做每日配额扣减的 Lua 脚本方案。-- quota_deduct.lua -- KEYS[1]: 配额 key如 quota:{tenant_id}:{metric}:{date} -- ARGV[1]: 本次扣减量 -- ARGV[2]: 配额上限 local used tonumber(redis.call(GET, KEYS[1]) or 0) local delta tonumber(ARGV[1]) local limit tonumber(ARGV[2]) if used delta limit then return -1 -- 超配额 end redis.call(INCRBY, KEYS[1], delta) redis.call(EXPIRE, KEYS[1], 172800) -- 保留 2 天方便对账 return used delta逻辑说明Lua 脚本在 Redis 里单线程执行GET和INCRBY之间不会被其他请求插入天然防超卖。返回-1表示超配额业务层据此返回 429 或提示升级套餐。参数说明EXPIRE设 172800 秒2 天是为了留出对账窗口过期后数据从流水表恢复配额上限ARGV[2]建议从配置中心读取避免改配额要重启服务。注意 Redis 如果做了集群同一个租户的 key 要落在同一个 slot可以用{tenant_id}做 hash tag。注意Redis 扣减是“预扣”最终账单要以流水表为准。我见过只信 Redis 计数、结果 Redis 故障后配额全乱的翻车案例后悔药就是每天把 Redis 计数和流水表做一次对账。3.3 账单生成的幂等与对账账单生成任务必须幂等否则重跑一次就多出一张账单。常见做法是用(tenant_id, billing_period)做唯一索引插入时用INSERT ... ON CONFLICT DO NOTHING或者先查后插加分布式锁。对账环节要把网关日志、Redis 计数、流水表三方比对差异超过阈值就告警。这一步不做客户投诉时你连“到底用了多少”都说不清。4. 弹性与隔离别让一个大客户拖垮所有人4.1 按租户限流令牌桶的粒度与参数SaaS 架构里限流不能只按 IP 或接口要按租户。常见做法是在网关层为每个租户维护一个令牌桶桶容量和补充速率从租户套餐读取。免费租户可能 10 QPS企业租户 1000 QPS。# tenant_limiter.py import time from redis import Redis r Redis() def allow(tenant_id: str, rate: int, burst: int) - bool: key frl:{{{tenant_id}}} now time.time() # 用 Redis 有序集合存令牌时间戳滑动窗口 pipe r.pipeline() pipe.zremrangebyscore(key, 0, now - 1) pipe.zcard(key) pipe.zadd(key, {str(now): now}) pipe.expire(key, 2) _, count, _, _ pipe.execute() return count burst逻辑说明这里用滑动窗口近似令牌桶zremrangebyscore清理 1 秒前的记录zcard统计当前窗口内请求数。相比固定窗口滑动窗口不会在窗口切换时出现双倍流量。参数说明rate是每秒允许请求数burst是突发上限一般设rate * 2key 用{tenant_id}做 hash tag 保证集群下同租户落同一节点。限流触发后返回 429 并带Retry-After头客户端才好做退避。4.2 资源隔离命名空间与节点池光限流不够大租户的慢查询、大文件导出还是会抢资源。常见做法是把租户分成几个等级不同等级跑在不同的节点池免费和小客户共享节点池企业客户独占节点池。Kubernetes 里可以用 namespace ResourceQuota LimitRange 做基础隔离再配合节点亲和性把企业租户调度到专属节点。租户等级节点池CPU 配额内存配额是否独占免费shared0.5 核512Mi否标准shared2 核2Gi否企业dedicated8 核16Gi是这张表不是拍脑袋是我按“一个企业租户的峰值 ≈ 20 个标准租户”估算的。实际配置要压测后调整压测时重点看 P99 延迟和数据库连接数。4.3 灰度发布按租户切流量的三种方式SaaS 灰度不能按机器切要按租户切。常见三种方式按租户 ID 白名单、按租户等级、按流量百分比。白名单适合内部测试租户等级适合先让免费租户试新版本百分比适合大规模验证。实现上可以在网关的路由规则里加tenant_id匹配条件把命中规则的请求转发到新版本服务。回滚时只改路由规则不用重新发版这是 SaaS 相比单机系统最大的运维优势。5. 避坑与排查多租户系统最容易翻车的五件事5.1 租户上下文丢失导致越权现象A 租户的用户查到了 B 租户的数据且偶发不是每次都出现。原因异步任务或线程池里没有传递租户上下文contextvars在新线程里是空的查询没带tenant_id过滤。解决所有异步入口显式set_tenant线程池提交任务时用copy_context().run包装并在数据访问层加“租户为空则拒绝查询”的兜底。5.2 共享表索引没带 tenant_id 导致全表扫描现象单租户查询慢数据库 CPU 飙高。原因索引建在created_at上查询条件tenant_id ? AND created_at ?用不上索引前缀。解决把联合索引改成(tenant_id, created_at)并用EXPLAIN确认执行计划走了索引。这个坑我在三个项目里都遇到过血泪经验是建表时就定好索引规范。5.3 配额扣减和账单对不上现象客户说没用那么多账单却超了。原因Redis 扣减成功但流水写入失败或者重试导致重复扣减。解决扣减和流水写入放在同一个事务性消息里用request_id做幂等每天跑对账任务差异超过 1% 就告警。5.4 独立库租户迁移时连接数打满现象迁移几个大客户后数据库报too many connections。原因每个独立库租户一个连接池租户数一多总连接数超限。解决独立库租户的pool_size降到 23用 PgBouncer 做连接池代理或者把不活跃租户的连接池回收。5.5 灰度发布时新旧版本数据结构不兼容现象灰度期间部分租户报错回滚后正常。原因新版本加了字段旧版本代码不认识或者新版本改了字段含义。解决数据库变更遵循“先加后用、先兼容再清理”灰度前用影子库跑一遍新旧代码确认双向兼容再切流量。6. 进阶把租户成本算清楚架构才站得住SaaS 架构设计到最后拼的不是技术多先进而是“每个租户到底花了我多少钱”。我一般会做一个租户成本看板把计算、存储、网络、数据库连接按租户维度分摊。分摊逻辑不复杂计算按 CPU 使用时长 × 单价存储按实际占用 × 单价网络按出流量 × 单价。难点在数据采集容器环境里可以用 cgroup 指标按 namespace 聚合数据库可以用pg_stat_statements按tenant_id归集查询耗时。有了成本数据定价和架构决策才有依据。比如发现某个租户的存储成本是收入的 3 倍就该推动他清理冷数据或升级套餐发现共享库的某个大租户占了 40% 的 IO就该把他迁到独立库。下面这个查询用来按租户统计慢查询占比我一般每周跑一次。-- 按租户统计慢查询次数和平均耗时 SELECT tenant_id, COUNT(*) AS slow_queries, AVG(duration_ms) AS avg_duration_ms, SUM(duration_ms) AS total_duration_ms FROM query_log WHERE duration_ms 500 AND occurred_at NOW() - INTERVAL 7 days GROUP BY tenant_id ORDER BY total_duration_ms DESC LIMIT 20;逻辑说明duration_ms 500是慢查询阈值按租户聚合后排序能快速定位“谁在拖垮数据库”。参数说明阈值按业务调整OLTP 系统一般 200500ms分析型可以放宽到 2soccurred_at加索引避免全表扫描。验证架构是否合理我有个习惯随机挑一个租户从请求入口到数据库查询把链路上每个环节的租户标识打出来确认没有一处丢失。这个动作我称为“租户链路走查”新版本上线前必做。另一个习惯是每次扩容前先看租户成本看板确认扩容是因为真实增长还是某个租户的异常用量。这些习惯帮我避免了好几次“盲目加机器但问题依旧”的尴尬。希望帮到你。本文还有配套的精品资源点击获取
返回列表