砍掉MinIO、Milvus和Vault:自托管Agent平台从11容器精简到5的实践
发布时间:2026/10/8 20:34:07 锦皓数字建站

上个月我把自托管 Agent 平台做了一次“大手术”docker-compose 里的容器从 11 个砍到了 5 个。动刀最狠的是三个“大块头”——MinIO、Milvus、Vault 全被换掉了。很多朋友第一反应是“这几个不是标配吗换掉能行”说实话在我这台单机部署、日均请求量有限的 Agent 平台上这三个组件其实是整个系统里性价比最低的部分——它们真正提供的核心能力我用到的不足 20%但内存、磁盘、运维精力却一直在为剩下 80% 的能力买单。这篇文章把我的替换方案、实操步骤、踩过的坑和真实的代价都摊开讲给各位自托管 Agent 平台的同行一个参考。1. 先交代背景那 11 个容器是怎么堆出来的1.1 一套 Agent 平台的“标配”容器长什么样我之前这套平台是典型的自建 Agent 服务前端 Web 页面 Agent API 后端 Celery worker 异步任务 PostgreSQL 存业务数据 Redis 做缓存和消息队列。这套骨架本身只有 5 个容器按说很精简。但为什么最后跑起来是 11 个因为我在“存储”这个维度上基本是被带节奏了。最初的想法是“既然是正经平台附件存储要 S3 兼容吧”于是加了 MinIO顺手加了 Nginx 做反向代理。接着是“Agent 记忆和知识库检索要用向量数据库”于是加了 Milvus standalone但 Milvus 天生依赖 etcd一加就是两个容器。后来想“密钥不能明文放在 compose 里”又加了 Vault。再算上 embedding 服务前前后后就凑到了 11 个。这 11 个容器看起来各司其职、架构“正规”但实际上里面相当一部分是在为“我可能以后会用到”的能力付账单而不是为“我现在真实需要”的能力付账单。1.2 内存和运维账真正吃掉资源的是谁把 11 个容器按内存占用排个序你会发现一个很扎心的规律业务容器都很瘦基础设施容器才是胖子。容器角色常驻内存实测PostgreSQL业务数据库350 MBRedis缓存/队列80 MBAgent API后端服务180 MBCelery worker异步任务150 MB前端 Web静态资源 SSR90 MBNginx网关15 MBMinIO对象存储260 MBMilvus standalone向量数据库1.2 GBetcdMilvus 依赖的协调服务380 MBVault密钥管理220 MBEmbedding 服务向量化80 MB三个想砍的组件加起来吃了 2 GB 内存占了整个平台内存账单的近四成。而它们带来的运维成本更麻烦Milvus 升级要看 etcd 版本的兼容矩阵MinIO 要定期处理存储桶的权限策略Vault 的 unseal 流程稍微改个配置就得重新摸一遍。也就是说我为了不到 20% 的需求场景背了 100% 的运维复杂度。1.3 触发手术的直接原因真正让我下定决心的是一次 OOM。某个周末知识库批量导入Celery worker 和 Embedding 服务把内存吃满了然后系统直接杀了 Milvus。当时我意识到一个问题Agent 平台的核心价值在于 Agent 的推理链路、工具调用、任务编排而不是把它变成一个“全家桶”基础设施展示平台。如果组件的复杂度已经超过它带来的价值那该动刀时就动刀。2. MinIO 换成本地存储对象存储其实在替我们负重前行2.1 为什么单机场景用 MinIO 是“杀鸡用牛刀”先理清 MinIO 在这套平台里的真实职责存上传的文档、Agent 生成的答案附件、知识库导入的原始文件。本质上就是“给应用提供一个读写文件的地方”。MinIO 作为一个 S3 兼容对象存储确实在很多场景下是标准答案——多节点部署、海量文件、CDN 分发、生命周期管理。但单机自托管场景下这些能力里真正被用到的有几个我反推了一下自己的实际使用路径平台只有一台服务器所有容器共享同一块磁盘文件量级在几十 GB 级别没有多区域复制需求不需要 S3 的版本控制来防止误删应用层自己做了回收机制。这种情况下MinIO 提供的托管 API、桶策略、访问密钥体系反而成了系统里的“第三层皮”——应用要存储文件通过 S3 SDK 访问 MinIOMinIO 再写到本地磁盘。中间多了一个网络请求、一层权限校验、一层配置管理全是开销。2.2 替换实操静态文件服务加应用层附件管理我的替换方案很朴素把 MinIO 干掉直接用 docker volume 挂载一个持久化目录给后端服务文件系统就是存储本身。具体改动分三步走。第一步在 docker-compose 里加一个命名卷挂载到 Agent API 容器的/data/files目录。第二步修改后端存储驱动代码把原来调用 S3 SDK 上传文件的地方改成直接写本地路径。第三步在 Nginx 里加一条/files/的静态路由指向同一个宿主目录这样前端下载附件就走 Nginx 直出不经过后端应用性能和 MinIO 时代没差别。如果你用的 Agent 平台本身是 Dify 或者 Flowise 这类有存储后端选项的开源项目操作更简单——它们的内置存储模式原生支持本地文件系统直接在环境变量里把存储驱动从s3改成local就行连代码都不用动。我自己是自建的 FastAPI 后端所以改的是业务代码里的一个storage模块总共大概改了几十行。这里有一条实操经验迁移时别只复制文件要保持目录结构。MinIO 里通常有bucket这一层路径但切换成本地存储后这层路径往往会被去掉。如果代码里存的是相对路径后端会自动拼上前缀问题不大但如果前端曾经硬编码过 MinIO 的访问 URL那迁移后这些链接都会失效需要顺手做一个数据库里的链接批量替换。2.3 换掉 MinIO 后真正付出去的代价讲完了收益必须说代价。最直接的是彻底告别了 S3 生态的通用性。如果以后平台要迁移到云服务器、要用 AWS S3 或 Cloudflare R2 做异地备份、要接 CDN 做附件分发代码得改回 S3 SDK 的方式目录结构也需要重新适配。另外 MinIO 自带一个简单的 Web 管理界面打开就能看桶里有什么、哪个文件占用空间大换成本地存储后这类“可视化”能力也没了想看磁盘文件只能du -sh加ls回归原始。还有一个很多人会忽略的细节MinIO 是独立进程它和业务容器之间有天然的资源隔离。文件上传导致磁盘 IO 飙高不会直接拖垮业务进程。而本地存储模式下如果恰好有大文件上传磁盘写 IO 会直接影响同机跑着的数据库查询和 Agent 推理响应。所以如果你打算照做建议在 Nginx 层给/files/加个client_max_body_size限制再给后端上传接口做一层文件大小校验避免一个大文件拖全部下水。说实话换掉 MinIO 之后Vault 其实也该换掉但是当时我不太放心想着先跑一跑再说。3. Milvus 换成 Chroma 和 pgvector向量检索没那么高大上3.1 Milvus 不只是“一个大容器”它是“两个容器加一套协调器”如果要给这次砍容器行动评一个“性价比之王”那一定是对 Milvus 下手。很多人对 Milvus 的体量没概念以为它和 PostgreSQL 一样是一个容器的事。实际上 Milvus standalone 模式跑起来后系统里至少要有两个进程Milvus 本体和它依赖的 etcd。这么说吧,你往 docker-compose 里加的其实不是一套向量数据库,而是一个金陵小乐团,光协调员就配了一个。Milvus 的架构是面向分布式设计的proxy、rootcoord、datacoord、indexnode、querynode每个角色都有自己的一套逻辑。standalone 模式虽然把它们塞进了同一个进程但资源占用并不会因为这个就变得轻量。我实测下来空载状态下 Milvus 就吃掉 1.2 GB 内存随着数据量上来还会继续涨。而真实场景里我的平台知识库有几万条文档切片对话记忆几百条embedding 维度是 1536。这个量级对 Milvus 来说,就像用火箭运快递——能送,但根本不经济。3.2 我最终换成了什么Chroma 加 pgvector 的组合最终方案是根据数据的两个不同用途分开处理的。Agent 的记忆片段量级小、写入频繁、读取时只按最近时间排序不需要复杂过滤。这部分我换成了 Chroma。Chroma 的部署形态是嵌入式不需要单独跑一个服务直接作为 Python 库跑在 Agent 后端进程里数据持久化到一个本地目录。相当于把“向量数据库”变成一个“向量文件夹”少一个容器少一堆网络调用。实测写入几百条记忆加上查询检索延迟在毫秒级体感上和 Milvus 没有任何区别。知识库的文档切片量级相对大一些、需要和业务数据做关联过滤比如按用户 ID 过滤可见范围这部分我直接用 pgvector 扩展跑在已有的 PostgreSQL 里。pgvector 提供的是vector数据类型和ivfflat/hnsw索引SQL 语法是标准 PostgreSQL可以直接把向量检索写进原来的 SQL 查询里。这样一来连新的数据库都不用启动直接在现有容器上加一个扩展就行。3.3 迁移代码的实操细节和数据迁移坑代码层改动分两块。第一块是 Chroma 的接入特别简单chromadb.Client()创建客户端collection.add写入collection.query检索完事。第二块是 pgvector 的接入需要写两行 DDL——CREATE EXTENSION vector和带vector列的表结构。与原来的 Milvus client 相比这两套方案的共同特点是配置项大幅减少不用再关心 collection 的 shard 数、副本数、索引类型这些概念。数据迁移我自己写了一个脚本从 Milvus 把向量和 payload 全部 dump 出来然后分别写入 Chroma 和 PostgreSQL。这里有一个需要特别注意的坑——向量维度必须一致。如果你之前用的是 OpenAI 的text-embedding-ada-002维度是 1536那 pgvector 的字段必须建为vector(1536)如果维度对不上索引建不出来查询直接报错。另外 Milvus 的 payload 里存的是key-value结构迁移到 pgvector 时要平铺成表的列写脚本的时候别偷懒,该有的字段全建上。3.4 代价边界什么场景下 Milvus 的地盘我碰不了把话说明白向量检索这种能力数据量和查询复杂度上来之后轻量方案是顶不住的。第一个天花板是数据量。Chroma 单机模式在几十万条向量、几百维的场景下问题不大一旦到几百万条向量且并发查询上来没有独立的索引服务、没有缓存层响应时间会明显劣化。pgvector 在千万级以内的数据量表现都还行但超过这个量级索引构建和查询的耗时会越来越难看。第二个天花板是复杂过滤。Milvus 支持标量字段与向量字段的混合过滤比如“在最近 7 天内、属于用户 A 的、类别为 B 的文档中做相似度检索”。pgvector 也能做这个但代价是 SQL 写起来复杂索引失效风险高。如果你未来的 Agent 平台要做多租户隔离、复杂权限过滤、大规模的元数据条件查询那还是直接上 Milvus 更省心。第三个天花板是召回效果。我在切换之后做了评测同一组问题在差不多的数据量下Milvus 和 pgvector 的召回质量几乎没有差别——因为向量检索的质量主要取决于 embedding 模型和索引的召回参数而不是数据库本身。但 Milvus 对批量导入、增量索引、动态分区的控制更细这些在高频写入场景下差距会体现出来。4. Vault 换成 .env 加 SOPS密钥管理被过度设计了4.1 当初为什么上 Vault后来为什么觉得没必要当初上 Vault 的理由听起来很站得住脚“平台的密钥不能明文放在 docker-compose 里要用专业的密钥管理工具”。Vault 也确实能提供密钥集中存储、访问控制、动态密钥签发、审计日志这些能力。但我在实际用了两三个月之后发现自己只用了它 5% 的功能把几个 API Key 写进 KV 存储里启动时让应用通过 Vault API 读出来。也就是说这套平台里 Vault 的日常角色就是一个“加密版的 config 文件”。但为了这个“config 文件”我要维护一个独立的服务进程、一套 Unseal 流程、一套 ACL 策略。单用户自托管场景下Vault 的“多角色密钥隔离”“动态密钥轮换”“细粒度审计”这些真正值钱的能力我一个都没用上。这就像为了锁一个抽屉给整栋楼装了一扇银行金库门。4.2 替换方案SOPS 加密文件启动时解密注入环境变量我的替换方案是“环境变量加 SOPS 加密”。具体做法如下。把所有的密钥集中写进一个secrets.yaml包括 API Key、数据库密码、Redis 密码、花第一批密钥。然后用 Mozilla SOPS 对这个文件做加密加密的密钥用age生成根密钥保存在宿主机的一个只有 root 可读的目录里。# 生成 age 密钥对 age-keygen -o keys.txt # 用 age 公钥加密 secrets.yaml sops --age age1xxxxxxxx... -e -i secrets.yamldocker-compose 启动时用一个 entrypoint 脚本先解密再注入环境变量sops --decrypt secrets.yaml | yq -e .api_key | export API_KEY$(cat)这样做之后docker-compose 文件里不再出现任何明文密钥secrets.yaml可以安全地提交到 Git 仓库前提是你的能力团队能保管好 age 的私钥本地环境只要把私钥放到固定位置就能一键解密。4.3 朴素方案补齐动态密钥和审计日志换掉 Vault 后很多场景需要自己做兜底。最典型的是密钥泄露后的快速轮换。Vault 可以一条命令吊销密钥并要求应用自动重新读取换成 .env 方案后轮换流程变成更新secrets.yaml里的密钥、重新加密、重启容器。好在自托管单机场景重启代价极低整个流程两分钟就能完成比 Vault 的动态密钥机制在响应时间上差不了多少。审计日志这块我承认回不来了。Vault 能记录谁在什么时间读取了哪个密钥我没有找到完美的开源替代品。实操上的折中方案是在应用日志里加一层密钥访问日志把“何时、何进程、读取了哪个环境变量”记下来。虽然粒度比 Vault 粗很多但对于单机自托管应付排查足够了。这里要说一个我踩过的坑把密钥从 Vault 改成环境变量之后首先确认应用是否在启动时一次性读取还是运行时每次都要读。如果应用像某些 Agent 框架那样在每次工具调用时都动态读取环境变量那环境变量的更新不需要重启进程就能生效如果应用只在启动时读一次那么轮换完密钥后必须重启容器否则旧密钥还会留在内存里直到进程退出。验证方法很简单轮换后看应用日志找到真正的 API 调用成功返回的记录别只看进程起来了就以为完事了。5. 砍完之后5 个容器的现状和真实代价清单5.1 现在长什么样5 个容器各司其职砍完之后整套平台剩下 5 个容器容器扮演角色PostgreSQL业务数据 pgvector 向量检索Redis缓存 Celery brokerAgent API后端逻辑 Chroma 嵌入式向量库Celery worker异步任务文档导入、Agent 执行Nginx网关 静态附件路由前端静态资源其实没单独跑服务直接交给 Nginx 托管了所以严格说是一个“容器加一个反向代理”。Milvus 和 etcd 一起消失MinIO 消失Vault 消失Embedding 服务并进 Celery worker 进程。整个平台从“基础设施全家桶”变成了一套真正围绕 Agent 业务运转的精简系统。5.2 资源消耗前后对比为了给各位一个直观的感受我把手术前后的资源数据放一起对比。指标手术前手术后变化容器数115减少 55%常驻内存约 2.9 GB约 850 MB减少 70%docker-compose 行数400 行不到约 150 行少了一大半冷启动时间约 3 分钟约 40 秒快了 4~5 倍升级迭代需动容器4~5 个2~3 个操作明显减少内存是最直观的收益。砍掉 MinIO、Milvus、etcd、Vault 这两个半胖子之后整机内存占用降了差不多 2 GB意味着同一台 8 GB 的服务器上可以多跑好几个其他服务或者把 Agent 的模型推理并发数调大一档。冷启动时间也非常体感以前重启一次平台等 Milvus 加载数据、etcd 重新选举、Vault 解封三分钟起步现在 PostgreSQL 和 Redis 是恢复最快的组件秒级起来API 服务编译好之后也是秒级整个过程在 40 秒上下。5.3 一份不想让你踩的坑健康检查也要一起改这是我做替换时没有提前预料到的一个坑单独拎出来说。很多 Agent 平台的后端代码里对 Object Storage 和向量数据库是有健康检查的。比如 Dify 有一个专门的“存储状态检查”系统启动时或者每次调用前置时会去 Ping 一下 MinIO 的/minio/health/live端点。我把 MinIO 去掉之后第一次启动系统直接报警“Stop 服务不可用”我当时还愣了一下因为我还没反应过来 MinIO 已经没了。解决方式很简单把健康检查逻辑改成检查存储目录的可写性。Python 里就是判断/data/files是否可读可写、磁盘空间是否足够。向量数据库的健康检查换成对 Chroma 或 pgvector 的一次轻量查询。但提醒各位这类检查一定要改在明面上别只写在文档里因为如果你用的是现成的开源平台代码里可能好几个位置都嵌入了存储健康检查不逐个排查干净换完组件后系统日志里能刷出几百条报错。5.4 代价清单哪些能力是真的“回不来了”最后把“代价”这件事说透。换掉这三个组件不是没有代价的我的判断标准是这些代价是“我以后可能遇到的问题”而不是“我现在已经出现的问题”。收益是立刻兑现的代价是对未来的透支——每个人都需要做一次这样的权衡。真实回不来的能力有几项S3 生态的通用性。附件存储如果以后要迁到云端、要接 CDN、要做多地域部署代码得改。但好消息是目前我的平台跑在一台服务器上这个需求大概率不会出现。向量检索的上限。千万级向量和复杂标量过滤的场景会被 pgvector 卡住到时候还得把 Milvus 请回来。但平台目前的增长速率大概再跑两年也到不了这个量级。密钥的审计能力。Vault 的审计日志确实是硬能力我用“应用层日志 手动轮换”做了补偿。对于合规要求严格的团队不建议这么做。独立的资源隔离。Milvus 和 MinIO 作为独立进程内存和 IO 是独立分配的。现在向量计算和文件读写都在业务进程里一个重型查询可能影响同进程的其他请求。这也是我把 Embedding 服务合并进 Celery worker 的原因——隔离在进程里做而不是靠容器。6. 什么情况下你别学我这么砍6.1 从数据量和团队规模判断该不该砍写这篇文章不是鼓励所有人都照着我砍。根据自己情况判断我总结了三条线第一条线如果你的向量数据量目标在百万级以上或者你要在向量检索里做非常复杂的标量过滤和多租户隔离Milvus 留着。这不是怀疑 pgvector 的能力上限而是 Milvus 的运维复杂度在高压力场景下是值得的。第二条线如果你的平台要对外开放、多人协作、有很多外部集成和 API Key 要管理Vault 的价值就会出现——审计日志和动态密钥能在事故发生后帮你快速定位问题。第三条线如果你的附件要分发到公网、要对接云存储、要做定期归档那 MinIO 别急着换。反过来如果你和我一样是单机自托管、一个人或者一个小组用、数据量在几十万条、没有严格的审计合规要求那这套精简方案大概率适合你。容器数量本身不是问题问题是你在为一个很少触及的能力上限持续付资源账单。6.2 如果真的决定砍按什么顺序砍最稳我的建议是分两步走不要一次性全删。第一步先砍 Vault。理由很简单它的替换风险最小。把密钥移到 SOPS 加密文件后应用层几乎不用改代码环境变量注入方式和原来读取配置的模式差距最小。只要验证启动正常、密钥能正常注入这个替换就成功了。Vault 砍掉后系统内存降低约 220 MB体感最先释放。第二步同时处理 MinIO 和 Milvus。因为这两者的代码改动都涉及存储层同时改可以避免“两套存储代码并存”的过渡期。先把存储层的抽象接口理清楚再分别切换实现——MinIO 切本地磁盘Milvus 切 Chroma 和 pgvector。每一步都做一轮完整的功能回归上传附件、查看附件、创建知识库、导入文档、Agent 对话查记忆、知识库检索回复。每项功能都在新旧两个存储实现上各跑一遍确认结果一致再往下一步。这个验证流程虽然琐碎但能省掉未来几天排查问题的时间。另外提醒一句动刀之前先把数据全量备份。我当时把 MinIO 的数据和 Milvus 的向量都导出了一份放到了外部磁盘。事实证明这个动作太重要了因为我在第一次迁移 pgvector 时维度建错了导致索引全部失效如果没有备份数据那一批知识库就得重新 embedding代价很大。踩过这一轮坑之后我个人的体会是自托管平台的选择逻辑一直不应该是“这个功能可能以后会用到所以现在就要装”而应该反过来——“我现在就是要解决这个问题什么方案能最简单地解决就先上什么方案”。等真的遇到扩展瓶颈时再针对瓶颈做专项替换。容器从 11 砍到 5表面上砍的是组件本质上砍的是“为未来过度准备”的冗余。现在这套平台跑起来轻快很多我也有更多精力去调 Agent 本身的编排逻辑而不是整天伺候那些“大块头”基础设施。如果你也在跑自托管的 Agent 平台不妨重新审视一下 docker-compose 里的每一个容器问问自己它现在到底在为我做什么答案如果含糊那它可能也该被换掉了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。