近期排查了一起极其惨烈的生产 K8S 集群级故障。监控大屏上,APIServer 的 99th percentile 延迟直接突破 8 秒,etcd 的 wal_fsync_duration_seconds 告警狂飙,部分核心业务的健康检查因 API 响应超时被误判驱逐。
直接抛出结论:这是典型的由于 Jenkins Kubernetes Plugin 基础网络配置错误,叠加无脑并发重试,最终演变成的微型 DDoS 攻击。开发团队在 Jenkins K8S Agent 配置中,将 Jenkins URL 填成了挂着 WAF 和七层鉴权的公网 Ingress 域名。动态拉起的 Pod 无法建立 JNLP 长连接导致闪退,而 Jenkins Master 在积压任务的驱动下疯狂重试,一小时内在集群里发起近 8 万次 Pod 创建请求,活生生把 APIServer 和 Kubelet 拖垮。
遇到网络不通,第一反应不是去查 DNS 和网络 ACL,而是在 Jenkinsfile 里强行加 retry(5) 甚至利用插件自身的重试机制去“碰运气”,这种用高频重试来掩盖网络隔离的草台班子做法,是系统稳定性的头号杀手。
现场还原:被击穿的 APIServer 与满屏的 Error
排查过程中,第一步自然是看 K8S 的核心指标。 通过 PromQL 查询 APIServer 的创建请求速率,数据触目惊心:
sum(rate(apiserver_request_total{resource="pods", verb="create"}[2m])) by (namespace)
结果显示 jenkins-build 命名空间的 QPS 高达几百,且持续了将近半小时。
切到命令行,看一眼现场:
$ kubectl get pods -n jenkins-build | awk '{print $3}' | sort | uniq -c
12 Running
180 Pending
4592 Error
1208 Terminating
随便抓一个 Error 的 Pod 看日志,熟悉的 JNLP 报错:
INFO: Locating server among [https://jenkins.company.com/]
SEVERE: Failed to connect to https://jenkins.company.com/tcpSlaveAgentListener/: java.net.ConnectException: Connection refused (Connection refused)
...
INFO: Protocol JNLP4-connect encountered an unexpected exception
java.util.concurrent.ExecutionException: org.jenkinsci.remoting.protocol.impl.ConnectionRefusedException: Connection closed before acknowledgment sent
这就是问题的万恶之源:Agent Pod 启动后,试图通过 JNLP (TCP 50000 端口) 或 WebSocket 连接 Jenkins Master。但 https://jenkins.company.com 是外部域名,流量出集群绕一圈走到公网网关,不仅被 WAF 拦截了 TCP 长连接,由于缺乏认证,HTTP 升级 WebSocket 的请求也被干掉。
Agent 连不上,容器退出(Exit Code 255)。Jenkins K8S 插件一看:“哎呀,Agent 没注册成功”,同时任务队列里还堆积着上百个等候编译的 Job。于是,调度循环再次触发,向 K8S 申请拉起新的 Pod。 建 Pod -> 网络不通 -> 闪退 -> 重新申请。 一个完美的死循环诞生了。海量的短生命周期 Pod 导致 kube-controller-manager 和 kubelet 的垃圾回收(GC)根本来不及清理,系统负载直线上升。
深层剖析:为什么 K8S Plugin 变成了攻击者?
如果仅仅是网络不通,顶多是 CI/CD 瘫痪,为什么会拖垮整个集群?这涉及到几个底层的缺失与滥用。
1. JNLP 寻址逻辑的错误
在 Jenkins K8S Plugin 中,Master 负责调用 APIServer 生成 Pod,但 Pod 必须主动向 Master 注册。
配置项中有两个致命参数:jenkinsUrl 和 jenkinsTunnel。
如果没有明确指定 jenkinsUrl,Plugin 会使用系统全局的 Jenkins Location。这通常是为了发送邮件通知配置的外网域名。Agent 在集群内部去解析外网域名,不仅增加了 CoreDNS 压力,还会受到外部防火墙规则的约束。
2. 并发与重试失控 (Lack of Circuit Breaking)
默认情况下,Jenkins 并没有严格的、针对 K8S API 调用的全局熔断机制(Circuit Breaker)。当队列里有 100 个任务等待时,Plugin 就会并发去创建 100 个 Pod。当这些 Pod 全部异常退出时,调度逻辑会认为这是“短暂的环境抖动”,立刻安排下一轮调度。 整个过程就像是没有任何限流措施的恶性重试风暴,K8S APIServer 成了最终的受害者。
3. 缺乏防御性的 Namespace 资源配额 (ResourceQuota)
整个 jenkins-build 命名空间处于完全无限制(Unbounded)状态。没有任何的 ResourceQuota 限制其最大 Pod 数量。Kubelet 在频繁创建和销毁容器时,底层的 containerd/runc 需要频繁操作 Cgroup 树和挂载 overlayfs,直接导致 Node 节点的 System CPU 耗尽,Load Average 飙升至 50 以上。
防御性重构:如何把 Jenkins 关进笼子里
处理这种问题,止血只是第一步(删掉堆积的队列,停止 K8S 调度),更重要的是从架构层面切断这种重试雪崩的可能。
1. 强制走集群内网寻址 (JCasC 固化配置)
既然 Jenkins Master 和 Agent Pod 都运行在同一个 K8S 集群内(或通过专线打通的集群),绝对不应该走公网绕行。直接使用 K8S 的内部 DNS (Service 发现) 解决连通性。 通过 JCasC (Jenkins Configuration as Code) 强制约束配置:
jenkins:
clouds:
- kubernetes:
name: "kubernetes"
serverUrl: "https://kubernetes.default"
# 核心修复点:使用内网 Service FQDN
jenkinsUrl: "http://jenkins-master.jenkins-sys.svc.cluster.local:8080"
jenkinsTunnel: "jenkins-master.jenkins-sys.svc.cluster.local:50000"
# 全局容器并发上限,防止雪崩
containerCap: 50
podRetention: "Never"
利用 jenkins-master.jenkins-sys.svc.cluster.local,流量直接走 kube-proxy 转发,不仅绕过了外部网关,还大幅降低了延迟。
2. 设置 Pipeline 防死锁与超时机制
在全局和单个 Pipeline 级别,必须引入严格的超时策略(Timeout),同时禁止在外部资源申请阶段使用无限重试。
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
activeDeadlineSeconds: 1200 # 防御性配置:Pod 绝对最大存活时间
containers:
- name: maven
image: maven:3.8-eclipse-temurin-17
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "4"
memory: "4Gi"
'''
}
}
options {
// 全局超时,防止僵尸 Job 挂起耗尽 Jenkins 线程池
timeout(time: 30, unit: 'MINUTES')
// 严禁在此处滥用 retry()
}
stages { ... }
}
特别是 activeDeadlineSeconds 属性,这是 K8S 原生的硬中断。即便 Jenkins Master 假死,K8S 也会在 1200 秒后强行清理该 Pod,防止僵尸 Agent 占用节点资源。
3. 命名空间级别的强硬隔离
永远不要相信 CI/CD 系统的自我控制能力。在运维层面,必须给 K8S 加上硬性 Quota。 应用如下配置到 CI/CD 命名空间:
apiVersion: v1
kind: ResourceQuota
metadata:
name: jenkins-build-quota
namespace: jenkins-build
spec:
hard:
pods: "100" # 绝对禁止超过 100 个 Pod
requests.cpu: "200"
requests.memory: 400Gi
当系统进入异常循环,创建到第 101 个 Pod 时,APIServer 会直接返回 403 Forbidden,这比让 kubelet 去硬扛崩溃要好得多。
排查清单:同类问题速查
-
APIServer 负载突增排查:立刻执行
kubectl get events -n,如果发现满屏同名/同规则 Pod 密集创建和 Failed 记录,大概率是外部调度器陷入死循环。--sort-by='.metadata.creationTimestamp' -
Jenkins JNLP 寻址校验:检查 Jenkins Master 系统设置中的
Jenkins URL,若配置为外部域名,确保该 Agent 所在网络可以直接解析并连通该域名的 8080 及 50000 端口,强烈推荐改为内部svc.cluster.local。 -
僵尸 Pod 排查:执行
kubectl get pods --field-selector status.phase=Failed -A,如果存在大量 Failed 或长期处于 ContainerCreating 的 Pod,检查是否缺少activeDeadlineSeconds约束。 -
Agent Pod 无限 Pending 分析:检查是否在 Jenkinsfile 中请求了集群无法满足的资源(如单 Pod 申请 32核 64G),导致调度器无法找到合适节点,同时 Jenkins 又在不断触发新构建。