多租户AI平台隔离设计:权限、数据、资源、计费四层落地与踩坑
这段时间一直在打磨一个多租户AI平台越做到后面越发觉真正折磨人的不是模型效果调参而是权限、数据、计费这三条隔离线怎么在工程上同时扎稳。表面上看多租户平台和普通SaaS没什么区别无非是登录、鉴权、访问控制那一套。但AI平台一旦牵扯到模型推理、向量检索、token计量、算力配额这些环节隔离的粒度、链路和复杂度就完全不一样了。今天把整个设计思路和踩坑记录整理出来不画大而空的架构图只讲工程落地时会遇到的真实问题以及我这边经过实测觉得靠谱的解决方案。无论你是在自研AI应用网关还是把大模型能力包装成API对外售卖这篇文章里的隔离策略都能直接用来做一次查漏补缺。1. 多租户AI平台的整体设计与核心权衡1.1 租户模型选型独立实例、独立Schema、共享Schema怎么选先说最基础的选择题租户之间到底隔离到什么程度。常见的做法大致有三类行业里通常叫隔离模型具体差别在于数据物理存放的位置和资源分配的颗粒度。第一类是独立实例模式每个租户拥有一套完整的应用服务、数据库甚至独立的模型推理资源。这种模式隔离性最强数据安全事故的概率最低但也最烧钱。AI平台和大模型推理的账单大头是GPU和显存占用如果给每个租户都拉一个推理实例一百个租户就要一百张卡这个成本绝大多数团队扛不住。除非平台定位是高端私有化交付否则独立实例模式在规模化阶段基本走不通。第二类是独立Schema模式所有租户共享同一个数据库实例但每个租户一套表结构。优点是数据隔离比较清晰导出某个租户的数据时直接把对应schema打包带走就行。缺点是连接数膨胀很快数据库实例的整体连接上限可能成为瓶颈而且后续要做跨租户的报表统计会比较麻烦得写跨schema查询或者额外汇总。第三类是共享Schema模式所有租户的表都在同一个库里每行数据带一个租户ID。这是目前大部分SaaS平台和AI中台最常用的方案。原因很直接资源利用率最高数据库连接数可控应用代码里用一条租户ID过滤就能完成绝大部分隔离需求。但它的风险也最隐蔽只要有一条查询漏加了租户条件就可能发生跨租户数据泄漏。从实际经验看共享Schema模式对工程纪律的考验最大但对团队预算和技术能力是相对平衡的选择。另外还有一个容易忽略的点AI平台的模型文件不一定适合放进业务数据库模型权重、知识库文档、向量索引通常放在对象存储或专门的向量数据库里。所以租户模型选型不仅要考虑业务数据表还要考虑模型资产和检索资产怎么跟着租户走。这一点在后续数据隔离章节里会专门展开。1.2 AI平台多出来的四层隔离需求一般业务系统谈多租户重点在“数据”和“权限”。但AI平台还有一个特殊性模型本身是会被调用的算力是会被消耗的而且调用模型是要花钱的。这就让隔离需求从两层扩展成了四层。第一层是权限隔离回答“这个租户里的用户能看什么、能点什么、能调哪个模型”。第二层是数据隔离回答“这个租户的私有知识库、对话记录、训练数据集如何避免被其他租户访问”。第三层是资源隔离回答“不同租户的推理请求如何排队、如何限流、如何防止一个租户把GPU资源打满影响其他人”。第四层是计费隔离回答“每次API调用消耗了多少token、多少算力、计在哪个租户头上余额扣减后账单怎么算”。我自己在实际设计时养成了一个习惯把每一层单独画一条链路而不是混在一起。权限链路管身份和授权数据链路管存储和查询资源链路管并发和配额计费链路管计量和账单。四条链路最终汇聚到租户ID这个主键上。只要租户ID在每一条链路的入口都被正确解析、校验、透传隔离的骨架就立住了。很多线上事故之所以发生本质上是某一层链路里丢了租户上下文结果后面所有操作都失去了过滤依据。2. 权限隔离工程租户边界从握手开始2.1 租户上下文的建立与传递权限隔离的第一步不是写鉴权中间件而是先明确“当前请求到底属于哪个租户”。这个问题处理不好后面所有隔离策略都是空中楼阁。业内现在比较标准的做法是用户在登录时完成身份认证认证服务在签发的令牌里写入租户ID。比如使用JWT那么payload里除了常规的user_id、role之外还必须包含tenant_id。后续每一次API请求网关层解令牌把租户ID放在请求头中向后透传比如用X-Tenant-ID这样的自定义头传递。这里有一个我从实际操作中总结的教训内部服务之间转发的时候绝不能直接信任客户端传上来的X-Tenant-ID。正确的做法是统一由网关或认证层从令牌中解析租户ID后再向下传内部服务只认网关传下来的这个值如果发现和令牌里的不一致直接拒绝。曾经遇到过一种事故场景客户端手动改写了租户请求头结果上游服务没有校验拿着伪造的租户ID去查询数据库数据自然就串了。所以我会在网关层做一道校验请求头里的租户ID必须和令牌里的租户ID完全一致否则返回403。这是最重要的一道防线务必在线上环境开启。进入业务服务之后租户上下文要能贯穿整个调用链。在Java体系里可以用ThreadLocal或者请求上下文对象保存当前租户在Python体系里通常放在请求作用域的ContextVar里。关键是所有数据访问层、缓存访问、模型调用SDK都要从这个上下文里取租户ID而不是从参数里现拿。这样写的好处是后续新增查询时开发人员不容易漏掉租户ID。# 伪代码示意请求上下文携带租户ID class TenantContext: def __init__(self, tenant_id: str, user_id: str, role: str): self.tenant_id tenant_id self.user_id user_id self.role role # 数据访问时强制要求上下文存在 def get_repository(tenant_ctx: TenantContext): if not tenant_ctx or not tenant_ctx.tenant_id: raise PermissionError(tenant context missing) return build_filtered_repository(tenant_ctx.tenant_id)2.2 RBAC与ABAC的组合模型级授权不能全靠角色权限模型方面通用后台管理大多用RBAC就够了也就是给用户分配角色角色绑定权限集合。但AI平台里有一个特殊对象是为模型本身租户可以调用哪些模型、不能调用哪些模型这不能简单用一个“普通用户”角色框定。比如平台方拥有基础模型A和某个行业定制模型B租户甲买了A的调用权但没买B租户乙正好相反。如果只用RBAC你得给每个租户动态创建角色角色数量一多就变成权限灾难。我目前更倾向的做法是RBAC和ABAC混用。RBAC管基础的操作权限比如“能不能创建知识库”“能不能查看调用日志”ABAC管细粒度属性匹配比如“当前请求的模型ID是否在该租户的可用模型白名单里”“当前请求的数据集是否属于该租户”。ABAC的策略可以做得非常灵活策略文件里写清楚条件即可。策略示意仅允许租户调用已授权模型 subject: { tenant_id: tenant_123 } action: invoke_model resource: { model_id: model_travel_v1 } condition: resource.model_id in subject.allowed_models effect: allow权限隔离落地时还有几个容易遗漏的点。一个是API Key很多AI平台允许租户自行创建API Key来调用模型那么这个API Key本身也要绑租户ID和模型范围不能生成之后就变成万能钥匙。另一个是管理端和运行端的权限分离租户管理员能管理自己团队的用户但不能查看其他租户的任何配置。这两个点在评审时是必查项。3. 数据隔离工程从数据库到缓存到向量的完整闭环3.1 数据库层的数据隔离设计数据库层是数据隔离最核心的阵地也是出错概率最高的地方。如果走共享Schema路线设计上有一条铁律所有涉及租户业务的表都必须带tenant_id字段并且索引里必须包含tenant_id。以某个业务表为例CREATE TABLE conversation_record ( id BIGSERIAL PRIMARY KEY, tenant_id VARCHAR(64) NOT NULL, user_id VARCHAR(64) NOT NULL, session_id VARCHAR(64) NOT NULL, model_id VARCHAR(64) NOT NULL, prompt_tokens INT NOT NULL, completion_tokens INT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW() ); CREATE INDEX idx_conv_tenant_time ON conversation_record (tenant_id, created_at DESC);这里加复合索引非常关键因为平台端给租户提供“按时间倒序查看调用记录”的功能时如果没有tenant_id领头的索引就会发生全表扫描数据库压力直线上升。很多生产事故从表面看是数据库慢查询本质上就是索引设计没覆盖租户维度。应用层写数据访问时我倾向于把租户过滤逻辑封装在数据仓库层而不是散落在业务代码里。所有查询都从上下文拿租户ID拼进WHERE条件。业务代码层不允许出现裸SQL查询。这样可以减少人为因素导致的泄漏风险因为研发在写新功能时只能通过仓库方法访问数据仓库方法强制接收租户上下文。独立Schema方案也有一些团队会选。特别是当每个租户的数据结构差异很大时用共享表模型会逼着你设计一张超级宽的“字段扩展表”查询越来越别扭。独立Schema的好处是每个租户的表可以有自己的演进节奏但代价是数据库连接数、迁移脚本数量、运维复杂度都会涨。AI平台里知识库文档这类数据结构比较统一一般不需要走到独立Schema这步所以共享Schema加tenant_id过滤对多数场景是性价比最高的选择。3.2 缓存、对象存储和向量检索的租户隔离技巧业务数据放在数据库里只是数据隔离的第一步一旦引入缓存、对象存储、向量检索隔离问题就会从单点扩散到多个存储系统。缓存层最大的风险是键设计没有租户维度。Redis里如果某个键只用了功能名加ID比如conversation:7890那么两个租户刚好都有ID为7890的会话时就会互相覆盖或者互相读到。正确的设计是把租户ID放进键名的前缀里ai:{tenant_id}:token:quota ai:{tenant_id}:conversation:{session_id} ai:{tenant_id}:model:calling:{model_id}键名带上租户前缀之后隔离是有了但也要注意不要让缓存键无限膨胀。租户数量多的时候可以考虑给键名加一层固定namespace分隔这样后续批量清理某个租户的数据也比较方便直接用ai:tenant_123:*模式扫描删除。对象存储方面最常见的做法是让每个租户拥有独立的路径前缀比如bucket/tenant_123/knowledge/docs/。这里有一个坑必须提醒如果对象存储服务支持全局列表操作而应用代码在枚举文件时没有加租户前缀过滤也会看到别人的文件。为了保险起见可以让文件上传接口把租户ID作为目录层级的一部分并且授权策略里限制只有该租户的角色能够读对应前缀。向量检索是AI平台独有的隔离难点。知识库通常会被拆成向量存进向量数据库不同租户的知识库向量如果混在一个集合里检索时就必须把租户过滤条件带上。做法一般有两种一是每个租户独立集合当租户数量大或集合管理成本高时变成负担二是共享集合但每条向量都带tenant_id元数据字段查询时先按租户ID过滤再加相似度检索。后者更常见但要注意向量索引过滤性能。如果租户数据量很大需要在租户ID字段上建立倒排索引或者标签索引否则过滤条件会变成后置过滤检索延迟和准确性都会受影响。我从实操中得到的经验是向量库隔离光靠过滤条件还不够还要在写入阶段就做好校验。比如执行“删除知识库向量”的操作删除条件必须同时包含向量ID和租户ID防止租户甲知道一个向量ID就顺手把别人的知识库删了。这类“按ID直接操作”的接口在AI平台里尤其危险因为很多向量数据库的原生操作日志里并不强制权限必须在应用层补齐。4. 计费隔离工程计量、配额与账单的工程化落地4.1 计量事件链路设计计费隔离的第一步是计费要能清楚地把每一次模型调用的成本归属到具体租户。这里最大的考验不是计算价格而是计量数据能不能准确、不重复、可回溯地进入账单系统。我推荐的计量链路是异步事件驱动。模型推理服务在完成一次请求后不直接写计费库而是先发出一个计量事件到消息队列比如Kafka或者RabbitMQ再由计量服务消费事件做聚合、计价、入账。这样做的原因很简单模型推理的时延非常敏感如果每次请求都要同步调用计费服务更新余额那么计费服务一抖推理接口也会跟着超时。异步化之后推理服务只负责把事件发出去就结束后续计费节奏可以由独立服务自己控制。一条计量事件最少要包含这些字段租户ID、用户ID、API Key标识、模型ID、输入token数、输出token数、开始时间、结束时间、是否命中缓存、是否属于测试流量。有了这些字段计费服务才能区分实际调用和缓存命中也才能做账单明细回溯。计量事件示例JSON格式 { event_id: evt_202409111020_88f3ab1f, tenant_id: tenant_123, user_id: user_008, api_key_id: ak_9271, model_id: model_travel_v1, prompt_tokens: 1580, completion_tokens: 420, started_at: 2024-09-11T10:20:00Z, finished_at: 2024-09-11T10:20:01Z, cache_hit: false, is_test: false }计量事件进队列之后消费端要做两件事一是写入明细存储一般是按时间分表的关系库或者OLAP数据库用于后续查账和报表二是做增量聚合更新当前计费周期内该租户的总用量。聚合结果会给到配额和余额校验服务使用这样可以避免每次校验配额都去扫描全部明细。4.2 配额与余额的双校验机制计费隔离不只是记账它还承担着“让租户花不了超过预期的钱”这个责任。我遇到过不少平台钱倒是记对了但租户能把配额打穿月底出现一大笔坏账。所以配额控制和余额校验必须做在请求路径上。配额限制一般分三层。第一层是全局接口级限制防止整个平台被某个突发流量瞬间打满第二层是租户级配额比如该租户每分钟最多调用多少次、每天最多消耗多少token第三层是用户或API Key级限制防止租户内部某个子账号异常调用拖垮整个租户。三层限制可以在网关层或者推理接入层统一实现。Redis是实现这种限流比较顺手的选择。固定窗口限流可以用INCR加EXPIRE滑动窗口可以用ZSet按时间戳记录请求。下面用固定窗口限流示例展示设计思路-- 每分钟最多120次调用按租户维度 local key KEYS[1] -- ai:{tenant_id}:rate:model:{model_id}:m1 local limit tonumber(ARGV[1]) -- 120 local current redis.call(INCR, key) if current 1 then redis.call(EXPIRE, key, 60) end if current limit then return 0 end return 1配额校验通过之后还需要校验余额。余额校验和配额校验有一点重要区别配额是“一段时间内的总量”余额是“当前账户还能花多少钱”。余额校验适合在计量事件异步处理时同时完成但实时扣减又容易出事。我实践下来比较稳的方案是请求进来时先同步读取账户剩余余额做一次前置校验如果余额低于阈值就拒绝推理完成后生成计量事件异步扣减余额扣减过程通过消息队列保证顺序。至于余额扣减的原子性则依赖数据库行锁或Redis原子操作确保不会因为并发请求重复扣费。4.3 账单周期聚合与对账账单周期通常按自然月或套餐周期来切分。每个周期开始时计费服务要有能力把上一周期的计量明细聚合成账单。聚合维度一般是租户ID 周期 模型ID计算出每个模型的调用次数和token消耗再按价格表换算金额。账单表和计量明细表一定要分开存。账单表关注聚合结果计量明细表关注原始流水。这两张表分开之后遇到“用户质疑账单金额不对”时能够从账单反查明细再从明细反查原始请求日志完成可追溯的审计链路。我特别建议增加一个每日快照表按租户ID和日期记录当天的用量汇总。这样即使账单周期中途调整了价格也能按日粒度重新折算不至于只能依赖过期账单。对账方面有一个实操细节计费服务从消息队列消费事件时需要做幂等处理。消息系统在异常重启或者网络抖动后可能重复投递同一条事件如果消费端不加幂等就会造成重复计费。我用的办法是给事件表建唯一索引把event_id作为唯一键重复插入直接报错跳过。这样一个简单的约束就能避免那一类最让财务崩溃的“同一笔调用被扣了两次钱”的事故。5. 常见踩坑与线上排查实录5.1 五个高频线上事故与排查思路第一个高频事故是跨租户数据串读。典型表现是租户A的对话记录出现在租户B的列表里。排查时先看SQL日志确认查询条件里有没有带上tenant_id再看代码调用链看是不是有的接口直接使用了对象ID查询而缺少租户上下文。这是共享Schema模式最经典的坑根治方法是把数据访问收口到强制带租户ID的仓库层。第二个是缓存串号。表现为租户A修改了配置租户B那边立刻看到了变化。排查方式是登录Redis查看键名十有八九是键名里缺少租户前缀。这类问题在测试环境很难被发现因为测试时通常只有一个租户生产环境租户一多就暴露了。所以代码评审时我必查所有缓存键定义凡是涉及业务数据的键必须包含租户ID。第三个是配额被“静默打穿”。流量突增时限流逻辑依赖本地内存计数每个服务实例各自计数合起来就超过配额了。解决办法是改成Redis集中式计数或者至少在网关层做集中限流。如果不想让每个请求都走Redis可以接受一定的松弛度比如允许百分之五的超出但必须明确这个偏差在财务上可接受。第四个是计费重复。症状是账单金额和调用记录对不上调用次数看起来正常但余额扣得很快。排查路径先看消息队列有没有重复投递再看消费端有没有做幂等。如果是先消费后记录重启时offset没提交大概率会造成这个结果。第五个是“幽灵租户”的资源配置问题。某个已删除的租户对象存储或者缓存里的数据没有清干净导致存储成本持续上涨偶尔还能在运维后台看到租户ID不存在但仍有请求记录。这个问题的根源在于删除租户时没有走完整的“数据销毁流水线”。我的建议是为租户注销定一条专门的销毁流程先停API Key、再注销身份权限、再清理缓存数据、再清理对象存储前缀、最后删除数据库中的业务表记录每一步都要有任务记录和校验结果。5.2 设计评审时我必查的七个隔离漏洞点这几条是我每次做多租户AI平台设计评审都要过一遍的清单说出来供大家参考。第一租户ID是否出现在所有核心表的索引最左侧。第二所有内部服务间透传租户上下文时是否校验了与令牌的一致性。第三Redis键和对象存储路径是否全局唯一并带租户前缀。第四向量检索接口是否在入库、查询、删除三个操作上全部带租户过滤。第五计量事件字段里是否包含足够信息来区分测试流量与实际流量防止内部联调数据污染租户账单。第六限流和配额校验是否有独立的集中式存储还是只靠应用实例本地内存硬撑。第七租户注销时是否有关联的自动清理流程而不是留下一堆孤儿数据。查完这七点基本上多租户AI平台常见的隔离缺口都能被提前发现。还有一些细节容易被忽视比如日志系统里打印的tenant_id也要脱敏以及内部监控仪表盘展示跨租户聚合数据时是否不小心把单个租户的明细暴露给了非授权运维人员。这些问题虽然不直接影响业务功能但一旦出现审计事故代价远高于开发时的修补成本。最后分享一个小经验多租户AI平台的隔离能力测试不能只看功能测试用例通过多少还要专门做“反向用例”。也就是故意让租户A访问租户B的资源看系统是否拒绝。把这些反向用例固化到自动化回归测试里每次发版之前跑一遍比上线之后到处救火要省心太多。我自己在这类项目上吃过不少亏但把这一套检验机制建立起来之后线上隔离类事故基本绝迹了。