企业出海 SaaS 多租户架构设计:Pool/Bridge/Silo 隔离模型 + vCluster 虚拟集群 + 行级数据隔离实战(2026最新版)

📅 · 企业出海多云架构实战 - ChengziCloud

一句话结论

Meta Description: 2026 出海 SaaS 多租户架构指南:Pool/Bridge/Silo 三种隔离模型、vCluster 虚拟集群、ResourceQuota 与 NetworkPolicy、PostgreSQL 行级安全、噪声邻居治理与三云能力对比,附 90 天落地路线。

本文要点
  • 先说结论:多租户不是"加个 tenantid",而是四道边界一起划
  • 三种部署模型:Pool / Bridge / Silo 与那条"隔离强度 × 成本 ≈ 常数"的曲线
  • 判定表:你的产品该停在哪一档
  • 计算隔离 L1:namespace + ResourceQuota + LimitRange
  • 计算隔离 L2:vCluster 虚拟集群(namespace 之后的下一步)
  • 网络隔离:默认拒绝 + 显式放行 + 别忘了 DNS
  • 数据隔离:三种模式与那张必须一起看的对比表
  • 对象存储与文件:前缀不是隔离,策略才是

Meta Description: 2026 出海 SaaS 多租户架构指南:Pool/Bridge/Silo 三种隔离模型、vCluster 虚拟集群、ResourceQuota 与 NetworkPolicy、PostgreSQL 行级安全、噪声邻居治理与三云能力对比,附 90 天落地路线。

关键词: 多租户架构、租户隔离、vCluster、行级安全 RLS、SaaS 出海


一、先说结论:多租户不是"加个 tenant_id",而是四道边界一起划

多租户架构最容易犯的错,是把它当成一个数据库字段问题——表里加个 tenant_id,查询带上 where tenant_id = ?,就当隔离做完了。真出海跑起来,出事的往往不是"数据查错了",而是某个大客户的批量任务把整个集群的 CPU 吃满、某个租户的 Pod 通过内网访问到了另一个租户的缓存、一个漏写 where 的报表 SQL 把 300 家客户的数据导进了同一份 CSV。

所以本文的定义是:多租户 = 计算、网络、数据、身份四条边界同时成立,任何一条缺失都等于没有隔离。

先把本文与站内既有文章的边界划清,避免重复造轮子:

既有文章它解决的问题本文的位置
09-18 多云管理平台 CMP平台自身的租户与权限模型(谁能申请什么资源)本文讲产品侧租户之间的隔离(客户 A 碰不到客户 B)
10-02 单元化架构故障影响面收敛(Cell 爆炸半径)租户是分片维度,单元是故障域;两者可叠加但不是一回事
08-19 容器运行时安全不可信代码的内核级隔离(gVisor/Kata)本文是逻辑隔离,沙箱是最后一道物理边界
08-25 零信任网络身份到流量的持续验证本文只取其中"租户边界"这一层做落地
09-23 数据治理元数据、血缘、质量、数据网格本文只讲一行数据的归属与可见性
10-14 K8s 成本治理工作负载级成本归因本文把归因对象从"团队"换成"租户",用于分账
09-10 对象存储跨云数据湖的桶与生命周期本文讲桶内前缀级租户隔离与临时凭证

一句话记法:CMP 管"你能申请什么",本文管"你只能看见你自己的"。


二、三种部署模型:Pool / Bridge / Silo 与那条"隔离强度 × 成本 ≈ 常数"的曲线

业界最通用的分类来自 AWS Well-Architected 的 SaaS Lens:Silo(每个租户一套独立基础设施)、Pool(全部租户共享一套)、Bridge(混合,按租户分档)。这三者不是"先进与落后"的关系,而是同一条成本-隔离曲线上的三个点。

维度Silo 全隔离Pool 全共享Bridge 混合分层
计算每租户独立集群/账号共享集群,namespace 隔离大客户 Silo,长尾 Pool
数据每租户独立库/实例共享库 + tenant_id + RLS分档:独立库 or 共享表
单位成本最高(利用率最低)最低(规模效应最强)居中,可控
故障影响面单租户全租户按档位
单租户定制自由(可改版本)极难(版本必须统一)大客户可定制
运维复杂度随租户数线性膨胀恒定双轨并行,最考工程能力
典型适用金融、医疗、政企、强合规PLG 型 SaaS、中小客户长尾出海 SaaS 的默认答案

核心判断:只要你的客户里同时存在"年付 50 万美元要求独立部署"和"月付 99 美元自助注册",你就已经身处 Bridge 模型了——问题不是要不要选,而是分档线画在哪、以及两档能不能共用一套发布流程。


┌──────────────────────── Bridge 混合模型(出海 SaaS 常见形态)────────────────────────┐
│                                                                                      │
│   ┌──────────── Silo 档(企业客户 / 强合规)────────────┐   ┌──── Pool 档(长尾)───┐ │
│   │  ┌──────────────┐        ┌──────────────┐          │   │  ┌─────────────────┐  │ │
│   │  │ 独立 VPC/账号 │        │ 独立 VPC/账号 │          │   │  │ 共享集群          │  │ │
│   │  │ ┌──────────┐ │        │ ┌──────────┐ │          │   │  │ ┌─────┐ ┌─────┐  │  │ │
│   │  │ │ 独立 DB   │ │        │ │ 独立 DB   │ │          │   │  │ │ns: A│ │ns: B│  │  │ │
│   │  │ └──────────┘ │        │ └──────────┘ │          │   │  │ └─────┘ └─────┘  │  │ │
│   │  └──────────────┘        └──────────────┘          │   │  │ 共享 DB + RLS     │  │ │
│   └────────────────────────────────────────────────────┘   │  └─────────────────┘  │ │
│                                                            └───────────────────────┘ │
│        统一控制面:租户目录 / 计费 / 发布流水线 / 审计日志(必须一套,不能两套)           │
└──────────────────────────────────────────────────────────────────────────────────────┘

这套模型的四层隔离架构可以画成一张依赖图——越往下越硬,越难临时补:


graph TD
    A[身份边界: JWT tenant_id / 网关校验] --> B[网络边界: NetworkPolicy + egress 白名单]
    B --> C[计算边界: namespace / vCluster / 配额]
    C --> D[数据边界: RLS / schema / 独立库]
    D --> E[物理边界: 沙箱 / 独立集群 / 独立账号]
    A -.漏了.-> X[越权读别人的数据]
    B -.漏了.-> Y[横向移动到别人的缓存/中间件]
    C -.漏了.-> Z[噪声邻居打满全站]
    D -.漏了.-> W[一条漏写 where 的 SQL 泄露全量]

三条铁律,先记住再往下看:

  1. tenant_id 必须在请求入口注入,不能由客户端传——任何"相信 Header 里的 X-Tenant-Id"的实现都是漏洞。
  2. 默认拒绝——网络、权限、数据可见性,初始状态一律是"看不见",靠显式授权打开。
  3. 隔离等级要与客单价匹配——给月付 99 美元的客户做独立集群,是在用毛利补贴架构洁癖。

三、判定表:你的产品该停在哪一档

不要凭感觉选,用下面六个问题打分(每命中一条 +1):

判据命中说明
客户合同里出现"数据不得与其他客户共库"法务/合规硬约束
需要按租户单独升级版本(客户拒绝跟随主线)版本碎片化已发生
单租户峰值会打满共享集群 20% 以上资源噪声邻居风险高
需要提供租户级 RTO/RPO 承诺(如 15 分钟)故障域必须切开
有客户要求指定地域/指定机房驻留数据主权约束
客户会做渗透测试并要求出具隔离证明举证责任在你这侧
  • 0–1 条:Pool 起步,把 tenant_id 与配额从第一天做对即可。
  • 2–3 条:Bridge,按"是否命中前两条"划出 Silo 档。
  • 4 条以上:默认 Silo,Pool 只留给自助注册的试用租户。

最贵的错误不是选错档,而是没有"分档线"这个概念——所有租户挤在 Pool 里,等第一个大客户提出独立部署时,才发现代码里到处是"假设只有一个库"。


四、计算隔离 L1:namespace + ResourceQuota + LimitRange

共享集群里最先要做、性价比最高的三件事,全在原生 API 内,不需要装任何东西。

4.1 三者的分工(最常被混为一谈)

对象管什么不管什么典型误用
Namespace逻辑分组 + RBAC 作用域 + 网络策略作用域不提供任何资源上限以为分了 ns 就隔离了
ResourceQuota命名空间总量上限(申请值/上限值/对象数)不约束单个 Pod 的形态只设 requests 不设 limits
LimitRange单个 Pod/容器的默认值与上下限不约束命名空间总量设了 LimitRange 就以为有配额

一句话:Quota 管"这一户总共能用多少",LimitRange 管"这一户里单个容器最大能多大"。 缺前者会被一个租户吃满集群,缺后者会有一个 Pod 申请 64 核把调度器玩坏。

4.2 租户命名空间的完整基线


apiVersion: v1
kind: Namespace
metadata:
  name: tenant-acme-prod
  labels:
    tenant: acme                    # 租户标识,用于配额模板与计费归因
    tier: silo                      # 档位:silo / pool
    env: prod
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-quota
  namespace: tenant-acme-prod
spec:
  hard:
    requests.cpu: "40"
    requests.memory: 80Gi
    limits.cpu: "80"
    limits.memory: 160Gi
    pods: "200"
    services.loadbalancers: "2"     # 防止租户随手开一堆 LB 烧钱
    services.nodeports: "0"         # NodePort 直接禁掉,出口统一走 Ingress
    persistentvolumeclaims: "20"
    requests.storage: 2Ti
    count/jobs.batch: "30"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: tenant-defaults
  namespace: tenant-acme-prod
spec:
  limits:
    - type: Container
      default:                      # 未写 limits 时的兜底
        cpu: 500m
        memory: 512Mi
      defaultRequest:               # 未写 requests 时的兜底
        cpu: 100m
        memory: 128Mi
      max:                          # 单容器上限,防"一个 Pod 申请 64 核"
        cpu: "16"
        memory: 64Gi
      min:
        cpu: 50m
        memory: 64Mi

三个实测坑,写进你的入职文档:

  • Quota 不会驱逐已存在的 Pod。调低配额后,超额部分照跑,只是新建被拒。要真正收敛,得配合 kubectl drain 或滚动重启。
  • 设了 limits 不设 requests 时,K8s 会把 requests 默认成 limits(若 LimitRange 提供了 defaultRequest 则以它为准),这会让调度器按峰值预留容量,集群看起来"很空但调度不上"。
  • services.loadbalancers 与 nodeports 是省钱配额,不是安全配额。真正防横向移动的是下一节的 NetworkPolicy。

4.3 把配额做成模板,而不是手抄

二十个租户靠手写 YAML 一定会漂移。用 Kustomize 的 replacements 或 Helm 的 values 生成,把"每租户资源档位"变成一张表:

档位requests.cpulimits.cpuPod 上限适用
starter4830试用/月付长尾
growth1632100中型客户
enterprise4080200大客户(共享集群内)

配合 kubectl create quota 做不到模板化,正确做法是把它放进 Git,用 Argo CD/Flux 按租户目录渲染——租户开通 = 一次 PR,这样每一次隔离变更都有 diff 可审。


五、计算隔离 L2:vCluster 虚拟集群(namespace 之后的下一步)

当"共享 namespace"开始不够用时(租户要装自己的 CRD、要自己的 RBAC 管理员、要自己的 Kubernetes 版本),最省力的中间档是虚拟集群。

5.1 它到底隔离了什么

vCluster 在宿主集群的一个 namespace 里跑一个轻量控制面(默认 k3s,也支持 k0s),租户拿到的是一个独立的 API Server:自己的 CRD、自己的 RBAC、自己的 Service CIDR、自己的 kubectl get nodes 视图。工作负载最终以 Pod 形式落在宿主 namespace 中,由宿主调度。

能力共享 namespacevCluster 虚拟集群独立物理集群
租户可自建 CRD❌ 会污染全集群✅ 隔离在虚拟 API 内✅
租户可做 cluster-admin❌ 等于给了全集群✅ 只在自己虚拟集群内✅
内核/容器运行时隔离共享共享(这点常被误解)独立
单租户控制面成本0低(单副本即可)高(含控制面费用)
跨租户网络默认拒绝需手写 NetworkPolicy需手写(同一宿主网络)天然
适用长尾小客户中大型客户、需自定义扩展强合规、强隔离

最关键的一句:vCluster 提升的是控制面隔离与"Kubernetes 体验隔离",不是内核隔离。租户的 Pod 仍与别人共享宿主节点内核——需要内核级隔离时,回到 08-19 的沙箱方案(RuntimeClass 定向到 gVisor/Kata)。

5.2 落地命令(OSS 版,不依赖商业平台)


helm repo add loft https://charts.loft.sh
helm repo update
vcluster create acme-prod -n host-acme --namespace acme-prod --connect=false
kubectl get ns host-acme
kubectl -n host-acme get pods
vcluster connect acme-prod -n host-acme
kubectl get nodes
kubectl get crd

前两条装 CLI/仓库,第三条在宿主 namespace host-acme 里创建虚拟集群,第四五条验证宿主侧资源,第六七条切换 kubeconfig 到虚拟集群内验证"租户视角"——如果 kubectl get nodes 在虚拟集群里看到的是宿主节点名,说明隔离只到 API 层,这是预期行为,不是配置错误。

三个运维要点:

  • 宿主 namespace 仍要配额。虚拟集群自己不会限制总用量,必须在 host-acme 上套 ResourceQuota,否则"虚拟"只是幻觉。
  • 宿主侧要拒绝 hostPath/hostNetwork/privileged。用 Pod Security Admission(pod-security.kubernetes.io/enforce: baseline 或 restricted)打在宿主 namespace 上,这是虚拟集群最大的逃逸面。
  • 备份要分两层:虚拟集群的 etcd(业务 CRD/ConfigMap)+ 宿主侧的 PV。只备份一边,恢复时会缺对象。

5.3 Capsule:不想引入第二套控制面时的轻量替代

Capsule(Clastix 开源)走另一条路:不创建新 API Server,而是用 Tenant CRD 把"一组 namespace + 配额 + 网络策略 + 镜像仓库白名单 + Pod 安全等级"打包,由租户 owner 自助创建 namespace。

方案机制适合
CapsuleTenant CRD 编排原生 namespace(不新增控制面)只需"自助开 ns + 强制配额",运维最轻
vCluster独立虚拟 API Server租户要 CRD/自定义控制器/cluster-admin
HNC(Hierarchical Namespace)namespace 树 + 策略继承组织层级深、需父子继承配额

两者可叠加:Capsule 管"谁能开 namespace",vCluster 给需要重度自定义的租户。


六、网络隔离:默认拒绝 + 显式放行 + 别忘了 DNS

数据泄露事故里,比"SQL 查错"更常见的是网络可达——租户 A 的 Pod 直接连上了租户 B 的 Redis,因为两者在同一 VPC 的同一个扁平网段里。

6.1 两条基线策略


apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: tenant-acme-prod
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-and-same-namespace
  namespace: tenant-acme-prod
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector: {}
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    - to:
        - podSelector: {}

第一条是"先关死":命名空间内所有 Pod 的进出流量全拒。第二条是"按需开":只允许同 namespace 内互访与访问 kube-dns。

这里有一个 100% 会踩的坑:默认拒绝 egress 后,如果没显式放行 53 端口,所有域名解析都会失败,报错是 no such host / i/o timeout,看起来像"网络不通",实际是 DNS 被自己的策略拦了。线上事故里这条能占到一半。

6.2 南北向与出网:租户级出口

方向推荐做法反模式
用户 → 租户服务统一 Ingress/网关按 Host 或路径分流,TLS 在入口终止每租户开一个公网 LB(成本爆炸 + 攻击面)
租户服务 → 公网共享 NAT 网关 + 出网白名单(域名/FQDN 或固定 IP 段)给租户 Pod 直接挂 EIP
租户 → 云服务(对象存储/DB)VPC 端点(PrivateLink/终端节点),流量不出 VPC走公网绕一圈
租户 A → 租户 B默认拒绝,仅当业务确有需要时用显式策略开单向同 VPC 全通 + 靠"约定"约束

出网白名单是很多出海团队的盲区:租户的服务会调第三方 API(支付、短信、地图),如果共享 NAT 且不限出网,一旦某个租户的服务被入侵,攻击者可以把它当跳板访问你的内网其他部分。出网策略要按"域名 + 端口"而不是"IP 段"来写——云厂商 IP 会漂移,域名不会。

6.3 三个常被忽略的横向通道

  • 共享中间件:所有租户连同一个 Kafka/RabbitMQ 时,必须用 vhost/topic 前缀 + 独立账号做 ACL,仅靠"不同 topic 名"不是隔离。
  • 共享缓存:Redis 的 SELECT db 不是隔离(运维一条 KEYS * 全看得见),要用独立实例或 ACL 用户 + key 前缀强校验。
  • 服务网格:Istio 的 AuthorizationPolicy 能按 source.namespaces 限制东西向,但它只作用于经过 sidecar 的流量,直连 Pod IP 仍要看 NetworkPolicy。

七、数据隔离:三种模式与那张必须一起看的对比表

这是多租户里唯一"改起来最贵"的一层。选型必须在写第一张表之前定,事后迁移的成本是设计期的十倍。

维度共享表 + tenant_id(+RLS)Schema-per-tenantDB/实例-per-tenant
隔离强度逻辑(依赖 SQL 正确性)中(schema 边界 + 权限)强(连接串就是边界)
租户数量上限十万级数百~数千(迁移与连接数受限)数十~数百
单租户数据导出需按 tenant_id 全表扫pg_dump -n schema 一行直接 dump 实例
大租户性能影响有(同表、同索引、同 buffer)较小(索引独立)无
迁移/DDL一次N 次(要遍历所有 schema)N 次(要遍历所有实例)
连接数省(一个池)中爆(每实例都要池)
合规举证弱(需自证 where 写对了)中强(物理可举证)

选择口诀:长尾用共享表 + RLS,中大客户用 schema,强合规客户用独立实例——这正好对应第二节的 Bridge 分档。

7.1 共享表 + PostgreSQL 行级安全(RLS)

只靠应用层写 where tenant_id 是不可靠的:一个报表导出、一个后台脚本、一个新同事的裸 SQL 就能绕过。RLS 把这条约束下沉到数据库,即使 SQL 忘了写 where,也查不到别人的行。


ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
  WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);

CREATE INDEX CONCURRENTLY idx_orders_tenant_created
  ON orders (tenant_id, created_at DESC);

四行 SQL,四个必须理解的细节:

  • FORCE ROW LEVEL SECURITY:默认情况下表 owner 绕过 RLS。如果你的应用用 owner 账号连库(很常见),不写 FORCE 等于策略不存在。
  • current_setting(..., true):第二个参数 missing_ok=true。不传它,未设置变量时直接报错;传了则返回 NULL,结果是"查不到任何行"——失败方向是安全的,这比报错更适合做兜底。
  • 超级用户与 BYPASSRLS 角色不受约束:应用账号必须是普通角色,别图省事给它 SUPERUSER。
  • WITH CHECK 管写入,USING 管读取:只写 USING 会导致"读不到但能写进去别人的行",是最隐蔽的一种半吊子配置。

7.2 连接池的致命细节:SET 泄漏到别的租户

租户标识通过会话变量传递:


SET LOCAL app.tenant_id = '3f2a1c88-...';
SELECT * FROM orders WHERE status = 'paid';

必须用 SET LOCAL,不能用 SET。 原因:

  • SET 是会话级,连接归还池子后变量仍然存在;下一个借到这条连接的请求如果没设置(走了一条不经过中间件的分支),就会继承上一个租户的身份——这是最典型、也最难复现的跨租户数据泄露。
  • SET LOCAL 只在当前事务内有效,事务结束自动失效,且必须包在显式事务里(autocommit 下 SET LOCAL 几乎立刻失效)。

配合 PgBouncer 时还要注意:transaction pooling 模式下会话状态不保证,SET 的语义会漂移,只有 SET LOCAL 在事务内是安全的。如果框架不好改,就在连接建立回调里显式 RESET ALL,把风险降到可控范围。

7.3 schema-per-tenant 的两个隐性成本

  • DDL 扇出:一次加字段要遍历 N 个 schema,且必须可重入、可续跑。用 flyway/golang-migrate 的 per-schema 版本表,配合一个 for 循环脚本,别手动点。
  • search_path 注入:切换租户靠 SET search_path TO tenant_x,同样是会话状态问题——必须 SET LOCAL,且 search_path 里不能包含 public,否则未限定的表名会落到公共 schema,形成越权读。

7.4 数据层隔离的三条验收标准

  1. 用一个没有 tenant_id 条件的 SQL 去查,返回 0 行(而不是报错,也不是全量)。
  2. 用租户 A 的凭证写一条 tenant_id 为 B 的记录,被 WITH CHECK 拒绝。
  3. 在连接池里连续跑 A → B → A 三个请求,用 pg_stat_activity 确认没有残留的会话变量。

八、对象存储与文件:前缀不是隔离,策略才是

数据库做完了,文件和对象常常是最后漏掉的一环。S3/OSS/COS 的"每个租户一个前缀"只是命名约定,真正的边界是 IAM 条件。


{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::acme-saas-files/*",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["${aws:PrincipalTag/tenant_id}/*"]
        }
      }
    }
  ]
}

要点:

  • ${aws:PrincipalTag/tenant_id} 来自会话标签,不是客户端随便传的。用 STS AssumeRole 时通过 --tags Key=tenant_id,Value=acme 打上,且信任策略里必须允许 sts:TagSession,否则标签根本带不进去(这个报错信息很不友好,容易查半天)。
  • 不要让租户拿到长期 AK。正确链路是:登录态 → 后端 AssumeRole(带租户标签)→ 返回 15 分钟~1 小时的临时凭证。
  • 预签名 URL 的过期时间设短(S3 SigV4 上限 7 天),且签名时要绑定 Content-Type 与大小,避免被当成上传任意文件的入口。
  • 列桶(ListBucket)权限单独收紧:只给 GetObject/PutObject 而禁 ListBucket 时,租户无法枚举别人的前缀,这是一个成本极低但收益很高的开关。

九、身份边界:五个必须堵死的越权口子

前面四层都做对了,只要入口这一层信错了东西,全部白做。

口子表现正确做法
信任客户端 Header后端读 X-Tenant-Id 决定查哪个库只从已验签的 JWT claim 取值,网关剥离外来的同名 Header
只校验登录不校验归属登录成功后 /api/orders/{id} 直接按 id 查,不校验该订单属于当前租户每次资源访问做归属校验(或让 RLS 兜底)
管理端与租户端共用接口运营后台的 ?tenant_id= 参数被前端可改管理端走独立路由 + 独立鉴权,参数只从管理员上下文取
JWT 里 tenant_id 可伪造用了 alg: none 或未校验签名固定算法白名单、校验 iss/aud/exp、密钥轮换
缓存 key 不含租户cache:get:order:123 跨租户命中key 前缀强制 t:{tenant_id}:,在缓存客户端封装层统一注入

这一整类问题在 OWASP Top 10(2021)里叫 A01 失效的访问控制,长期排在第一。它对多租户产品的杀伤力尤其大,因为一个越权 bug 等于一次跨客户数据泄露,而不是"一个用户看到了别人的一条记录"。

工程上最省力的兜底,是把"归属校验"变成框架级能力:数据访问层强制要求一个 TenantContext,缺失即抛异常(fail closed),而不是默默用 null 去查。


十、噪声邻居:多租户特有的性能故障

单租户系统里没有这类故障。多租户里它是头号 P1 来源,而且症状极具迷惑性:监控上看是"全站变慢",根因是某一个租户跑了一个全表扫描的报表。

噪声类型打爆什么手段
CPU 密集(批量计算/视频转码)节点 CPU、调度延迟配额 + 独立节点池 + 优先级
内存增长(缓存无界/大对象)节点 OOM、连带驱逐邻居 Podlimits 必设 + 独立节点池
IO 密集(全表扫/大文件)共享存储 IOPS、数据库 buffer独立只读副本 + 查询超时 + 资源组
连接数(连接池打满)数据库连接上限每租户连接上限 + 代理层排队
请求风暴(重试/爬虫)网关、下游限流租户级限流(令牌桶)
大 key / 大对象缓存与网络抖动单次返回大小上限 + 分页强制

三道防线,按成本从低到高:

  1. 配额与限制(第 4 节):让"打满"变成"打不满",但配额是静态的,挡不住瞬时脉冲。
  2. 优先级与抢占:给核心租户的负载设更高 PriorityClass,让低优先级批处理先被抢占——这是 K8s 原生能力,很多团队完全没用。
  3. 物理隔离节点池:把最大的 3~5 个租户的负载用 nodeSelector/taints 固定到专属节点池。贵,但比赔 SLA 便宜。

监控上要有的四个租户维度指标(普通监控里通常缺失):

指标含义告警建议
tenant_cpu_usage_ratio租户实际用量 / 其配额> 85% 持续 30 分钟
tenant_throttled_secondsCFS 限流累计时长环比突增 3 倍
tenant_p99_latency按租户切分的接口延迟与自身 7 天基线比 > 2 倍
tenant_error_budget_burn租户级错误预算燃烧率1 小时燃烧 > 5%

关键设计:告警必须能回答"是哪个租户在影响别人",而不只是"集群 CPU 高"。这要求从 Pod 到 namespace 到租户的三级映射在指标里始终是完整标签——tenant 标签一旦缺失,事后只能靠猜。


十一、三云托管能力对比:三朵云都没有"租户"这个一等公民

维度AWS(EKS)阿里云(ACK)腾讯云(TKE)
原生租户对象❌ 无(namespace + RBAC)❌ 无❌ 无
命名空间级配额✅ ResourceQuota✅ ResourceQuota✅ ResourceQuota
网络策略✅ 需 CNI 支持(VPC CNI 需启用相应能力,或装 Calico/Cilium)✅ Terway 支持 NetworkPolicy✅ 需确认网络插件能力(GlobalRouter 常需自装 Cilium/Calico)
虚拟集群✅ 可自建 vCluster(OSS)✅ 可自建 vCluster✅ 可自建 vCluster
多集群统一视图✅ EKS + Argo CD / Fleet 类方案✅ ACK One 多集群管理✅ TKE 集群联邦/多集群管理
租户级成本归因✅ 分账数据 + OpenCost/Kubecost✅ ACK 成本洞察(按命名空间)✅ TKE 成本洞察(账单下钻)
审计日志含租户线索✅ CloudTrail + 请求级日志✅ ActionTrail✅ CloudAudit
Pod 安全基线✅ Pod Security Admission✅ 安全策略/PSP 替代方案✅ 类似机制
内核级隔离✅ 支持 gVisor/Kata(自管节点)✅ 支持 sandboxed 容器✅ 支持

三条结论:

  1. 别等云厂商给你"租户"对象。三家的能力上限都是"namespace + 配额 + 策略",租户这层抽象必须你自己建(Capsule/vCluster/自研 operator 三选一)。
  2. 网络策略的可用性取决于网络插件,这是三云最容易踩差异的地方。上线前必须在测试集群用一条 default-deny + 一个探针 Pod 实测,别信文档描述。
  3. 审计日志要能把操作映射到租户。三云的审计都只到"谁调了什么 API",租户归属需要你在标签或资源命名里带出来,否则出事时无法回答"这个变更影响了哪些客户"。

十二、成本:多租户的账要算"隔离溢价"

隔离不是免费的,但也不是线性的。用一组示意算例说明量级(单价按站内 10-14 篇的折算口径派生,仅作口径演示,不构成报价):

方案控制面成本每租户增量100 租户月成本量级
共享 namespace0(复用集群)≈ 0仅业务负载
vCluster(每租户一个)0(OSS)约 0.1~0.2 vCPU / 128~256 MiB(经验区间,需实测)约 10~20 vCPU 额外开销
独立集群(每租户一个)EKS 控制面 0.10 USD/小时 ≈ 73 USD/月至少 1 个 2 vCPU/4 GB 节点 ≈ 51 USD/月(0.029×2 + 0.003×4 = 0.070 USD/小时 × 730)≈ 12,400 USD/月(仅控制面 + 最小节点)

由表可得三条判断:

  • Silo 档的成本几乎全部由"最小可用规格"决定,而不是由负载决定——100 个租户就是 100 份控制面费用,跟客户用了多少无关。这就是为什么 Silo 档必须对应高客单价。
  • vCluster 的溢价主要是"每租户一份控制面副本"的资源,通常在个位数百分比,是性价比最高的中间档。
  • 共享 namespace 的隐性成本在别处:噪声邻居导致的 SLA 赔付、越权事故的处置成本、以及"一个租户要独立部署时重构"的一次性投入。把这些算进去,很多团队会发现自己"省下来的钱"其实没省。

分账落地接 10-14 的方案:OpenCost/Kubecost 按 namespace 聚合,把 tenant 标签作为第一聚合维度,再叠加"共享组件(Ingress、监控、日志)按用量比例分摊"的规则——不分摊共享组件,租户看到的成本会系统性偏低。


十三、合规视角:删除、审计与举证

多租户把三个合规动作的难度放大了:

动作难点做法
租户数据删除(DSAR/注销)数据散在库、对象存储、备份、日志、缓存五处建"租户数据地图",删除清单化并可验证;备份走"到期自然过期 + 记录留存期"
审计举证需证明"技术上做不到跨租户访问"保留 RLS 策略、NetworkPolicy、IAM 策略的版本化证据 + 渗透测试报告
数据驻留不同租户要求不同地域租户目录里记录 home_region,写入路径按它路由(接 10-01 数据主权篇)

最容易被忽略的是备份:删了主库的记录,备份里还在。合规上通常可接受"备份在留存期内不可检索、到期自动销毁",但必须在隐私政策里写明,否则就是"承诺删除但没删干净"。


十四、90 天落地路线

阶段时间交付物验收标准
一、定边界第 1~2 周租户分档表 + 四层隔离基线文档能回答"某客户属于哪档、每层用什么机制"
二、堵致命口子第 3~5 周JWT tenant claim + RLS(含 FORCE)+ 默认拒绝网络策略三条数据层验收标准全过;无 where 的 SQL 返回 0 行
三、建自助能力第 6~9 周配额模板 + Capsule/vCluster + 租户开通 PR 流水线新租户开通 ≤ 30 分钟,全流程可审计
四、可观测与分账第 10~12 周租户维度四指标 + 成本按租户聚合任一告警能定位到具体租户;账单能出到租户级

15 分钟验收四问(拿这四个问题去问团队,答不上来就还没做完):

  1. 用一个没有 tenant_id 条件的查询,能拿到别人的数据吗?
  2. 客户端伪造 X-Tenant-Id 会发生什么?
  3. 某个租户把 CPU 打满,其他租户的 p99 会掉多少?有数据吗?
  4. 客户要求"删掉我们全部数据",你要动几个系统、多久能给出证明?

总结

多租户架构的难点从来不是"怎么加 tenant_id",而是四条边界要同时成立、且隔离等级要和客单价匹配:

  • 计算:Quota 管总量、LimitRange 管单体,vCluster 解决"租户要自己的 Kubernetes",但别把它当内核隔离。
  • 网络:先默认拒绝,再显式放行——记得放行 DNS,这一条能省掉一半的线上排障。
  • 数据:RLS 是兜底不是替代,FORCE 与 SET LOCAL 是两个决定成败的开关。
  • 身份:只信已验签的 claim,永远不信客户端传来的租户标识。

最后一句:隔离做得好不好,不看架构图画得多漂亮,看"把 where 条件删掉"和"伪造一个租户 ID"这两件事发生时,系统会不会兜住你。

🚀 企业出海需要云架构咨询?通过 7.chengzicloud.cloud 联系我们,获取专属方案。我们提供 SaaS 多租户隔离架构设计、vCluster 与配额体系落地、行级安全与跨租户越权测试、租户级成本分账与 90 天实施路线的一站式咨询与实施服务。

相关阅读

本文由 7.chengzicloud.cloud 提供,点击访问首页了解更多

常见问题(FAQ)

Q1:共享表 + tenantid 到底安不安全?

在正确配置 RLS 且应用账号非 owner/非超级用户的前提下,它是安全的,也是十万级租户唯一经济可行的方案。不安全的是"仅靠应用层写 where"——那种方案在生产上必然出一次事故,只是时间问题。判断标准很简单:把 where 删掉,看数据库会不会兜住你。

Q2:vCluster 和独立集群,隔离强度差在哪?

差在内核与节点。vCluster 隔离的是 API Server、CRD、RBAC 与对象视图,Pod 仍共享宿主节点内核与网络数据面。所以它能解决"租户要装自己的 CRD""租户要 cluster-admin"这类需求,但解决不了"客户要求内核级隔离"或"客户要求专属节点"。后者只有独立集群(或沙箱运行时 + 专属节点池)能做到。

Q3:schema-per-tenant 什么时候值得用?

三个条件同时成立时值得:租户数在数十到数百之间、单租户数据量差异大(大租户单独索引有实际收益)、且你能接受 DDL 扇出。如果租户数会到几千,schema 数量会让 pg_dump、连接数、catalog 查询都变慢,那时候共享表 + RLS 更合适。

Q4:用了 RLS 还需要在代码里写 where 吗?

要写,但目的变了:RLS 是兜底,代码里的 where 是性能。没有 where 时 RLS 会先用 tenant_id 过滤,但查询计划可能退化成全表扫再过滤,索引用不上。正确写法是两者都有,并在 CI 里加一条检查:高风险表的查询必须带租户条件。

Q5:小团队只有几十个客户,要不要一上来就做四层隔离?

不必全做,但两件事必须第一天就有:一是从入口注入的 tenant_id(且写进所有表),二是每张业务表上的 tenant_id 索引。这两件事后补的成本极高(要改所有查询和所有历史数据),其余三层都可以随客户增长逐步加。判断顺序:先堵越权(身份 + 数据),再做噪声邻居(配额),最后才是虚拟集群这类体验型隔离。

Q6:租户级限流放在哪一层做?

放在网关/入口层,不要放在业务代码里。原因:业务代码里的限流在实例扩容后失效(每实例各限一份),而网关是流量的唯一入口。实现上用"租户维度 + 接口维度"两级令牌桶,租户维度给总量,接口维度给热点保护。已有的 API 网关(10-05 篇)就带这个能力,直接复用比自研划算。

Q7:多云部署时,租户的"家"放在哪?

在租户目录里显式记录 home_region 与 home_cloud,并让它成为写入路径的唯一依据。跨云读可以就近,跨云写不行——跨云写会带来一致性、延迟与跨境流量费三重问题(10-09 缓存篇已量化过跨境流量费)。租户迁移是"数据搬迁 + 目录改指向 + DNS 切换"三件事,别指望只改一个配置项。

Q8:怎么向客户证明"我们真的隔离了"?

三样东西:一是技术证据(RLS 策略、NetworkPolicy、IAM 策略的版本化 diff),二是测试证据(每次发布跑的跨租户越权测试用例,含"用 A 的 token 访问 B 的资源应返回 404 而非 403"),三是第三方证据(年度渗透测试报告)。注意越权应返回 404 而不是 403——403 会泄露"这个资源存在",这本身也是一条信息泄露。