ARTICLE DETAIL

资讯详情

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

对象存储多租户隔离:从共享前缀到独立Bucket的架构演进

对象存储多租户隔离:从共享前缀到独立Bucket的架构演进 1. 从 scoop 的一次 main bucket not found 说起Bucket 的边界价值在哪前几天在一台新机器上折腾开发环境顺手装了 Scoop跑了一会儿命令突然弹出一个报错scoop error main bucket not found。我第一反应是网络问题折腾半天才发现是本地仓库引用出了问题最后用scoop bucket add main把官方源重新加回来才恢复正常。这个报错本身不值一提但让我停下来想了很久。Scoop 里的 bucket 是软件源仓库对象存储里的 bucket 是顶层容器两个领域的语义完全不同但它们传递的是同一个概念bucket 是一个有明确归属、有独立边界、可被整体引用的资源容器。Scoop 找不到 main bucket所有依赖它的软件安装全部瘫痪对象存储里如果租户边界画得不清晰服务不会立刻报错但数据泄露、误删、权限混乱这些问题会在某个深夜集中爆发。多租户存储架构的本质问题从来不是怎么把数据存下来而是怎么把不同租户的数据安全、高效、可运维地隔开。对象存储给我们的隔离手段从粗到细大致有三层Bucket 级、Prefix 级、Object 级。绝大多数团队起步时都选了 Prefix因为改动最小一个桶管全部代码里拼一下路径就完事。这个方案能用但撑多久取决于你的租户规模、数据量、权限复杂度、合规要求。我在自己的项目里把「单 Bucket Prefix」整体迁移到了「Agent Bucket 空间隔离」即每个 Agent 租户独立一个 Bucket这篇文章就围绕这次迁移把设计思想、实现细节、踩坑记录和实测数据完整写出来给正在做同样选型的团队一个参考。先说清楚适用人群如果你的项目正在做 SaaS、私有化平台、或者任何一个需要面对多个客户/多个项目组/多个业务方共享一套存储的系统这篇文章值得读完。如果你只有一两个租户数据量也就几万个对象那看完第二节的隐患分析再决定也不迟不一定非要迁移。2. 单 Bucket Prefix 为什么能跑又为什么让人越来越难受2.1 先还原 Prefix 方案的快乐起步期单 Bucket Prefix 的做法字面上非常简单在对象存储里创建一个公用桶比如app-data所有租户的数据都往里放通过路径前缀区分归属——app-data/agent_1001/avatar/xxx.jpg app-data/agent_1001/report/2025/01/xxx.pdf app-data/agent_1001/logs/2025/01/01.log app-data/agent_1002/avatar/yyy.jpg业务代码里只需要在写对象时拼一个agent_id前缀读对象时带上同样的前缀就能做到逻辑上的租户隔离。起步阶段这个方案体验确实不错一套存储配置全公司共用API 只有一个桶不需要写任何桶级策略上传下载的 SDK 代码几乎不用封装直接拿社区例子改改就能用。我当时选择这个方案还有一个现实原因第一版系统要做的事情太多存储只是其中一块团队不想在桶的管理上浪费精力。Prefix 方案让存储的接入成本几乎为零这种选择本身没有问题问题出在先这样以后再说这句话上——很多架构债就是这么欠下的后面连本带利一起还。2.2 隐患一List 操作没有真正的租户边界全靠业务代码自觉用 Prefix 做隔离第一个让我不舒服的地方是 List 操作。对象存储的 ListObjects 接口虽然支持prefix参数来过滤对象但它本质上是在整个桶范围内做遍历。当桶里的对象数量从几十万涨到几千万甚至上亿时即使带了前缀每次 List 也需要扫描大量元数据响应延迟会明显上升。更麻烦的是分页S3 协议默认每次最多返回 1000 条十万个对象翻一百页对服务端和客户端都是负担。代码层面还有一个更隐蔽的风险如果你在某个接口里忘记了传 prefix或者从前端传入的 agent_id 没有做校验直接拼进 prefix那一次 List 就会把全部租户的对象路径暴露出去。这不是危言耸听我在排查别人项目的过程中见过不止一次这种问题。用 Prefix 做隔离租户边界是软边界它依赖每一处写代码的人都能正确拼接、过滤、校验参数。人是会犯错的而存储层的数据泄露是最难被及时发现的那类问题。2.3 隐患二权限模型在 Prefix 上越绑越乱如果只有业务代码层面的隔离那权限管理迟早会找上门来。很多共享桶的方案初期根本没有细粒度权限控制所有租户共用一个 AccessKey读写全部放开。后来审计和合规要求上来了才开始往桶策略里加条件。S3 风格的策略可以这样限制某个 IAM 用户在某个前缀下的访问权限{ Effect: Allow, Action: [s3:GetObject, s3:PutObject], Resource: arn:aws:s3:::app-data/agent_1001/* }单看一条策略没什么但租户从 10 个涨到 100 个时这份策略文件里会有几百条 Resource 规则每上线一个新租户都要手动改策略、走发布流程、验证放行是否正确。一旦某个旧租户的数据路径被误删策略校验不通过线上故障直接出现。我在实践中体会最深的一点是当权限规则的维护频率高于业务功能的迭代频率时说明你把权限粒度选错了。Prefix 方案的权限规则粒度太细细到每个租户的每个前缀都要单独管理这本质上是让开发者去做存储平台应该做的事。2.4 隐患三生命周期、版本控制、加密策略全部一桶同仁共享桶模式下桶级配置是对整个桶内所有租户生效的这就产生了一个很尴尬的局面版本控制开关是全桶级别的。你想给某个重点租户开版本控制防误删其他租户的数据也会跟着被多存一份历史版本存储成本直线上升。生命周期规则虽然可以按前缀配置但大多数对象存储平台对一条桶策略里的规则数量有限制。几十个租户还够用几百个租户时前缀规则根本写不下。服务端加密、CORS 跨域配置、访问日志记录这些全都是桶级属性。想让租户 A 开启特殊加密租户 B 保持默认共享桶模式下做不了。这些配置需求往往不是一开始就出现的而是业务发展到某个阶段客户提出了定制化要求你才发现一个桶管所有的模型根本没有办法支持差异化配置。改也不是不改也不是最后只能新增一个独立的桶给特殊租户结果架构又变成了一种奇怪的混合形态。2.5 隐患四配额、计量、误删恢复都没有物理边界Prefix 模式下的租户数据量统计实现方式通常是按前缀调用 List 数对象或者用存储平台提供的前缀级用量接口。数据量小的时候没问题数据量大了以后每次统计都是一次全桶扫描报表延迟以小时计。误删和恢复是更头疼的问题。删除一个前缀下的对象和删除整个桶在共享桶模型下风险等级完全不同你只是想清理租户 A 的临时文件一旦脚本里的参数写错删到了别人的前缀而桶版本控制又没开因为开了一整个桶都要承担成本那数据就是永久丢失。反过来说如果想把某个租户的数据整体备份或者迁移到另一个存储区域只能遍历这个前缀下所有对象逐个拷贝没有任何原子化的操作可以一次性完成。这类问题不会在系统上线第一天暴露但会在你快要忘记它的时候给你上一堂代价昂贵的课。3. Agent Bucket 空间隔离的架构设计为什么说别自己造轮子3.1 核心思路一个 Agent 一个桶把隔离边界画到最底层既然 Prefix 的问题出在隔离层太薄那自然的解法就是把隔离边界下沉到 Bucket 层。我采用的模式是系统里每个独立的租户实体我这里统一叫 Agent可以是一个企业客户、一个部门、一个接入方在对象存储中拥有自己独立的 Bucket所有数据操作都在自己的桶内闭环跨租户访问在存储层就被天然拒绝。业务层完全不需要再维护一套前缀校验逻辑来防止数据串号。这个设计其实不新鲜各大云厂商的多租户实践文档里都能看到类似的建议。但很多团队仍然选择自己折腾 Prefix 方案原因是觉得每个租户一个桶会有数量限制、管理困难、迁移成本高。这些顾虑有些是真实的有些是信息不对称造成的下面逐个拆解。3.2 Bucket 数量上限没有想象中可怕首先要破除一个最常见的误区对象存储服务对单账号下的 Bucket 数量限制远比很多人以为的宽松。以几个常用平台为例平台单账号 Bucket 数量限制说明AWS S3默认 100可申请提升通过配额申请可以调到 1000MinIO无硬性限制自建场景桶数量只受集群规模影响阿里云 OSS默认 200可通过工单申请提升腾讯云 COS默认 200可通过工单申请提升也就是说即使是默认配额也够支撑上百个租户。如果你的业务确实有上万个租户需要每个独立一个桶那更合理的做法不是回到 Prefix而是做账号/区域拆分比如按业务线分配多个云账号每个账号里再建桶。对象存储的桶只是一个逻辑容器本身不产生费用真正产生费用的是存储量和请求量所以桶多并不会直接导致成本上涨。这里补充一个我在实践中验证过的认知MinIO 自建场景下我在一个集群上创建过上千个 Bucket只要命名规范合理、客户端连接池配置正确日常读写没有明显性能劣化。真正需要关注的是 Bucket 名称的全局唯一性和命名可读性而不是数量本身。3.3 命名规范与元数据映射是命根子Agent Bucket 模式能不能落地一半取决于命名规范设计得够不够好。我的建议是 Bucket 命名遵循一套显式规则例如{产品代号}-{环境}-{agent编号}比如report-prod-ag-1001其中产品代号避免多业务线共用一套存储时出现语义混乱环境prod/staging/dev保证测试数据和生产数据物理隔离Agent 编号直接对应业务侧租户 ID避免通过额外查询才能定位桶。这套命名规则看起来简单却解决了三个实际问题一是排障时看到桶名就能立刻判断属于哪个环境和租户二是避免了在代码里通过数据库反查租户 A 的桶叫什么带来的延迟和失败率三是存储平台控制台的审计页面可以按桶名直接筛选操作记录和费用账单天然按租户分好了。同时业务侧需要一张元数据映射表存 Agent 与 Bucket 的对应关系。这张表不需要很复杂核心字段就是这几个字段说明agent_id业务租户 IDbucket_name对应 Bucket 名称regionBucket 所在区域status启用/迁移中/已停用created_at创建时间有这张表在新租户开通时只需要在表里插入一条记录同时调用一次存储 SDK 创建桶整个初始化流程就算完成了。3.4 权限模型从一条条拼条件变成一个桶一张策略共享桶模式下的权限困境在 Agent Bucket 模式下几乎消失。每个桶独立配置权限策略租户 A 的策略只出现在租户 A 自己的桶上不会和其他租户耦合。具体落地时我推荐用模板策略 STS 临时凭证的组合方式创建一个统一的 IAM 角色或 RAM 角色角色权限被限制在某一个桶下租户侧不需要保存长期 AccessKey而是通过 STS 接口换取临时凭证临时凭证的 Policy 参数里动态指定Resource为当前 Agent 对应的桶资源。以 S3 兼容 API 为例客户端拿到的临时凭证策略大致长这样{ Version: 2012-10-17, Statement: [ { Effect: Allow, Action: [ s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket ], Resource: [ arn:aws:s3:::report-prod-ag-1001, arn:aws:s3:::report-prod-ag-1001/* ] } ] }这样每次生成临时凭证时都只针对当前租户的桶下发权限。租户侧即使拿到了凭证也无法访问其他 Agent 的 Bucket。权限验证发生在存储平台这一层而不是业务代码里安全强度高了一个量级。3.5 桶级配置独立化带来的运维红利Bucket 级隔离还有一个容易被低估的价值所有存储平台的配置能力都从全桶共享变成了按租户独立。版本控制可以针对高价值租户单独开启不用为所有人承担双份存储成本生命周期规则可以按租户业务特点分别设置比如某些租户日志只保留 30 天另一些保留 180 天服务端加密、CORS、回源规则、自定义域名全部可以按需配置监控告警可以按桶维度配置某个租户的存储异常会在自己的桶指标里暴露不至于影响全局判断费用账单可以按桶维度拆分做资源成本分摊时直接拉账单即可。共享桶模式下需要为所有租户设计同一套规则的妥协在这里变成了每个租户按自己的规则运行架构上的自由度完全不同。4. 迁移实战从共享桶到 Agent Bucket 的完整替换路径4.1 第一步先抽象存储访问层把桶在哪从业务代码里抠出来老系统的问题往往是业务代码里到处散落着拼接桶名和前缀的逻辑迁移之前如果不做抽象改造成本会失控。我做的第一件事不是建新桶而是在业务代码和存储 SDK 之间加了一层存储路由Storage Router。这层路由的核心逻辑很简单接收Agent ID 业务路径作为参数在内部查映射表得到目标 Bucket 名称再拼出真正要操作的对象 Key最后调用存储 SDK。对外暴露的接口保持和原来一致这样上层业务代码不需要感知底层存储结构的变化。这一步是整个迁移的枢纽。如果业务代码里还在用bucket agent_ agent_id / path这种硬编码方式迁移就会变成一次伤筋动骨的大改而有了路由层后续的迁移动作都收敛在很小的范围内。路由层的实现需要注意缓存。每次请求都查数据库拿映射关系会在高并发下成为瓶颈需要在内存里维护一份Agent ID - Bucket Name的映射缓存用定时任务或消息事件刷新。Bucket 名一旦创建基本不变缓存刷新频率不需要很高本地缓存五分钟过期完全够用。4.2 第二步存量数据迁移用先拷贝、再校验、后切换三步走存量数据迁移是整件事里风险最高的环节我的策略是三步走先拷贝再校验后切换。先拷贝使用存储平台自带的数据复制工具或者云厂商提供的批量迁移能力把老桶中每个租户前缀下的对象复制到对应新 Bucket 中。目标路径可以保持相对路径不变比如老桶里的对象 Key 是agent_1001/avatar/x.jpg新桶里的 Key 就是avatar/x.jpg去掉前缀这一层。批量拷贝的粒度建议按租户逐个执行而不是写一个脚本全局跑。原因很简单对象存储平台对并发拷贝请求有速率限制全量并发容易触发限流更重要的是按租户分批执行每一批做完都可以立即校验出问题影响范围可控。再校验拷贝完成后对每个租户比较源和目标的对象数量、总大小、文件 ETag 抽样。光比数量不够ETag 校验才能确认内容没有损坏。我当时的校验脚本逻辑大致是按前缀 List 老桶得到全部源对象清单按对象 Key 逐个 HEAD 新桶中的对应对象比对 ETag、Content-Length、Content-Type输出差异清单差异对象重新拷贝。这一步看起来繁琐但非常值得。实际迁移中我发现有个别对象在拷贝过程中 Content-Type 变成了默认的application/octet-stream如果不对 Content-Type 做校验线上用户下载文件时就会全部变成未知文件类型是很隐蔽的线上事故。4.3 第三步灰度切换与回滚设计不能一拍脑袋全局切迁移到新桶后还要解决怎么把线上读写从老位置切到新位置的问题。我强烈建议不要做全局一次性切换而是按租户维度灰度推进。具体的做法是在存储路由层加一个开关配置支持按 Agent ID 白名单决定当前请求走老桶还是新桶。切换某个租户前先把该租户在路由层标记为已切换然后观察日志、错误率、存储访问延迟确认稳定后再切下一批。整个灰度周期不需要太长我个人经验是每批观察 30 分钟到 2 小时如果业务写入量不大一天之内可以完成全部切换。回滚设计同样按租户维度做。一旦发现问题把路由层开关改回去该租户的请求立刻回到老桶路径。这里有一个细节容易踩坑回滚前要确保新桶上的增量数据能同步回老桶或者接受回滚窗口内的数据只存在于新桶的事实。所以切量期间建议开启双写也就是路由层同时向老桶和新桶写入读路径走灰度的桶。双写会增加一倍写入请求但回滚时数据完整性的收益远大于这点开销。我最终采用的完整切换流程是存量拷贝完成并通过校验开启双写观察一段时间确认新桶写入正常按 Agent 白名单灰度切换读路径确认稳定后关闭老桶写路径只保留回调任务继续落数据同步清理老桶中的存量数据。4.4 迁移过程中踩过的三个坑整个迁移过程里有几个问题让我印象特别深刻写下来给后来者避坑。第一个坑是对象元数据丢失。批量拷贝工具默认可能不会保留完整的元数据尤其是Content-Type、Content-Disposition、自定义 Headers。拷贝完成后如果直接对外服务会出现文件下载格式错乱、预览失效等问题。排查这个问题花了我不少时间最后发现是最初的拷贝脚本没有显式指定元数据复制策略。在 S3 兼容 API 里CopyObject 操作默认 Copy 时元数据跟随但某些批量工具或跨云复制场景会重置元数据必须在配置里明确选择REPLACE并逐项填入原始元数据或者使用保留了元数据的同步工具。第二个坑是命名不规范导致映射错位。老系统里的前缀有的是agent_1001有的是agent_1001_还有历史原因导致的agent_1001_copy这种脏数据。我写迁移脚本时直接用字符串解析前缀结果一部分对象被划到了错误的租户映射列表里校验阶段才发现数量对不上。后来干脆改成迁移前先从数据库导出每个租户的真实 Agent ID 清单用清单去匹配前缀而不是用前缀反向推 Agent ID。第三个坑是删除顺序。我最初的计划是切量完成后立刻删除老桶里的 Prefix 数据来省存储费幸好被同事拦住提醒了一句双写和回滚的窗口还没完全关闭老桶数据一旦删了回滚就变成不可能。正确的做法是保留老桶数据至少一个完整的计费周期确认线上稳定后再通过生命周期规则设置延迟删除而不是手动立即清除。5. 上线半年后的实测对比性能、成本与运维变化5.1 性能对比List 操作和并发读写实测迁移完成并稳定运行半年后我做了一次数据对比。在同样的数据规模下单租户约 500 万对象共享桶 Prefix 方案和 Agent Bucket 方案的表现差异很明显List 性能共享桶模式下按前缀 List 一个租户的 500 万对象大概需要 30 秒以上的时间分页压力大Agent Bucket 模式下同样的租户数据在自己的桶里 List因为桶内对象总数变小响应时间下降到秒级以内。单租户故障隔离共享桶模式下某一个租户的高频 List 请求可能把存储服务的配额、连接池占满其他租户被一起拖慢。独立桶模式下即使某个租户产生了异常流量也只影响它自己的桶其他租户的请求延迟没有明显波动。并发写两种模式在小对象并发写上的差异不大因为存储底层都是同一套集群。但如果某个租户的 key 前缀出现极端热点独立桶可以把热点分散到不同桶的元数据分区上高并发场景下的峰值吞吐更稳定。这些数据没有包含复杂调参但也足够说明问题Bucket 级隔离在性能上至少不会比 Prefix 差在故障隔离上则完全不是一个量级。5.2 成本对比桶本身免费省下的是看不见的钱直接说结论Bucket 数量增加不会产生额外费用存储成本、流量费用、请求费用都取决于实际使用量和桶的数量没有线性关系。真正的成本差异体现在两个地方一是版本控制策略的差异化配置。共享桶模式下你几乎不敢对整个桶开启版本控制因为所有租户的历史版本都要计费独立桶模式下可以只对合同要求高、数据价值高的租户开启版本控制其余租户保持默认关闭。仅这一项我这边每月就能省下大约 15%-20% 的存储增量成本。二是运维人力的隐性成本。共享桶模式下新租户开通、权限变更、数据统计都需要开发人员手动处理每次操作都要走流程、写脚本、做校验独立桶模式下租户开通变成了一个创建桶 绑定策略模板 插入映射表的自动化流程重复性工作几乎归零。人天的节省比存储费用本身更值钱。5.3 运维方式的变化从救火变成按桶管理接手过共享桶系统的运维人员应该都有类似体验排障时先问这个报错是哪个租户的然后把租户 ID 换算成前缀再到庞大的桶里去筛日志、筛请求、筛费用。这套流程在 Bucket 级隔离后变得非常直接告警按桶配置用量告警、请求错误率告警某个租户的异常第一时间出现在对应桶的监控面板上费用核算成本账单按桶拆分财务和业务部门对账时直接拉每个桶的消费明细不用再通过前缀做二次统计数据恢复某个租户需要回滚到某天的数据只需针对它的桶做按时间点恢复其他租户完全不受影响合规定位某个对象的泄露调查可以聚焦到具体桶的访问日志审计路径短、定位快。运维人员不需要再在心里维护一张前缀到租户的映射表系统的可运维性提升是这次迁移中我最满意的收获之一。6. 别急着动手什么情况下该继续用 Prefix什么情况下才值得迁移讲完了 Agent Bucket 的优势和迁移经验最后必须给一句泼冷水的话不是所有系统都适合立刻迁移。架构选型不是选最好的而是选当前约束下最合适的。如果你的系统还处于以下状态继续用单 Bucket Prefix 完全合理租户数量在 20 个以内短期也没有大幅增长的计划单租户数据量不大全桶对象总量在百万级别以内所有租户共享同一套访问控制策略不需要差异化配置团队规模和排期不允许这个阶段投入架构改造。反过来出现以下任何一个信号就是该认真考虑迁移的时候了租户数量超过 50桶策略或前缀规则开始频繁变更存储对象总量过千万List 操作出现明显延迟有客户提出了独立域名、独立加密、独立生命周期等差异化需求合规审计要求对租户数据做更严格的隔离和访问控制团队开始花大量时间在处理租户 A 访问了租户 B 数据这类问题上。迁移的窗口期选择也很重要。尽量选在业务迭代的空窗期开始不要在大促、新版本发布、客户集中上线的时间点动存储层。整个迁移过程如果按我上面说的抽象路由层→存量拷贝→双写灰→切流→延迟清理来推进一个中等规模系统从开始改造到全部切完两周左右是比较现实的预期如果团队人手紧张放宽到一个月也正常。最后再分享一个小技巧迁移完成后把新租户开通手册彻底重写一遍。旧版手册里写的都是在共享桶里创建前缀、加策略规则新版手册应该变成调用系统接口创建 Bucket、绑定策略模板、映射表自动入库。手册的厚度和复杂度下降往往意味着架构的健康度在上升。如果你发现团队要花大量精力去维护存储隔离逻辑那大概率不是人的问题而是隔离粒度选错了。
返回列表