记一次多 Agent 架构在单节点 K8s 触发的 PID 耗尽与 Pod 驱逐(Evicted)大摸排

📅 2026/7/9 16:32:24
记一次多 Agent 架构在单节点 K8s 触发的 PID 耗尽与 Pod 驱逐(Evicted)大摸排
1. 现象描述频繁出现的僵尸 Pod在维护基于 KubernetesK8s v1.31.0单节点集群部署的 AI 多 Agent 系统包含主调度程序、8个独立的 MCP 后台异步工具服务以及 5个 MySQL、3个 Kafka 等密集有状态中间件时发现主应用 Pod 频繁闪退并残留大量状态为ContainerStatusUnknown的僵尸 PodBash$ kubectl -n agent-intent-system get all NAME READY STATUS RESTARTS AGE pod/agent-intent-79666568d5-5cvlp 0/1 ContainerStatusUnknown 1 5d16h pod/agent-intent-79666568d5-f7zq2 0/1 ContainerStatusUnknown 1 38h pod/agent-intent-79666568d5-j5jgb 1/1 Running 0 38h通过kubectl describe pod物理追查死亡 Pod 的临终元数据抓到了核心错误YAMLStatus: Failed Reason: Evicted Message: The node was low on resource: pids. Threshold quantity: 6553, available: 5127. QoS Class: BestEffort2. 根因拆解藏在字面背后的 K8s 驱逐算法第一眼看到available: 5127时很容易产生直觉误判“明明还有 5127 个可用 PID为什么 K8s 会说不够并引发驱逐”2.1 报警线而非耗尽线在 Kubernetes 驱逐硬阈值Eviction Hard Thresholds机制中Threshold quantity: 6553代表的不是上限而是安全死守底线保护水位线。K8s 的底层判定逻辑是为了确保单节点宿主机操作系统的内核不因进程数耗尽而物理死机必须时刻留出至少 6553 个 PID 额度。一旦集群可用 PID 掉到 6553 以下立刻进入紧急避险模式。当时系统的实际状态5127 (可用) 6553 (报警线)触发安全熔断。2.2 为什么你配了10000却完全不够用系统默认配了podPidsLimit: 10000但分配公式为$$\text{业务合法可用车位} \text{总配额 (10000)} - \text{系统内定应急车道 (6553)} 3447 \text{ 个}$$在 Linux 操作系统中线程Thread和进程Process在底层使用的是完全相同的结构task_struct在内核看来一个线程就是一个 PIDMCP 后台服务矩阵8 个并发工具服务基于asyncio/gRPC开发带有高并发连接池开开机就常驻了数百个内核 PID。多 Agent 链式调度主程序在进行 LLM 协同调度和密集计算时频繁调用线程池ThreadPoolExecutor。在之前 Kafka 遭遇网络解析冲突期间应用高频触发死循环重试导致发生严重的线程泄漏Thread Leaks瞬间堆积数千个线程。单节点中间件污染同台机器上的 5 个 MySQL 实例和 3 个 Kafka 节点Java 线程大户互相高频握手多重夹击之下总进程/线程数瞬间挤爆了3447的狭窄配额踩中雷区。2.3 为什么沦为第一牺牲品因为主应用 Pod 未设置resources.requests和limits其QoS Class被内定为BestEffort低优先级。当单节点 PID 余额告急时Kubelet 会优先挑软柿子捏直接跨过容器运行时Containerd通过物理信号强杀Evict该 Pod。Containerd 无法定位容器肉身随之向上层吐出ContainerStatusUnknown状态。3. 治理全记录解封物理限制与服务升舱Step 1彻底放开单节点 PID 封印既然是高并发 AI 矩阵系统必须把单节点的水库容量做大。物理登录node1修改 Kubelet 核心配置文件Bash$ sudo vim /var/lib/kubelet/config.yaml # 将 podPidsLimit 修改为大厂标准级高上限 podPidsLimit: 65535重启中枢神经Bash$ sudo systemctl daemon-reload $ sudo systemctl restart kubeletStep 2精准打通参数验证利用 K8s 隐藏接口和 Linuxcgroup控制组内核文件进行双重验证确保65535被物理引擎吃进去Bash$ kubectl get --raw /api/v1/nodes/node1/proxy/configz | grep -E -o podPidsLimit:[0-9] podPidsLimit:65535 $ cat /sys/fs/cgroup/pids/kubepods.slice/pids.max 65535数据完美对齐内核限额彻底解开。Step 3应用服务升舱QoS 防御修改 Helm 模板中的deployment.yaml显式为应用打上资源标签脱离BestEffort低优先级升级为Burstable确保未来即使节点再次发生资源震荡也不会被 K8s 优先强杀。YAMLresources: requests: cpu: 500m memory: 1Gi limits: cpu: 2 memory: 4Gi4. 总结与避坑指南警惕 AI 框架的线程堆积在 Python 多 Agent 架构中任何网络未就绪引发的重试盲目并发都会在 Linux 内核层疯狂制造“PID 弹药”很容易在单节点上反噬 K8s 宿主机。单节点运维扩容先行在全栈单节点All-in-One测试环境中跑密集微服务与有状态数据库时务必提前将操作系统的podPidsLimit以及max_user_instances等紧箍咒放大防止系统防御机制误伤主业务线。