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

1. 现象描述:频繁出现的僵尸 Pod

在维护基于 Kubernetes(K8s v1.31.0,单节点集群)部署的 AI 多 Agent 系统(包含主调度程序、8个独立的 MCP 后台异步工具服务,以及 5个 MySQL、3个 Kafka 等密集有状态中间件)时,发现主应用 Pod 频繁闪退,并残留大量状态为 ContainerStatusUnknown 的僵尸 Pod:

Bash

复制代码
$ 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 的临终元数据,抓到了核心错误:

YAML

复制代码
Status:        Failed
Reason:        Evicted
Message:       The node was low on resource: pids. Threshold quantity: 6553, available: 5127.
QoS Class:     BestEffort

2. 根因拆解:藏在字面背后的 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),在内核看来,一个线程就是一个 PID!

  • MCP 后台服务矩阵 :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 kubelet

Step 2:精准打通参数验证

利用 K8s 隐藏接口和 Linux cgroup 控制组内核文件进行双重验证,确保 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 优先强杀。

YAML

复制代码
resources:
  requests:
    cpu: "500m"
    memory: "1Gi"
  limits:
    cpu: "2"
    memory: "4Gi"

4. 总结与避坑指南

  1. 警惕 AI 框架的线程堆积:在 Python 多 Agent 架构中,任何网络未就绪引发的重试盲目并发,都会在 Linux 内核层疯狂制造"PID 弹药",很容易在单节点上反噬 K8s 宿主机。

  2. 单节点运维扩容先行 :在全栈单节点(All-in-One)测试环境中跑密集微服务与有状态数据库时,务必提前将操作系统的 podPidsLimit 以及 max_user_instances 等紧箍咒放大,防止系统防御机制误伤主业务线。

相关推荐
逐流人27 分钟前
手工建集群到全自动发布:k8s集群部署与DevOps流水线整合实战
ci/cd·云原生·kubernetes·云计算·devops
我不会起名字3221 小时前
K8s Pod 反复重启 CrashLoopBackOff:从 exit code 到根因的 5 步排查
云原生·容器·kubernetes·k8s
瑞克-桑切斯1 小时前
ERP 企业生产管理系统部署手册
运维·docker·云原生·容器·eureka
freejackman1 小时前
【工程化】从“单兵作战”到“多引擎架构”:基于门面模式的重构实战
大数据·数据库·hive·sql·测试工具·设计模式·架构
我不会起名字3222 小时前
K8s 节点磁盘打满导致 Pod 被驱逐:4 类占用与回收顺序
运维·kubernetes·k8s·diskpressure·evicted
瑞克-桑切斯2 小时前
docker mail商城
运维·docker·容器
java_logo3 小时前
Docker 部署 ReMe:轻松搭建 AI Agent 记忆管理平台
人工智能·docker·容器·ai agent·轩辕镜像·reme·agentscope-ai
西安小哥3 小时前
大厂把大模型做为一层“网关”,比换个框架更值得琢磨
架构·langchain
不庸.3 小时前
WPScan 实战入门:从 Docker 靶场到弱密码爆破
运维·安全·web安全·docker·容器·安全性测试·wpscan
吴建旭 智宅焕4 小时前
智能家居全国交付中报价工具与交付基础设施的架构分离设计:接口化路径与交付确定性系统实现
架构·智能家居