
把250个AI Agent塞进8个Pod最早听到这个需求时我是不太情愿的。Agent在我印象里应该是独立部署的服务单元250个Agent哪怕不搞250个Pod至少也得铺个几十个Pod才说得过去。可转念一想手上能用的节点就那么几台运维同事也明确表过态集群里Pod数量超过200之后控制面压力肉眼可见地上升各种LIST、WATCH请求让API Server的CPU直接抬头。再看这些Agent的画像——大多数不是7x24小时高负载运行的在线服务更像夜班保安大部分时间待在角落里任务来了才醒过来。既然如此为什么不试着安排一间大通铺这个标题里最值钱的不是250个Agent这个数字而是塞进8个Pod背后的整套取舍逻辑。本文想聊的就是这套逻辑为什么要做高密度部署、怎么设计Pod内部结构、资源账怎么算、Pod配置里哪些细节能让你深夜不被电话叫醒以及250个Agent挤在一起之后怎么监控、怎么排障。适合正在做Agent平台化、或者准备把一批内部智能化任务统一托管到Kubernetes上的团队参考。1. 从为什么说起250个Agent与8个Pod的配比逻辑1.1 这250个Agent到底是什么物种先交代一下背景。我们内部有个智能自动化平台250个Agent各自负责一个细分任务域有的是周报生成有的是工单分类有的是竞品价格抓取后的摘要有的是客服话术起草。每个Agent的Prompt、工具集和知识库引用都不相同但骨架完全一致收到任务 → 组装Prompt → 调用大模型 → 按需调用工具 → 输出结构化结果。这类Agent有几个共同特征。第一单个负载极低每天几百次调用摊到秒级就是几分钟一次第二对延迟不敏感任务跑3秒还是5秒用户基本无感第三依赖外部大模型API真正的瓶颈在API配额而不是本机CPU第四生命周期相对稳定不会像C端服务那样突然被流量打爆。这些特征决定了它们完全不需要独立Pod的隔离级别——它们需要的只是共享大模型API、共享知识库连接、共享日志通道然后各干各的。1.2 250个Deployment方案的代价很多人第一反应是给每个Agent开一个Deployment一个Pod一个Agent干净又隔离。但250个Deployment意味着集群里至少要管理250个ReplicaSet、250套Service和对应的Pod对象加起来500多个Kubernetes资源对象。这个数量级下etcd的存储和watch压力、API Server的QPS、kubelet维护Pod状态的开销都会同步上涨。更现实的麻烦是资源碎片化。一个Agent实际可能只用0.1核CPU、100MB内存但K8s为每个Pod分配IP、挂载网络策略、跑日志采集都要消耗固定成本。250个Pod摊下来一大块集群资源被K8s自身吃掉了。再加上冷启动问题每个Pod的镜像哪怕只有几百MB批量创建时对镜像仓库的并发拉取也是压力滚动升级的时候更是灾难。运维同学说Pod数超过200之后控制面明显变慢这个经验值在很多生产集群里都适用。1.3 为什么是8而不是2或者32Pod数量从250降到8核心是想清楚两个边界。一个边界是故障域。如果一个Pod挂掉里面所有Agent一起失联。把鸡蛋全放一个篮子里比如1个Pod塞250个AgentPod一崩整个平台瘫痪但开32个Pod又回到控制面压力大的老路。8个Pod意味着单Pod故障影响面是1/8再配合Pod内部的多Worker进程隔离实际损失能控制在单Pod内的四分之一左右对业务来说就是少部分任务重试一下的事。另一个边界是资源上限。单Pod能申请的资源最多不能超过所在节点1个巨型Pod很难找到能容纳250个Agent负载的节点。8个Pod能均匀分布到多台机器调度灵活滚动发布时也能按Pod逐个升级。8这个数字不是算出来的是在可用节点数和故障域大小之间取的折中。后来复盘每个Pod承载30个左右的Agent是一个比较舒适的密度再多的话日志和指标会开始互相干扰再少的话又没有必要。2. 架构路线比较为什么不是250个Pod也不是1个巨型Pod2.1 三条路线放在一起看方案隔离性K8s对象数量资源效率发布效率故障域250个Deployment每Pod一个Agent强500控制面压力大低碎片化严重可单Agent发版但批量操作复杂小单Agent故障1个Pod塞250个Agent弱单点故障极少高但受节点上限约束全量一起重启大全平台故障8个Pod每Pod约31个Agent中上进程级隔离少高能打满按Pod滚动可控最终采用的是第三行。方向定下来之后关键决策变成Pod内怎么组织这31个Agent。2.2 进程模型Manager Worker两级结构一个很常见的错误是把250个Agent塞进同一个Python进程里跑250个协程。GIL导致CPU密集型操作互相拖累不说一个Agent的全局状态污染可能直接带崩整个进程250个Agent集体陪葬。我们用的是 Manager Worker 两级进程模型每个Pod里有一个Manager进程负责Agent注册、任务分发、指标聚合和健康检查Manager下面挂4个Worker进程每个Worker用asyncio协程并发跑自己名下的Agent平均一个Worker管8个Agent。Manager进程本身非常轻不执行任何Agent逻辑只做调度和守护。如果某个Worker进程因为底层库崩溃退出Manager会在几秒内把它拉起来并把这个Worker名下未完成的任务重新入队等待新Worker接盘。这个设计和Kubernetes的Pod重启机制形成了两级故障恢复Pod挂了由K8s负责Worker挂了由Manager负责Agent异常由AgentRunner负责每层只处理自己这一级的故障。2.3 为什么没有上LangGraph这类重框架很多团队做Agent平台的第一反应是引入LangGraph、CrewAI之类的编排框架。我们评估过一轮就放弃了。原因很简单这250个Agent绝大多数是单步或两步任务任务进来 → 组装Prompt → 调LLM → 调工具 → 输出结果没有复杂的图依赖、没有多Agent协作、没有记忆回放需求。引入框架只是增加了序列化开销、概念负担和升级风险。我们最后写了一个不到200行的AgentRunner支持Tool注册、超时控制、重试和审计日志。Agent本身的代码量也不大一份Prompt模板、一个工具函数列表、一个输出解析器打包成一个目录放到注册表里就行。等真正遇到需要多Agent协作、需要图编排的复杂场景再上框架不迟。框架永远是第二位的先把你自己的执行循环写扎实比什么都强。这就是大通铺的运营哲学大多数Agent本来就只是Mapper任务不需要给每个住客都安排一个总统套房。3. 资源账本250个Agent到底吃掉多少CPU与内存3.1 单个Agent的负载画像与共享连接要给250个Agent配置Pod资源得先搞清楚单个Agent的消耗模型。以我们的典型Agent为例每天约300次任务平均每次执行耗时2到4秒其中真正占CPU的时间不到0.2秒——主要消耗在Prompt组装、工具结果解析和JSON序列化上剩下的大头时间都在等待大模型API响应。内存方面一个Agent本身很轻Prompt模板、状态结构体、工具连接的引用大约30到50MB。这里有个特别容易被忽视的坑如果每个Agent都new一个自己的LLM Client实例250个Agent就会各自维护一套HTTP连接池端口和文件描述符直接被打爆。正确做法是Pod内所有Agent共享同一个LLM Client实例底层连接池复用。这样整个Pod的对外连接数量就能控制在合理的几十条量级不然资源账怎么算都是错的。3.2 算一笔账从任务量反推资源配置先看QPS。假设平台日均处理10万次任务集中在早8点到晚8点的12个小时内平均QPS大约2.3。峰值按4倍估算也就10个并发请求。10个并发分摊到8个Pod每个Pod连2个并发都不到CPU完全不是瓶颈。算CPU也一样。每次任务平均消耗0.2核秒的CPU10万次任务总计2万核秒摊到全天86400秒是0.23核的平均消耗峰值按5倍算差不多1.2核。这个数字告诉我们requests里的CPU根本不用给高给太高反而会挤压同节点上的其他工作负载。内存账本要按Pod粒度算我实际压测后得到的数据大致是这样项目估算说明Manager进程约200MB管理调度、指标聚合4个Worker进程每个约400MB共1.6GB每个Worker加载公共依赖和Tool代码LLM连接池与缓冲约300MB共享Client tokenizer缓冲日志与临时对象约200MBJSON日志缓冲、请求上下文余量30%约700MB防止回收不及时导致OOM合计约3GB单Pod内存请求量3.3 requests与limits的取舍能打满才是好配置资源配额的设置上我和很多人意见相反requests不应该等于limits。在多Agent并发场景下任务有突发性和排队特征把limits设成和requests一样一旦瞬时并发上来CPU就会被throttling限制大模型API的等待时间进一步拉长反而让整个Pod变慢。实测配置是requests 给 1核CPU 3Gi内存limits 给 4核CPU 8Gi内存。requests按稳态需求来limits按峰值的两三倍来。内存limits没有给更高是因为内存不像CPU可以杀后重来一旦逼近limits就会被OOMKilled整Pod重启代价太大。宁可给一个吃紧一点的8Gi让压测提前暴露内存泄漏也不要给一个永远不会触发的20Gi然后某天宿主机内存被拖垮。压测方法很简单按150%的预估峰值流量打进去持续跑30分钟观察Pod的内存水位和Agent任务的P99延迟。我们实测4核8Gi的Pod内跑32个AgentP99延迟从2秒涨到3秒左右可以接受配置就这么定了。4. Pod清单里的关键细节探针、限额与优雅退出4.1 StatefulSet而不是Deployment很多人以为Agent是无状态服务应该用Deployment。但这里有一层需求每个Pod需要知道自己是谁才能去注册表里领取属于自己的31个Agent。所以我们用了StatefulSetreplicas设为8通过Pod序号agent-bunk-0到agent-bunk-7做天然分片标识。apiVersion: apps/v1 kind: StatefulSet metadata: name: agent-bunk spec: serviceName: agent-bunk-headless replicas: 8 podManagementPolicy: Parallel template: spec: containers: - name: agent-runtime image: registry.internal/agent-runtime:2.4.1 ports: - containerPort: 9100 envFrom: - configMapRef: name: agent-runtime-config env: - name: POD_INDEX valueFrom: fieldRef: fieldPath: metadata.labels[apps.kubernetes.io/pod-index] resources: requests: cpu: 1 memory: 3Gi limits: cpu: 4 memory: 8Gi livenessProbe: httpGet: path: /healthz port: 9100 initialDelaySeconds: 30 periodSeconds: 15 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /ready port: 9100 initialDelaySeconds: 20 periodSeconds: 10 terminationGracePeriodSeconds: 60podManagementPolicy设为Parallel很关键8个Pod同时启动而不是串行等一个起来再起下一个把集群初始化时间从几分钟压到几十秒。POD_INDEX通过Downward API从内置标签读取如果集群版本低于1.27直接用StatefulSet的hostname后缀也能拿到同样的序号。4.2 探针只探Manager不探Agent探针设计上是又一个容易踩坑的地方。250个Agent如果都做探针kubelet要维护上千个探针任务探针本身就会成为性能瓶颈。实际做法liveness探针只探Manager进程的/healthz接口确认进程活着即可。readiness探针探/ready接口确认Pod已经完成分片加载和Agent注册可以接收任务了。initialDelaySeconds给30秒是有讲究的。41个Agent的配置加载、Tool代码导入、LLM连接池初始化在容器启动后是要花时间的。给短了会出现启动期间探针失败被K8s反复重启的恶性循环越重启越起不来。这个延迟值建议根据实际启动耗时再加10到15秒余量。4.3 配置下发ConfigMap挂载而不是环境变量硬灌250个Agent的Prompt模板、模型参数、工具路由表如果全部塞进环境变量容器环境变量段会膨胀到几十KB不仅触及系统上限排障时更是噩梦。我们的做法把这些配置整理成 Agent 注册目录通过ConfigMap挂载到容器内的/etc/agent-registry/路径下Pod启动时按 POD_INDEX 读取自己的切片。需要注意ConfigMap有1MB的大小限制。250个Agent的Prompt模板加起来如果超了就得把公共部分抽进镜像、私有部分放外部配置中心。我们实践中把模板和路由表压缩到了400KB左右还有余量。每个Agent的敏感凭据不要放ConfigMap用Kubernetes Secret单独挂载这是安全底线。4.4 优雅退出restart不该变成事故每个Pod里31个Agent可能同时在处理任务。如果K8s滚动更新或节点维护时直接SIGTERM杀掉容器执行中的任务会被截断下游系统的重试风暴会瞬间把队列打爆。优雅退出是我们上线后补的第一块板。现在的链路是preStop钩子调用Manager的/shutdown接口进入drain模式停止消费新任务 → Manager把正在执行的任务标记为可重投交给消息队列做at-least-once投递 → Worker收到退出信号后完成当前正在进行的LLM调用最多等10秒→ 落盘检查点文件 → 进程退出。terminationGracePeriodSeconds设60秒给这串动作留足时间。有一次我们没配优雅退出一个Pod滚动发布时几十个任务全部中断下游系统连续收到超时告警折腾到大半夜。后来加了drain逻辑同样操作再跑一轮任务中断率直接降到0。这块代码不复杂但带来的稳定性收益极高。5. Pod内部的秩序并发、限流、超时与熔断5.1 并发控制信号量而不是无限协程250个Agent挤在8个Pod里最怕的就是某个时刻所有Agent同时醒来把LLM API配额瞬间打穿。每个Worker如果放任名下8个Agent同时执行就是8路并发请求往外冲4个Worker就是32路集群瞬时压力直接翻几倍。解决方式是给每个Worker一个asyncio.Semaphore默认5。也就是一个Worker最多同时跑5个Agent任务多余的自动排队。这样单个Pod的理论最大并发被压在20路左右8个Pod最多160路LLM请求远低于账户配额的安全区间。Agent任务本身不敏感排队所以这种压并发的方式对整体吞吐影响很小。sem asyncio.Semaphore(5) async def run_agent(agent_id: str, task: dict): async with sem: return await agents[agent_id].execute(task)5.2 LLM API限流本地令牌桶信号量控制的是并发数但LLM API限流通常按TPM每分钟Token数和RPM每分钟请求数来算。并发数控制住了不代表Token消耗不会爆因为每个请求的Token数差异很大。我们给每个Pod配了一个本地令牌桶按速率限制调用频率超过速率就把请求排到下一轮。class TokenBucket: def __init__(self, rate: float, capacity: float): self.rate rate self.capacity capacity self.tokens capacity self.updated_at time.monotonic() async def acquire(self, cost: float 1.0): ...令牌桶的好处是允许短时间突发但又约束了长期均值。实测下来8个Pod的瞬时总消耗被稳定压在一个可控区间内既不会触发平台侧的限流告警也不浪费配额。限流参数别拍脑袋去看你账户的配额上限留出30%的安全余量。5.3 超时与熔断别让一个慢工具拖死整个通铺Agent执行过程中最危险的不是LLM慢而是某个下游工具突然变慢。我们遇到过第三方百科抓取接口突然变慢的情况单次请求要50秒结果一个Worker下8个Agent全在等这个工具整个Pod的任务吞吐肉眼可见地往下掉。应对措施做了两层。第一层是超时每个Agent任务包一个总超时默认180秒LLM单次调用超时30秒工具调用HTTP、DB、爬虫单次超时10秒。所有超时后写一条结构化错误日志。第二层是熔断为每个下游工具维护一个连续失败计数器失败超过5次就把该工具标记为open状态后续Agent直接快速失败不再等待60秒冷却窗口过后放一个探测请求试试水温。这层熔断防止了一个慢依赖拖死整Pod的传导效应。问题被隔离在特定Agent自身其他Agent完全不受影响。熔断阈值调低一点没事宁可误伤也不放慢。5.4 异常隔离每个Agent一个逃生舱Agent的代码不像常规服务那么可控Prompt内容千奇百怪Tool返回的数据结构也可能随时变化任何意外都可能触发未捕获异常。如果异常穿透到进程级别Manager进程会崩溃整个Pod重启31个Agent集体陪葬。我们在AgentRunner里给每个Agent套了独立的异常捕获边界任何未捕获异常记录下来、任务标记失败、错误计数器加1进程继续跑。连续失败超过30次Manager把该Agent标记为disabled并发出告警由人工介入。这个保护机制上线第2周就发挥了作用——某个新Agent写错了工具参数如果异常穿透到进程级Manager会崩溃当时跑着的全部Agent都会遭殃。现在这个Agent自己倒下其他人安然无恙。6. 监控与排障在250个Agent里精准揪出问题儿童6.1 结构化日志是排障的基石250个Agent挤在8个Pod用普通文本日志排障就是灾难。所有日志统一JSON格式输出到stdout由采集端按pod、agent_id建立索引。一条典型的完成任务日志长这样{ ts: 2025-06-12T10:23:45.123Z, pod: agent-bunk-3, agent: weekly-report-gen, task: t_9f2c7e1a, event: task_finish, latency_ms: 2143, llm_tokens: 320, tool_calls: 2, error: null }有了这个结构查问题就是一条查询语句的事按pod过滤、按agent过滤、按时间范围过滤链路清晰。没有结构化之前我们靠grep还经常抓到别的Pod串过来的日志后来统一加pod字段后这类误判基本消失。6.2 指标设计一个指标加label而不是250个指标Prometheus的指标设计有个原则不要为每个Agent创建独立的指标名用一个指标加label区分。250个Agent对应250个label组合在Prometheus里完全不是负担。我们暴露的指标不多够用就行agent_runtime_info当前启停的Agent数量和状态agent_tasks_total任务处理的counteragent_task_duration_seconds执行耗时的histogram按agent和task_type打labelagent_errors_total错误计数agent_busy当前正在执行的Agent数Grafana看板上任务耗时的P90/P99、错误趋势、各Agent活跃度一目了然。每季度做一次容量演练按2倍流量压测验证熔断和伸缩逻辑是否还正常。6.3 一次真实的排障链路复盘举一个实际案例。某个Agent的任务耗时从平均3秒涨到60秒业务方先在群里吐槽。排查链路是这样的第一步Grafana上看agent_task_duration_seconds的P90曲线发现涨幅集中在某个agent_id上其他Agent完全正常——这就把排查范围从250个缩到了1个。第二步拉取这个Agent最近一小时的日志发现工具调用price_searcher.query全部超时而且没有触发熔断因为失败率没到阈值只是单纯变慢。第三步进一步看LLM响应时间发现该Agent的Prompt因为检索文档膨胀导致输入Token翻倍大模型API排队时间变长。最后解决办法是压缩检索文档、限制context长度P99从60秒降到8秒。对比250个独立Pod的场景这个排障链路的第一步可能变成从250个Deployment里找到底是哪个Pod在出问题光是这一步就要多花10分钟。高密度部署反而让排查路径变短了这是个意外收获。6.4 上线后还得管的几件事第一版本碎片管理。250个Agent不能共用一个镜像版本Agent迭代会有快有慢要给每个Agent独立的版本号灰度阶段用金丝雀标记路由到小流量。第二定期清理僵尸Agent。有些Agent上线后被业务遗忘任务量长期为零要定期扫描并下线省出的资源留给真正有需要的Agent。第三和集群团队约定好不要把其他工作负载混到这些Agent所在的节点避免节点内存水位波动影响探针超时。第四给重要Agent配几套不同的Prompt模板做灰度对比A/B测试不能只存在于业务上线环节Agent上线也一样。如果让我总结这套方案最核心的收获不在K8s配置而在秩序二字。250个Agent本身不是问题250个没有并发控制、没有限流、没有超时熔断、没有优雅退出的Agent才是问题。把基础设施的活干完剩下的是制度和纪律。如果你也在做类似的事情我建议从资源核算和优雅退出这两块开始抄作业它们是最快见效、也是出事时最救命的。Pod里的并发控制、超时和熔断值得在业务代码里多花两天去写扎实这笔账怎么算都不亏。