尧图精选

250个AI智能体压缩进8个Pod:K8s多Agent部署的架构实践

🕒 发布时间:2026/9/28 9:05:37 📁 来源:尧图网络
250个AI智能体塞进8个Pod——这个项目刚开始立项时我们内部开玩笑说这就是个“Agent大通铺”一个Pod里塞进几十个AI智能体资源共享、任务分着干。别人做Agent平台通常是“一号一Pod”或“一号一容器”每个智能体独立部署资源确实充裕但250个Agent就意味着250个Pod、250份冗余进程K8s资源开销直接失控。我们这次偏偏反着来在8个Pod里跑250个Agent还要保证响应速度、稳定性、可观测性都不拉胯。这篇文章不是我写产品PR也不是教程式PPT而是把这套方案从设计、踩坑到压测的完整复盘。适合正在做AI Agent平台、多智能体协作系统或者说服务化Agent部署的工程师参考哪怕你只是刚接触K8s也能从里面get到Pod资源规划、ConfigMap配置管理和Agent进程编排的基本盘。1. 项目背景与整体设计思路1.1 “大通铺”模式把一个Pod当一间共享宿舍传统做法是每个Agent独立占一个容器或一个Pod比如企业客服Agent、文档解析Agent、代码辅助Agent各跑各的。这个思路清晰隔离也好但到250个Agent规模时成本和资源浪费肉眼可见地失控每个容器里都有一份Python解释器、一堆模型依赖库、一个常驻HTTP框架空闲时占着内存不干活高峰期又都挤在一起抢CPU。我们换个思路把Pod当一间共享宿舍。一个Pod就是一个小型计算单元里面塞多个Agent进程共享同一份基础镜像、同一个网络栈、同一个生命周期。这样做的好处非常直接镜像只拉一次、Pod数量从250降到8、内存通过进程级复用大幅压下来。代价是隔离性差一些某个Agent崩了可能波及其他Agent——所以后面必须用进程管理和可观测性来兜底。我见过不少团队盲目模仿“一个Pod多Agent”结果把几十个线程全部塞到一个Python进程里出现一个Agent的全局锁拖垮所有Agent的惨案。所以这个大通铺不是简单堆数量而是“进程级别隔离 线程级别共享”每个Agent是一个独立进程各自内存隔离互相通过消息队列通信宿主的Pod只负责资源配额和生命周期。1.2 250个Agent的职责拆解先分类再谈编排250个Agent不是250个复制品。我们当时把这些Agent按职责分成了几类这也决定了下面Pod的划分逻辑。短平快的工具型Agent查天气、查库存、做翻译、格式化数据这类任务平均不到1秒并发高但每个请求消耗资源小。长文本理解型Agent会议纪要点提取、合同条款分析、文章总结单次调用可能要读几万字上下文内存占用大、推理耗时久。多轮对话型Agent客服、销售助手需要维护会话状态上下文窗口不断叠加容易形成内存增长。编排型Agent负责调度其他Agent像是“老板”自己不直接干活但会派活。如果不分类直接均分很容易出现某个Pod里全是长文本Agent高峰期内存被打爆另一个Pod里全是短任务AgentCPU闲置。经验是按任务特性分Pod而不是按编号均分这是Agent大通铺架构里第一个要做的决策。后续所有资源规划、滚动更新策略、压测瓶颈分析都建立在这个分类基础上。1.3 为什么是8个Pod数量不是拍脑袋定的8这个数字不是玄学。当时我们根据三类因素算出来的单Pod里的Agent密度上限、容错要求、以及运维成本。先看密度上限。单个Pod里进程数量不是无限的每个Agent进程至少有一个HTTP客户端连接池、日志文件句柄、若干线程操作系统默认的文件描述符限制通常1024很容易被吃满。我们测试过通用配置下单个Pod塞30个Agent比较稳超过35个就开始出现连接复用失败、端口耗尽这类问题。如果优化了系统参数和连接池可以到45个左右但没必要为极限值牺牲稳定性。再看容错要求。8个Pod意味着单Pod故障时损失约12.5%的Agent产能这类损失可以通过其他Pod临时接管任务来对冲不需要3个副本的冗余。如果只做4个Pod单点故障影响面太大如果做16个Pod密度太低又退回到“大Pod小用”的老路。8个是我们的平衡点。资源侧的计算放到第3章细说这里先给结论8个Pod250个Agent平均每个Pod约31个Agent分布均匀后CPU和内存都留有20%以上的buffer。2. 核心细节解析与关键技术实现2.1 Agent运行时容器基础镜像与进程管理大通铺的核心是容器内进程编排。我们统一选择了Python 3.11-slim作为基础镜像而不是完整版Anaconda镜像——后者动辄几个GB拉取一次要几分钟启动还要导入一堆用不到的包。镜像能瘦则瘦我们的最终镜像控制在了900MB以内包含了常用的HTTP客户端、向量检索库、结构化输出解析库还算能接受。每个Agent进程用supervisor统一托管而不是裸启动一堆python agent.py后台任务。Supervisor在这套架构里好处明确能自动拉起崩溃的Agent、统一管理标准输出、控制并发启动顺序。配置大概长这样[program:agent_weather] commandpython /app/agents/weather_agent.py --role weather directory/app/agents autostarttrue autorestarttrue startsecs5 stopsignalINT不过supervisor也有局限它只管进程拉起管不了“哪个Agent是否已经注册到调度中心”。所以我们在应用层做了一次心跳上报Agent启动后先向Redis写入一条注册信息再由Pod里的健康检查端汇总这个后面会说。2.2 Pod内Agent调度与并发控制信号量才是主角250个Agent并发跑任务第一反应可能是“每个Agent开一个线程池”错了。我们踩过这个坑早期版本里每个Agent进程起10个线程结果Pod里250个线程同时抢解释器锁和数据库连接吞吐没上去CPU倒是打满了。后来改成统一的事件循环asyncio加全局信号量。所有任务进入一个Redis Stream队列消费端按agent_type分发到具体Agent的逻辑但同一时刻活跃的Agent数量由信号量控制。例如一个Pod里有31个Agent允许同时处理的任务上限设为24剩下的额度给请求排队和状态收集留余量。这样做的好处是削峰填谷不会出现所有Agent同时被塞满任务然后集体超时。真正决定并发上限的不是Agent数量而是Pod的资源配额、下游模型API的RPS限制、以及数据库连接池大小。我们当时的经验公式是Pod并发上限 min(Agent数量, CPU核数 x 4, 下游API RPS限制 / Pod数)。这个公式看起来简单但能避免很多“看起来配置没问题但一压测就崩”的场景。2.3 ConfigMap与Deployment250个Agent的配置管理250个Agent肯定不能把配置写死在镜像里否则改一个Prompt都要重新构建镜像。我们的做法是所有Agent的元信息、系统提示词、模型参数、工具开关全部放到ConfigMap里。apiVersion: v1 kind: ConfigMap metadata: name: agent-catalog data: agent_list.json: | [ {name: weather-agent, role: tool, model: qwen-plus, temperature: 0.1, max_context: 4096}, {name: contract-agent, role: longtext, model: qwen-max, temperature: 0.0, max_context: 32000}, {name: orchestrator, role: router, model: qwen-plus, temperature: 0.3, children: [weather-agent, contract-agent]} ]Deployment里通过configMapRef把它挂到每个Pod的/etc/agents/agent_list.json启动脚本读取这个文件动态拉起对应的Agent进程。这样改配置只需kubectl apply新的ConfigMapPod内部监测文件变化后热加载不用重启整个Deployment。为什么用Deployment而不是StatefulSet因为250个Agent共享同一套配置模板没有固定的网络标识需求Deployment的滚动更新和随机Pod名足够至于Agent的身份、会话状态放到外部的Redis和对象存储里不依赖Pod的持久化。这是我比较推崇的模式——Agent应该是“无状态进程 有状态存储”否则任何Pod重建都会导致状态错乱。滚动更新时还遇到一个坑默认maxUnavailable策略是25%也就是8个Pod中最多同时下线2个。但Agent重启需要重新注册如果下线速度太快线上注册表会瞬间少一批Agent导致编排型Agent派活失败。我们把maxUnavailable调成1maxSurge调成1确保至少7个Pod在线秒级损失可控。2.4 对外暴露与负载均衡入口不能直连Pod还有一个关键设计是流量入口。250个Agent不是外部直接调用的——对外只暴露一个统一网关网关根据任务类型路由到对应Pod里的对应Agent。如果外部直接打Pod负载会倾斜比如某个热点Agent集中在同一个Pod单个Pod被打挂后整个Agent类型就瘫痪了。所以我们在网关层做了二级路由先按agent_type选Pod再在Pod内部路由到具体进程。这里补充一个我后来才悟到的点大通铺架构下Pod不是服务单元整个Agent集群才是服务单元。所有对外能力、弹性伸缩、故障转移都要放在集群维度考虑。Pod只是承载进程的木桶而不是服务的边界。3. 实操过程从0到1搭建这套系统3.1 资源预估与Pod规格计算我不喜欢拍脑袋定资源所以这里把我们的计算过程完整列出来照着改就行。当时250个Agent的模型调用全部走云端API所以Pod本身不需要GPU只需要CPU、内存和网络带宽。真正的内存大头是三个地方Agent进程自身的Python运行时约80MB、上下文窗口缓存长文本型Agent尤其大单进程轻松到500MB、以及向量检索的本地索引视规模而定一般每个Agent预留100MB。短任务型Agent平均120MB长文本型Agent平均600MB编排型Agent平均200MB。按比例加权250个Agent的稳定内存需求约45GB。为了保证波动空间我们按峰值需求的1.3倍预留也就是约58.5GB。分摊到8个Pod每个Pod的requests.memory设为6Gilimits.memory设为7Gi保证单Pod内即使某个Agent异常膨胀也有1Gi缓冲区给K8s做驱逐决策的时间。CPU这边250个Agent按每个平均消耗0.2核、高峰期1.5倍超卖计算需要约75核。但实际任务大多阻塞在外部模型API返回和网络IO上CPU利用率没有线性增长。我们实测下来40核配额每个Pod 5核即可稳定运行预留配额做弹性避免限额卡脖子。看下面的表格PodAgent数requests.memorylimits.memoryrequests.cpu主要Agent类型pod-agent-0326Gi7Gi5短任务工具型pod-agent-1306Gi7Gi5短任务工具型pod-agent-2316Gi7Gi5多轮对话型pod-agent-3316Gi7Gi5多轮对话型pod-agent-4306Gi7Gi5长文本理解型pod-agent-5316Gi7Gi5长文本理解型pod-agent-6336Gi7Gi5编排回调pod-agent-7327Gi8Gi5混合兜底注意最后一行多给了1Gi内存它承担了兜底任务允许调度系统临时把其他Pod溢出的任务迁过来。这是我的个人习惯永远留一个“脏活Pod”专门接突发流量和故障转移其他地方能少则少。3.2 Deployment与ConfigMap配置示例直接给一份可以抄的Deployment片段。apiVersion: apps/v1 kind: Deployment metadata: name: agent-mesh spec: replicas: 8 strategy: rollingUpdate: maxUnavailable: 1 maxSurge: 1 selector: matchLabels: app: agent-mesh template: metadata: labels: app: agent-mesh tier: agent-runtime spec: containers: - name: agent-runtime image: registry.internal/agent-runtime:2.3.1 imagePullPolicy: IfNotPresent env: - name: POD_INDEX valueFrom: fieldRef: fieldPath: metadata.name - name: AGENT_CATALOG_PATH value: /etc/agents/agent_list.json - name: REDIS_DSN valueFrom: secretKeyRef: name: infra-secret key: redis-dsn volumeMounts: - name: agent-config mountPath: /etc/agents readOnly: true ports: - name: metrics containerPort: 9100 readinessProbe: httpGet: path: /ready port: metrics initialDelaySeconds: 20 periodSeconds: 10 resources: requests: cpu: 5 memory: 6Gi limits: cpu: 5 memory: 7Gi volumes: - name: agent-config configMap: name: agent-catalog这里有个细节POD_INDEX通过fieldRef取Pod名是因为启动脚本要根据Pod序号来决定加载哪一份Agent分组配置。ConfigMap里的agent_list.json是所有Agent的汇总脚本根据Pod序号截取自己负责的那一段这样不用为每个Pod单独写不同的ConfigMap。readinessProbe打到/ready接口这个接口返回的是“当前Pod内已注册Agent数 / 期望Agent数”。只有当注册数量达到期望值的90%以上Pod才被标记为Ready进入负载均衡池。这个设计直接解决了“Pod起来了但Agent还没就绪就被派活”的问题。3.3 启动验证与压测实测启动时有个明显的顺序要求先确认Redis和网关服务可用再部署Deployment。我们踩过一个坑第一次直接部署Deployment8个Pod同时启动Redis连接数瞬间被打满大量Agent注册失败然后Readiness探针一直不通过Pod被不断重建越重建越乱。解决方法是两个一是把批量启动改成批次启动通过K8s的initContainers先探活Redis二是在启动脚本里给每个Agent注册加随机延迟sleep random(1~3)避免250个注册请求同时到达。压测结果也很有参考价值。我们用500、1000、2000并发请求各压了20分钟数据如下并发量成功率P99延迟单Pod最大CPU单Pod最大内存50099.98%1.5s78%5.8Gi100099.92%2.3s86%6.4Gi200098.76%4.1s95%7.2Gi2000并发时已经能看到部分Pod触及内存limit有轻微驱逐风险所以线上建议把业务请求控制在1500并发以内。如果要扩容不要简单加Pod副本而是先看是哪种类型的Agent成为瓶颈——比如长文本型Agent打满那应该只扩容负责长文本的那两个Pod而不是全量扩容。4. 常见问题与排查技巧实录4.1 内存泄漏与OOMAI智能体最典型的慢性病这个项目上线一周后我最先遇到的就是OOMKilled。现象是Pod重启频率忽高忽低看kubectl describe pod发现被系统杀掉的进程集中在几个长文本Agent上内存一直涨到limit。排查思路分三步先看是不是上下文缓存问题。Agent每次对话会把历史消息攒在内存里如果用户多轮问答不清理内存只增不减。我们在Agent层增加max_context_len的截断机制超过长度就做摘要压缩而不是硬塞更多token。再看是不是工具调用链路里的文件句柄泄漏。有个Agent频繁调用本地文档解析每次打开文件后没有关闭句柄导致内存和文件描述符同步上涨。用lsof -p排查时看到数百个残留的文件句柄修复后内存立刻平稳。最后是Python GC问题。为了赶进度早期代码里用了一些“临时列表存全量结果”的写法对象一多GC就跟不上了内存碎片化严重。改成流式处理一次只保留一个结果对象GC压力小了很多。经验就是大通铺里边角料泄漏会放大250倍单Agent看起来几十MB的泄漏乘上数量就是灾难。4.2 冷启动慢与注册失败250个Agent同时冷启动第一次实际跑下来花了近5分钟。原因不是K8s拉镜像而是启动时每个Agent都要初始化模型客户端、加载本地轻量词表、然后逐个注册。我们的优化用initContainers预下载依赖和配置把公共词表做成只读层挂载Agent真正启动时只需读取本地文件注册从同步改为异步重试失败后指数退避。还有一个我觉得特别实用的技巧是把启动日志分两段看。第一段是基础设施初始化第二段才是Agent业务注册这样当启动慢时能一眼定位是卡在哪一段。我们后来把每段的耗时都输出到结构化日志配合Prometheus的agent_boot_duration_seconds直方图基本不用再猜。4.3 日志爆炸与可观测性250个Agent的日常怎么管250个Agent同时打日志不夸张地说一天的原始日志就有好几个GB。如果按默认格式2025-... INFO xxx这样打排查问题基本靠眼睛找纯纯的灾难现场。我们把所有日志改成JSON结构化输出每一条都带agent_id、task_id、model、latency_ms、status_code{ts: 2025-06-01T10:23:11.882Z, level: INFO, agent_id: contract-agent-07, task_id: t-8839201, model: qwen-max, latency_ms: 1830, status_code: 200, msg: contract clause extracted}这样排查问题时直接grep agent_id:contract-agent-07 | grep status_code:500就能拿到单一Agent的完整失败链路。我们还在Pod内做了日志轮转单个文件100MB切分再由轻量采集器汇总到Loki元数据和K8s标签关联查询效率高了不少。4.4 常见问题速查表现象可能原因解决措施Pod反复OOMKilled长文本Agent上下文缓存膨胀设置max_context_len超限摘要压缩Agent进程启动即崩配置缺失或模型API认证失败先单独跑python agent.py确认环境变量Pod Ready很慢250个Agent并发注册打满Redis启动时加随机延迟注册改异步重试压测时单个Pod延迟暴涨网关层未做二级路由请求全打到一个Pod按agent_type分配Pod入口层加柔和权重更新ConfigMap后Agent不生效进程没有监听配置变更启动脚本轮询文件hash变化后热加载Agent间状态错乱全局共享一个内存对象状态强制隔离会话状态全部外置到Redis滚动更新后注册数下降maxUnavailable过大调整为1确保至少7个Pod在线响应不稳定偶发超时信号量设置过小任务排队过多按公式min(Agent数, CPU核数*4, API RPS/Pod数)调整如果只说一条经验我推荐先做“稳定性底线设计”无论Agent多智能先把崩溃回收、注册重试、流量熔断这三件事做成默认能力。这三件事做完大通铺里住多少Agent都不慌。最后说点我个人真实的感受。这个项目做到后面我最大的体会不是K8s技巧有多重要而是250个Agent的规模会让任何“侥幸”都被放大一个Agent的内存泄漏看起来不起眼250个就是OOM一个Agent的注册失败看起来是偶发250个同时启动就是雪崩。我们最后把Agent当成“住在同一个宿舍里的人”来管理每个人都有独立床位进程隔离有公共厨房共享依赖有事找宿管编排Agent——这套比喻听起来很糙但确实帮我把架构讲清楚了也让后续加Agent时团队能快速对齐。还有个扩展方向我可以直接推荐如果业务继续涨不要盲目把250变成500先拆一层“Agent区域”——也就是把Agent按业务域分成几个小型大通铺再用一级网关做区域路由这样故障爆炸半径会更小人也更容易维护。这套玩法我还在迭代中等跑完几个月再分享第二批数据。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →