2、租户模型设计:租户隔离策略、共享与独享模式、租户元数据管理

租户模型,说白了就是回答一个问题:不同客户的数据,到底该怎么放?

我见过不少团队,一开始图省事,把所有客户的特征数据全塞进一个大池子里。结果呢?客户 A 要删数据,你不敢删,因为怕影响客户 B 的模型训练。客户 C 要加个自定义字段,你得改全局 schema,所有客户都得跟着升级。这种「一个池子养所有鱼」的做法,迟早会出问题。

这一节,我们就来聊聊租户隔离的几种策略,以及它们各自的适用场景。

2.1 三种隔离策略:从「共享」到「独享」的光谱

租户隔离不是非黑即白的。它更像一个光谱,一端是完全共享,另一端是完全独享。我习惯把它分成三个档次:

隔离策略 核心思路 典型场景 我踩过的坑
Schema 级隔离 同一张表,通过 tenant_id 字段区分 SaaS 初创期、POC 验证 误删数据时,恢复成本极高
Database 级隔离 每个租户独享一个数据库实例或 schema 中型客户、合规要求严格 跨租户查询变得非常麻烦
集群级隔离 每个租户拥有独立的计算/存储集群 超大规模客户、金融级客户 运维成本直线上升

我的建议:不要一开始就追求「完美隔离」。先用 Schema 级隔离跑起来,等客户体量上来后,再按需升级。我见过最惨的案例,是某公司花三个月搭建了集群级隔离,结果第一个客户只有 100 万条特征数据,纯属浪费。

2.2 共享模式 vs 独享模式:成本与灵活性的博弈

共享模式和独享模式,其实对应的是「成本优先」和「体验优先」两种思路。

2.2.1 共享模式(Multi-Tenant Shared)

所有租户共用一套基础设施。特征数据通过 tenant_id 做逻辑隔离。优点是资源利用率高,运维简单。缺点是「吵闹邻居」问题——某个租户的突发查询,可能会拖慢所有人的特征读取速度。

我曾经遇到过一个场景:某大促期间,一个电商租户疯狂写入特征数据,导致同一集群下另一个金融租户的实时特征查询延迟从 5ms 飙升到了 200ms。金融客户直接投诉到 CEO 那里。后来我们不得不紧急给这个金融租户做了资源限流和优先级队列。

避坑指南:共享模式下,一定要做 资源配额(Quota) 和 优先级调度。别等到出事了再补,那时候客户已经跑了。

2.2.2 独享模式(Single-Tenant Dedicated)

每个租户拥有独立的资源池。性能隔离最好,但成本也最高。适合对 SLA 要求极高、或者有数据合规需求的客户。

独享模式有个隐藏成本:运维复杂度。每个租户的集群都要单独升级、打补丁、监控告警。我记得有一次,我们同时维护了 30 多个独享集群,光是版本兼容性问题就让人头大。后来我们引入了自动化运维平台,才勉强撑住。

2.3 租户元数据管理:特征平台的「户口本」

租户元数据,就是记录「哪个租户用了哪些特征、特征怎么定义的、数据存在哪里」的信息。没有好的元数据管理,特征平台就是一盘散沙。

我习惯把租户元数据分成三层:

  1. 租户基本信息:租户 ID、名称、套餐等级、创建时间、状态(激活/冻结/删除)。
  2. 租户资源配置:存储配额、QPS 上限、特征数量上限、关联的集群/数据库信息。
  3. 租户特征目录:该租户下所有特征的元数据,包括特征名、类型、来源、更新频率、血缘关系。

这里有个容易忽略的点:租户套餐等级。不同等级的租户,能使用的特征数量、存储空间、查询性能都不一样。元数据里必须记录这些限制,并在特征写入和查询时做校验。

注意:租户元数据本身也需要高可用。我见过有人把租户元数据放在单机 MySQL 里,结果 MySQL 挂了,整个特征平台都无法判断请求来自哪个租户,所有服务全部瘫痪。租户元数据至少要做主从复制,最好用分布式存储。

2.4 一张图看懂租户模型设计

下面这张图,概括了租户模型设计的核心逻辑:从租户接入,到隔离策略选择,再到元数据管理,最后落地到特征存储和查询。

租户模型设计核心流程 租户接入 隔离策略选择 Schema 级隔离 Database 级隔离 集群级隔离 租户元数据管理 特征存储 & 查询服务 提示:隔离策略可动态调整,建议从 Schema 级起步,随客户规模增长逐步升级

2.5 实战:租户元数据表设计

最后,给出一份我常用的租户元数据表结构。你可以直接拿来用,或者根据业务调整。

-- 租户基本信息表
CREATE TABLE tenant_info (
    tenant_id       VARCHAR(64) PRIMARY KEY,
    tenant_name     VARCHAR(256) NOT NULL,
    tier            ENUM('free', 'pro', 'enterprise') NOT NULL,
    status          ENUM('active', 'frozen', 'deleted') DEFAULT 'active',
    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 租户资源配置表
CREATE TABLE tenant_resource (
    tenant_id       VARCHAR(64) PRIMARY KEY,
    storage_quota   BIGINT NOT NULL COMMENT '单位:字节',
    qps_limit       INT NOT NULL COMMENT '每秒查询次数上限',
    feature_limit   INT NOT NULL COMMENT '最大特征数量',
    cluster_id      VARCHAR(128) COMMENT '独享集群 ID,为空表示共享集群',
    FOREIGN KEY (tenant_id) REFERENCES tenant_info(tenant_id)
);

-- 租户特征目录表
CREATE TABLE tenant_feature_catalog (
    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
    tenant_id       VARCHAR(64) NOT NULL,
    feature_name    VARCHAR(256) NOT NULL,
    feature_type    VARCHAR(64) NOT NULL COMMENT '如 int, float, string, vector',
    source          VARCHAR(512) COMMENT '特征来源,如 kafka topic, hive 表',
    update_freq     VARCHAR(32) COMMENT '如 realtime, hourly, daily',
    created_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_tenant_feature (tenant_id, feature_name),
    FOREIGN KEY (tenant_id) REFERENCES tenant_info(tenant_id)
);

一点小经验:租户特征目录表一定要加 UNIQUE KEY,防止同一个租户下出现同名特征。我见过有人忘了加,结果两个团队同时创建了同名特征,导致线上模型读取到错误的数据,排查了整整两天。