自托管 Agent 平台的容器精简,一直是个让人上头又纠结的话题。前阵子我把自己搭的一套 Agent 平台从 11 个容器砍到了 5 个,当时主要是有两个触发点:一是扩容新机器时发现内存占用实在离谱,16G 的机器光基础设施就吃掉 11G,留给 Agent 推理调度的余量所剩无几;二是拉取 MinIO 和 Milvus 镜像时反复出问题,部分网络的镜像下载体验非常折磨,等镜像的时间比配置服务还长。冷静下来盘点才发现,这 11 个容器里有不少是为了“理论上的扩展性”和“高可用架构”而存在,但真实场景根本用不上那些特性。于是我做了一轮彻底的架构减法:MinIO 换成了本地磁盘直存,Milvus 换成了 PostgreSQL 的 pgvector,Vault 换成了 Docker Secrets 和环境变量注入。文章我会把每个替换方案、背后的选型逻辑,以及实实在在付出的代价全部摆出来,帮后面想动手的兄弟少走点弯路。
1. 先复盘:11 个容器里到底装了些什么
动手砍容器之前,得先搞清楚原来的架构里到底有谁,为什么会有这么多。很多自托管项目扩容到后面,不是业务复杂了,而是依赖盘根错节,每个组件都觉得自己必须有专属服务,叠加在一起就成了容器动物园。
1.1 当时为什么会有 11 个容器
我那套 Agent 平台跑的是多智能体协作场景,有主控 Agent、工具调用 Worker、知识库召回、记忆系统和外部 API 网关。最初设计时参照了企业级平台的部署形态,想着把存储、检索、密钥管理这些都拆成独立服务,于是容器列表长这样:
| 容器名 | 用途 | 是否关键 |
|---|---|---|
| agent-core | Agent 主服务,负责编排和推理 | 核心 |
| agent-worker | 异步工具调用与后台任务执行 | 核心 |
| redis | 任务队列、缓存、会话状态 | 核心 |
| postgres | 业务结构化数据、用户配置 | 核心 |
| minio | 对象存储,保存知识库文档、图片、临时文件 | 中间层 |
| milvus-standalone | 向量数据库,知识库检索的核心 | 中间层 |
| milvus-etcd | Milvus standalone 模式依赖的元数据存储 | Milvus 附属 |
| milvus-minio | Milvus 内置的对象存储,存放向量索引快照 | Milvus 附属 |
| nginx | 反向代理、SSL 终止、静态资源访问 | 边缘 |
| vault | 密钥管理、Token 签发、加密存储 | 辅助 |
| watchtower | 自动检测并拉取镜像更新 | 辅助 |
Milvus standalone 模式是个经典陷阱:它名字里写着“single mode”,实际跑起来默认会拉起来三个组件,milvus、etcd、built-in MinIO 一个不少,这在 Docker Compose 里就占了三个名额。原本以为引入 Milvus 只是多加一个容器,结果它自带一个元数据库和一个存储引擎,直接给架构加了两倍负担。等我意识到这个事实时,11 个容器里已经有一个隐性的“三角组合套餐”了。
1.2 真正承担业务价值的是哪几个
把所有容器按功能和数据链路拆开看,真正承载业务价值的只有四条线:
- 推理与编排线:agent-core + agent-worker,负责把用户请求拆分、调用工具、汇总结果
- 记忆与状态线:redis + postgres,一个管会话缓存和队列,一个管结构化持久化
- 知识冷却线:这套 Agent 的核心是知识库问答,文档要切块、向量化、存进去、按相似度召回,这条链路过去完全依赖 MinIO + Milvus
- 入口线:nginx,负责对外暴露服务和静态资源
Vault、watchtower 在整个链路里其实是“非业务依赖”。Vault 管密钥没问题,但只要没到“多应用共享密钥、需要动态轮换”的规模,vault 大部分能力都在空转。watchtower 是自动化更新容器镜像,也是锦上添花的东西,机器不稳定时自动更新反而容易引入新问题。
关键认知来了:真正不能砍的是 Redis 和 PostgreSQL。原因很朴素,这两者承担了高频读写和强一致性数据存储,想用文件系统或别的组件替代它们,成本远大于收益。而 MinIO、Milvus、Vault 属于“服务化基础设施”,在单机规模下有非常明确的下沉替代方案,这就是这次精简的突破口。
1.3 精简思路:不是砍功能,而是砍组件
很多人做架构裁剪容易走极端,要么一刀切全塞进一个二进制里,要么觉得每个服务都是必不可少。我的思路很明确:每个服务承担的能力可以保留,但承载能力的容器不一定要存在。所谓砍容器,砍的是“独立部署的进程”,而不是把功能从系统里抹掉。
比如对象存储这个能力,小规模场景下完全可以用挂载卷里的普通文件目录替代;向量检索这个能力,PG 装上 pgvector 扩展后照样做 HNSW 近邻搜索;密钥管理这个能力,Docker Secrets 加环境变量注入,配合本地chmod 600权限文件,对于单机部署已经足够。每个组件都提供“同构能力”的轻量替代,再逐步迁移数据,这才是合理路径。
2. 替代方案选型逻辑:不折腾就是最好的方案
决定把 3 个基础组件换掉之后,我给自己定了几个选型原则,避免为了精简而引入一个更复杂的替代品。说白了,这次减容不是为了跑分或者玩酷,纯粹是维护成本高到忍无可忍,所以替代方案的核心诉求只有一个:省心。
2.1 自托管场景的第一性原则
自托管平台(尤其是个人或小团队使用)的第一性原则,是“在保证可用性和数据安全的前提下的最低维护成本”。按这个原则,我之前部署的很多企业级组件都属于严重超配:
- MinIO 可以做多副本、纠删码、版本控制、生命周期管理,但这些在单机场景大部分时间用不上,反而每个版本升级都要关注兼容性
- Milvus 提供了分布式向量检索、多租户、复杂索引类型,但数据量就几十万条向量,用单机 PG 的 HNSW 索引完全够用,而 Milvus 每次容器重启、元数据初始化、索引加载都要等半天
- Vault 提供加密存储、动态密钥、解封流程,但单机只有一两个应用在读密钥,Vault 本身就需要存储后端、解封机制,出问题还得手动操作
先说个结论:在自托管场景里,“不折腾“远比“更强”重要。你部署一个组件时节省的那点配置时间,远不如运行期间它能安静如鸡地活着更值钱。
2.2 替换决策的四个判断标准
我当时对每个候选替代方案做了四个维度的筛选,这也是我建议你换组件前自己问一遍的问题:
- 数据量级是否匹配:向量数据超过千万级,或者文档/对象数量达到百万级,那别折腾了,老老实实用专用组件。但如果只有几万到几十万条向量、几千个文件,PG 和本地磁盘绰绰有余
- 是否已有现成依赖:数据库、缓存这些你是必须要有的,那么在现有组件上扩展能力,一定比引入新组件更划算。我用了 PostgreSQL,加 pgvector 只是一个扩展,不用多部署一个服务
- 运维操作难度:要看平时维护这个组件要做多少事。MinIO 有 Web 界面要监控,Milvus 要管 etcd 快照和索引内存,Vault 要处理解封,这些都不算“零维护”。换成磁盘目录、PG 扩展和 env 文件,维护动作减少百分之八十
- 迁移成本:从现有组件导出数据的难度。MinIO 里文件直接
mc cp或挂载卷复制;Milvus 向量先查出来写文件再加到 PG;Vault 密钥本来就是手动托管,重新放环境变量就行。三者迁移成本都可控
基于这四个判断标准,替代方案很快就收敛了:MinIO 换成本地卷,Milvus 换成 pgvector,Vault 换成 Secrets 和 env 文件。每个方案都是“复用已有能力,而不是新增依赖”。
2.3 为什么依然保留这些容器
我保留了 Redis 和 PostgreSQL,有人可能会问:你既然这么激进地砍组件,为什么不再进一步把 Redis 也省了?这里我要强调一个边界:
Agent 平台和普通 Web 应用不一样。Agent 的编排过程有大量异步任务和中间会话状态,任务队列如果丢了,知识库处理的批次、工具调用的回调、多轮对话上下文都会出问题。Redis 提供的高性能和数据结构(比如列表、哈希、带过期时间的缓存)用文件系统根本替代不了,硬要替代的话代码侵入性极大,等于大改了业务核心。
PostgreSQL 就更不用说,它是整个系统的“真相源”。业务数据、用户配置、知识库切片元数据、向量索引,全部要有事务保障,不能用文件代替数据库。而且 PG 本身的稳定性已经经过十几年验证,多一个扩展并不会显著增加维护负担。
保留这两个核心组件,是为了把改动限制在“基础设施层”而不是“业务代码层”。减少改动面,事故率才会真正下降。
3. MinIO、Milvus、Vault 各换成了什么
这节是全文的核心,我把三个组件的替换方案和每个决策后的具体得失全部摊开。如果你是准备抄作业的,重点看这一节的“代价”部分,别只看成果。
3.1 MinIO 换成本地磁盘直接存取
MinIO 在我架构里承担的职责很典型:知识库文档(PDF、Markdown、TXT)、用户上传的场景图片、Agent 工具调用产生的临时文件。过去接入方式是通过 S3 SDK,后端代码统一用对象存储 API 读写。替换方案很直接:在 Docker 卷里划一个./data/storage目录,把所有文件按业务子目录存放,后端代码理论上需要从 object_store 接口改成文件接口,但现实是,我的代码本来就在部分场景直接走本地路径,所以改动量很小。
做法是这样:
mkdir -p /opt/agent/data/storage/documents mkdir -p /opt/agent/data/storage/images mkdir -p /opt/agent/data/storage/temp chmod 755 -R /opt/agent/data/storage然后在 Docker Compose 里把目录挂载给 agent-core 和 agent-worker:
volumes: - /opt/agent/data/storage:/app/storage访问路径通过 Nginx 暴露成静态文件 URL,实现文件直链下载。后端在存储文件时直接用带时间戳的 UUID 文件名,保留原始扩展名,写入成功后把相对路径存到 PostgreSQL。整个过程非常简单,但也带来了明确的边界:所有文件只保存在本地单机磁盘上,没有多副本和纠删码,磁盘损坏意味着文件永久丢失。所以我每周做一次异地备份,把整个storage目录增量同步到另一台廉价的备份服务器上,相当于用外部 cron 任务补上 MinIO 原本有的“对象版本保留”能力。
代价清单如下:
| 能力维度 | MinIO 版本 | 本地磁盘版本 | 影响级别 |
|---|---|---|---|
| 文件冗余与纠删码 | 支持 EC/N 副本 | 无,单盘单点 | 高,需要外部备份补位 |
| S3 API 兼容 | 完整 S3 接口 | 无,改为文件路径 | 中,代码改动量看设计 |
| 生命周期管理 | 支持过期删除、冷热分层 | 需要 cron 手动清理 | 低,小规模临时文件不多 |
| Web 管理界面 | 自带 | 无,直接用系统文件管理 | 低 |
| 防盗链和签名 URL | 支持预签名 | 无签名机制 | 中,如果要对公网开放需自己加验证 |
如果对核心 Agent 平台做代码改造的时候,还需要注意路径访问安全。本地文件路径很容易出现目录穿越风险,我的解决方式是:所有文件访问通过后端接口校验权限后重定向到 Nginx 的 X-Accel-Redirect,而不是直接暴露静态目录。这部分逻辑代码量不大,但安全性不能再依赖 MinIO 的 bucket 权限了。
3.2 Milvus 换成 PostgreSQL 的 pgvector
Milvus 被替换是我最纠结的一个决定,因为知识库召回是 Agent 平台的灵魂,向量检索稍有闪失,问答质量直接塌方。Milvus 的独立部署带来三个附加负担:
- Milvus standalone 内部依赖 etcd 和内置 MinIO,三个容器联动,启动顺序错了要等很久
- 索引加载非常吃内存,几十万条向量常驻内存,导致整机内存紧张
- 每次备份要同时考虑 Milvus 的元数据、etcd 快照、对象存储中的索引文件,备份链路复杂
pgvector 是 PostgreSQL 的扩展,用 SQL 表存向量,执行近似最近邻搜索。我把原来的collection对应到 PG 的一张表,设计如下:
CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE document_embeddings ( id BIGSERIAL PRIMARY KEY, document_id BIGINT NOT NULL REFERENCES documents(id) ON DELETE CASCADE, chunk_order INTEGER NOT NULL, chunk_text TEXT NOT NULL, embedding VECTOR(1536) NOT NULL ); CREATE INDEX ON document_embeddings USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);调用时直接查询:
SELECT id, chunk_text, 1 - (embedding <=> '[...]') AS cosine_similarity FROM document_embeddings ORDER BY embedding <=> '[...]' LIMIT 10;这里有个关键点:Milvus 默认使用的检索方式是 HNSW,pgvector 在最新的版本中也支持 HNSW 索引,所以从 Milvus 迁移到 pgvector 在索引算法层面是接近的。不过m和ef_construction这两个参数特别考验内存和检索精度的平衡。我试过m=32, ef_construction=128,召回效果确实更好,但构建索引时的内存峰值明显升高,在 8G 内存的机器上出过 OOM,后来退回m=16, ef_construction=64,磁盘和内存压力小了非常多,召回率虽然理论上略降,但配合重排序后实际问答效果差别不大。
代价方面,pgvector 相比 Milvus 的主要损失是:
| 能力维度 | Milvus | pgvector | 影响级别 |
|---|---|---|---|
| 千万级向量性能 | 专属分布式引擎,性能好 | 单机横向受限,百万级是舒适区 | 中,超出范围需重新选型 |
| 动态管理接口 | 提供 collection 管理、分区、索引状态接口 | 只能通过 SQL 管理,部分操作要手动维护 | 低 |
| 标量过滤+向量检索 | 支持复杂的 filter 然后 ANN 检索 | 支持 WHERE 条件后执行 ANN,效果一致 | 低 |
| 索引构建内存控制 | 有专门参数调优 | 参数少,有效但限制也多 | 中 |
| 多租户/权限 | 支持多租户资源隔离 | 依赖 PG 行级权限,实现更繁琐 | 低,个人自托管无所谓 |
还有个隐性好处:向量数据跟业务数据在同一个数据库里,备份变成一次pg_dump加 WAL 归档,不再需要单独对 Milvus 做一套备份方案。这对我来说是最直接的运维收益,风险也比以前更集中、更可控。
3.3 Vault 换成 Docker Secrets 和环境变量
Vault 在我的架构里管的东西并不复杂:数据库密码、Redis 密码、外部模型 API 的 Key、JWT 签名密钥。过去为了“安全”,所有密钥通过 Vault API 动态获取,业务启动时先连接 Vault 解封再拉取密钥。这套机制在真实运行中让我头疼的事不少:Vault 要么服务重启后未解封,要么网络抖动造成密钥获取失败,底层 Agent 服务直接启动不了,排查起来简直要命。
替换方案是什么呢?Docker Secrets 加环境变量:
secrets: db_password: file: ./secrets/db_password.txt redis_password: file: ./secrets/redis_password.txt openai_api_key: file: ./secrets/openai_api_key.txt services: agent-core: environment: DB_PASSWORD_FILE: /run/secrets/db_password REDIS_PASSWORD_FILE: /run/secrets/redis_password OPENAI_API_KEY_FILE: /run/secrets/openai_api_key然后把/run/secrets下的文件读进来,启动时再注入到配置对象中,避免把密钥写死在环境变量里暴露给运维面板。对于单个容器内的应用来说,Docker Secrets 的形态已经足够了。
代价是本质上的“密钥管理能力”是后退的:
| 能力维度 | Vault | Docker Secrets/env | 影响级别 |
|---|---|---|---|
| 密钥存储加密 | 支持 KMS、加密后端 | 依赖文件系统权限加密,无透明加密 | 中 |
| 动态轮换 | 支持动态生成和吊销 | 只能手动修改文件并重启容器 | 中 |
| 审计日志 | 完整操作审计 | 无审计,需要外部日志补 | 中 |
| 解封流程 | 需要手动解封,快速恢复难 | 无需解封,文件常驻 | 低 |
| 多机共享密钥 | 支持 | 不支持,多机需要额外同步 | 中 |
我个人认为,在单机场景下,Vault 换来的是复杂度和故障点,不是安全感。Vault 适合 10 个以上应用共用密钥、密钥经常动态签发、有安全审计强制要求的团队,个人自托管用 Docker Secrets 加chmod 600的文件描述符就足够了。前提是设置好文件权限,别用 644 挂在宿主机上,否则等于公开了。
3.4 代价对比总表
省流总结,待办清单一样的代价已经出来了:
| 被替换组件 | 替代方案 | 不可忽视的代价 | 可接受的原因 |
|---|---|---|---|
| MinIO | 本地磁盘 + Nginx 直链 | 无冗余、无 S3 兼容、无签名鉴权 | 小规模数据量 + 外部定期备份 |
| Milvus | pgvector | 单机性能上限、索引调参维度减少 | 数据量在百万级以内,业务模型完全匹配 |
| Vault | Docker Secrets + env | 密钥轮换手动、无审计、无动态签发 | 单机单应用,安全威胁模型可控 |
| Milvus 附带的 etcd + 内置 MinIO | 直接删除 | 原有分布式元数据能力失去 | 组件本身就是跟随 Milvus 走的,主组件移除后没有存在价值 |
| watchtower | 直接删除 | 失去自动更新 | 自动更新引发的风险大于便利 |
这么看,砍掉 6 个容器不是“我忍痛丢了功能”,而是每个功能都找到了更低成本的落点。真正的代价是三个能力维度后撤,必须靠运维习惯去补位:文件要定期备份、密钥要谨慎管理、向量库单机有上限。
4. 实操过程:从 11 个容器到 5 个容器的完整步骤
光谈概念没用,把切换过程完完整整写一遍才有价值。下面按时间线拆解我实际操作的步骤、动作量和踩坑的地方。
4.1 数据迁移三步曲
切换最怕的是数据丢,我按数据性质分成三条链路挨个迁:
第一步是对象数据迁到本地卷。MinIO 里的文件,无论有多少个 bucket,本质都是磁盘上的对象。我先用 MinIO 客户端(mc)把所有文件同步到宿主机的一个临时目录:
mc alias set oldminio http://localhost:9000 accesskey secretkey mc mirror --overwrite oldminio/agent-bucket /tmp/storage-mirror/同步完核对文件数量,然后把临时目录移动到最终挂载路径,调整所有权:
mv /tmp/storage-mirror /opt/agent/data/storage chown -R 1000:1000 /opt/agent/data/storage第二步是向量数据迁到 pgvector。Milvus 里按 collection 遍历所有向量数据,用 Python 脚本把每条向量的文本、向量数组、文档 ID 读出来,再批量 insert 到 PG 表。这个动作有个坑:向量维度必须严格一致,我原来的向量是 1536 维的 embedding,在写入前最好先用pgvector的vector(1536)类型约束校验一遍,否则批量插入中途报错就只能回滚重来。
第三步是密钥迁移到 Secrets。把 Vault 里存的值逐个导出,写入对应文件:
vault kv get -format=json secret/db | jq -r .data.data.value > ./secrets/db_password.txt chmod 600 ./secrets/db_password.txt为什么强调chmod 600?因为容器内的 secret 文件默认挂在/run/secrets/,权限是 0444,任何容器进程都能读。宿主机上面源文件如果权限太开,等于把密钥明文放门口了,这点必须管住。
迁移期间新旧系统是并行的,我在 Nginx 层面切流量,先切内网测试域名,验证 Agent 的知识库召回、文档上传下载全部 ok,之后再把正式域名切过去。这波操作能避免回滚时数据不一致的混乱。
4.2 最终 Docker Compose 配置参考
这是我最终留下的 5 个容器的精简 Compose 骨架,稳定性跑了一个多月,没出过大问题:
version: "3.8" services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_DB: agent POSTGRES_USER: agent POSTGRES_PASSWORD_FILE: /run/secrets/db_password volumes: - /opt/agent/data/postgres:/var/lib/postgresql/data secrets: - db_password restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U agent"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"] volumes: - /opt/agent/data/redis:/data restart: unless-stopped healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 agent-core: image: my-agent-core:latest depends_on: postgres: condition: service_healthy redis: condition: service_healthy environment: DB_HOST: postgres REDIS_HOST: redis SECRET_FILE_PATH: /run/secrets PYTHONUNBUFFERED: "1" volumes: - /opt/agent/data/storage:/app/storage secrets: - db_password - openai_api_key restart: unless-stopped agent-worker: image: my-agent-worker:latest depends_on: agent-core: condition: service_started environment: DB_HOST: postgres REDIS_HOST: redis SECRET_FILE_PATH: /run/secrets volumes: - /opt/agent/data/storage:/app/storage secrets: - db_password - openai_api_key restart: unless-stopped nginx: image: nginx:1.27-alpine ports: - "80:80" - "443:443" volumes: - /opt/agent/data/storage:/var/www/storage:ro - /opt/agent/nginx/conf.d:/etc/nginx/conf.d:ro - /opt/agent/nginx/ssl:/etc/nginx/ssl:ro depends_on: - agent-core restart: unless-stopped secrets: db_password: file: ./secrets/db_password.txt openai_api_key: file: ./secrets/openai_api_key.txt几个配置细节值得说明:
postgres使用了pgvector/pgvector:pg16镜像,它把扩展打包好了,省去进容器执行CREATE EXTENSION的步骤,但建议第一次启动后还是手动登进去执行一次建表脚本和索引脚本,确保对象都建好再启动业务redis的密码我没有走 secrets 文件,因为 Redis 命令行要求明文传参,走${REDIS_PASSWORD}环境变量反而更简单,代价是密码在 Compose 文件或.env里可见,所以我把.env文件权限也设成了 600agent-core和agent-worker其实可以合并成一个容器,通过启动参数区分进程模式。我保留了拆分配置,原因是工具调用异步任务容易阻塞核心循环,独立 Worker 能保证重型任务失败后不影响主服务稳定性,这个取舍你们按实际并发量来定
4.3 资源参数与启动顺序调整
容器数量少了,资源分配反而要更细心。我按照最终架构整理了内存规划:
- PostgreSQL 分配 2G 内存,兼顾向量索引的构建和查询
- Redis 分配 512M,主要用于队列和缓存
- agent-core 分配 4G,这是推理编排的主战场,预留一些大模型批量请求的缓冲
- agent-worker 分配 2G
- Nginx 分配 256M
- 系统本身缓存占用 1G 左右
这套组合在 16G 机器上运行非常从容,剩余内存还能跑其他小服务。对比之前的部署,Milvus 常驻的索引内存、etcd 的快照缓存、MinIO 的对象缓存全部取消,内存占用直接降了接近一半,这才是 11 砍到 5 最直观的红利。
启动顺序上,PostgreSQL 和 Redis 必须优先健康,之后 agent-core 起来时会自动跑数据库迁移(建表、初始化向量索引),然后 agent-worker 启动并连接队列,最后 Nginx 加载路由并对外提供服务。我没有用复杂的编排脚本,直接靠depends_on的condition: service_healthy就能把依赖关系管理好。
4.4 第一次启动后的验证清单
容器全部切换启动后,不要急着宣布成功,我按这个清单做了一遍功能验证:
- 数据库连通性:
docker compose exec postgres pg_isready确认正常 - 向量检索能力:跑一个真实的知识库查询,观察返回结果相似度排序是否合理
- 文档上传:通过前端上传 PDF,确认文件落到
/opt/agent/data/storage/documents,然后能通过 URL 访问 - Agent 工具调用:触发一个读文件的工具,确认 worker 能拿到文件内容并参与推理
- Redis 队列消费:观察任务从 pending 到 completed 的状态流转,确认 worker 没有被取消后闲置
- 重启恢复:手动
docker compose restart agent-core,确认依赖的服务能自动恢复,不需要人工干预
验证通过以后,我才删掉了所有旧容器和旧镜像。这里提醒一句:旧容器不要立刻删除,保留一两天,等新系统完全稳定后再清理,防止回滚时需要重新拉镜像浪费时间。
5. 踩坑记录与问题排查
每个架构变更后面都埋着不少坑。我把自己真实遇过的几个高频问题列在这里,算是给同路人一份避坑笔记。
5.1 镜像拉取失败的真实场景
触发这次精简的直接原因之一就是镜像拉取失败。MinIO 和 Milvus 的镜像体积都比较大,特别是 Milvus standalone 镜像加上 etcd 镜像,在部分网络环境下拉取经常中断。我遇到过一个现象:Docker pull 每次拉到一半就报错,重试三五次才成功,中间一旦超时,容器服务就处于不可用状态。表面上这是网络问题,深层暴露的是镜像体积和依赖复杂度的问题。如果你也被这个折磨,建议换成体积更小的发行版镜像,或者干脆学我这套方案把相关组件砍掉。
另外,热词里多次出现的“minio拉取失败”在社区里其实很常见。MinIO 官方镜像体积大约 300M 到 400M,网络差的时候确实难拉。如果只是因为镜像拉不下来就要完整替换 MinIO,那太可惜。可以试试配置 Docker 镜像加速源,或者拉取更精简的 community 版本镜像。但如果本身就只在单机用对象存储,本地卷方案更省心。
5.2 pgvector 索引和查询的坑
pgvector 用起来不是零门槛,我踩了三个具体坑:
第一个是 HNSW 索引构建时内存暴涨。第一次建索引使用m=32, ef_construction=128,PG 进程内存直接冲到 5G,然后机器触发 OOM。解决方式是把maintenance_work_mem降到 512M 或 1G,同时降低索引参数。HNSW 参数不是越大越好,它是检索精度和资源的平衡点,按我的数据量,m=16, ef_construction=64已经够用。
第二个是查询没走索引。pgvector 的 HNSW 索引并不保证所有查询都走索引,当查询条件带的过滤器选择性太差,或者表数据量太小时,PG 优化器可能选择顺序扫描。排查方法是用EXPLAIN ANALYZE看执行计划,确保 SQL 里对embedding列的操作符用的是<=>(余弦距离),并且索引类型是vector_cosine_ops,跟查询算子匹配才生效。
第三个是删除和更新性能低。DELETE后 HNSW 索引不会立即紧凑,残留标记会导致查询退化,需要定期VACUUM或重建索引。我的经验是,每天凌晨跑一次轻量维护脚本,对增量变化大的表执行一次REINDEX INDEX CONCURRENTLY,不影响线上查询。
5.3 文件卷权限与备份变化
MinIO 在容器里管理文件时,所有对象都通过 S3 API 写入,宿主机上文件权限由容器统一处理,体验比较顺滑。换成本地卷加挂载后,权限问题立刻浮出水面:容器内进程是以UID=1000运行的,但宿主机目录如果默认属于root,就会报 Permission denied。
解决方式分两层:
chown -R 1000:1000 /opt/agent/data/storage同时把 Compose 文件里的 user 字段显式设为user: "1000:1000",确保进程身份稳定。这个改动虽然小,但没做的话你会看到一堆诡异的读写失败,排查起来很浪费时间。
备份方案也要重新设计。以前 MinIO 的数据可以用mc mirror增量备份,Milvus 的向量库单独导 dump。换成本地方案后,我改成了一条命令搞定全量备份:
tar czf /backup/agent-$(date +%Y%m%d).tar.gz \ /opt/agent/data/storage \ /opt/agent/data/postgres \ /opt/agent/data/redis加一个 cron 每周执行一次,再配合 PG 的连续归档,基本能恢复到任意时间点。代价是备份粒度没那么细,但单机场景这种粗粒度备份完全够用。
5.4 容器资源隔离引发的 OOM 排查
自托管 Agent 平台上,Docker 默认没有给容器设置严格的内存上限。你可以在 Compose 文件用mem_limit或deploy.resources.limits限制内存,但如果不设置,一个容器就能拖垮整台机器。我之前在旧架构里遇到过:Milvus 索引构建期间内存飞速上涨,MinIO 也在缓存文件块,两者叠加把机器内存打满,导致 agent-core 无响应。
换成新架构后,我主动给每个容器设置了明确的mem_limit,例如:
services: postgres: mem_limit: 2g redis: mem_limit: 512m agent-core: mem_limit: 4g agent-worker: mem_limit: 2g这样一来,即使某个服务的负载突然升高,也只是它自己先触发 OOM 重启,不会把整台机器拖垮。容器资源隔离放到实践中必须是主动行为,而不是 Docker 默认帮你做好的事。
另外,agent 平台里最危险的其实是 embedding 批处理任务。大批量文本同时喂进 embedding API,返回的向量列表一次性写入 pgvector,内存里会同时存在原始文本和向量数组。我建议在 Worker 里加一个批量大小限制,比如每次只处理 64 个切片,处理后立即提交并释放内存,否则再大的内存也扛不住长文档。
结尾:这套精简方案真正适合谁
我在这次架构切换后最大的感悟,不是“我删了 6 个容器很爽”,而是重新理解了自托管平台的边界。这套从 11 砍到 5 的架构,非常适合个人开发者、三五人小团队、原型阶段的产品,以及数据量在百万级向量以内、日活在几千以下的 Agent 场景。它能帮你节省服务器开销,降低运维复杂度,换来可预期、可解释的稳定状态。
但如果你要做的是面向企业客户的多租户平台、高并发知识库问答、必须通过安全审计的服务,那就不要盲目照搬我这个方案。MinIO 的多副本、Milvus 的高并发检索、Vault 的审计能力,在那些场景里是硬需求,砍掉就是给自己埋雷。到时候你需要的是租用云厂商的对象存储、托管向量数据库,而不是所有东西都自托管。
说到底,架构没有绝对的好坏,只有是否匹配当下的资源规模和业务阶段。我选择降级基础组件,是因为它们的功能超出需求得不偿失。等到哪一天我的 Agent 平台真的增长到单机撑不住,我的下一步动作不是重新捡起 MinIO 和 Milvus,而是直接把存储和检索迁到云原生的托管服务,本质上是同样的“按规模选型”思路。
最后分享一个小习惯:每次架构调整后,我会把 Compose 文件、数据迁移脚本、验证清单打成一个 repo 存档,并且把“为什么要这么改”写进 README。几个月后再看,你一定会感谢当时记下的那些决策理由。