Jenkins 动态 Agent 架构在处理高并发构建时极易触发系统雪崩。核心元凶通常是 kubernetes-plugin 默认极低的 Client-Go QPS 限制引发 API 节流与 Pod 调度积压,叠加 NAT 网关静默丢弃 JNLP 空闲连接导致断连风暴。破局的关键在于:切换 Agent 通信至 WebSocket 协议,利用底层 System Properties 强行拉高 K8S 客户端 QPS/Burst 阈值,并通过 JCasC 实施防御性的超时与重试固化配置。
故障现场:几百个 Pipeline 瞬间卡死,Master 线程池耗尽
某次在应对业务大版本集中发布时,Jenkins(版本 2.426.1 LTS,kubernetes-plugin 版本 4136.v7233)出现突发性大面积卡顿。
现场症状:
-
构建积压:超过 300 个 Pipeline 任务处于 pending 状态,卡在
Jenkins doesn’t have label XXX。 -
僵尸 Pod 泛滥:K8S 集群中存在大量状态为
Terminating或Running但未在执行任务的 Jenkins-Agent Pod。 -
Master 假死:Jenkins Web UI 响应极其缓慢,Load Average 飙升至 80+,JVM 老年代内存使用率长期处于 95% 以上,频繁触发 Full GC。
通过 jstack 抓取 Jenkins Master 的线程快照,发现大量线程阻塞在 Kubernetes 客户端的 HTTP 请求调度上,同时伴随疯狂报错的系统日志:
# 报错一:JNLP Ping 超时风暴
WARNING: Ping thread for channel JNLP4-connect connection from 10.244.5.122:38912 failed.
java.util.concurrent.TimeoutException: Ping started at 171xxxxxxx hasn't completed by 171xxxxxxx+240000
at hudson.remoting.PingThread.ping(PingThread.java:132)
# 报错二:Kubernetes Plugin API 限流
WARNING: Failed to provision a new node.
io.fabric8.kubernetes.client.KubernetesClientException: too many requests (429)
at io.fabric8.kubernetes.client.dsl.internal.OperationSupport.requestFailure(OperationSupport.java:694)
为什么 Jenkins Master 会被 K8S 动态 Agent 拖垮?
表象是 Jenkins 性能不足,底层其实是通信协议缺陷与默认配置短板在并发场景下的集中爆发。
1. K8S 插件 Client-Go QPS 限流导致的调度饥饿
Jenkins Kubernetes 插件底层依赖 fabric8io/kubernetes-client。在缺乏显式配置的情况下,该客户端继承了极低的默认流控阈值(早期版本 QPS=5,Burst=10)。
当瞬间涌入几百个动态 Agent 申请时,Jenkins 向 Kube-APIServer 发起大量的 Pod Create/Watch 请求。触发限流(HTTP 429)后,客户端会指数退避重试。这不仅导致 Pod 迟迟无法拉起,还会使 Master 端负责 Provisioning 的专属线程被长时间挂起,最终耗尽线程池资源,引发 Web UI 卡死。
2. NAT 网关静默丢弃引发 JNLP 断连风暴
传统的 JNLP 代理协议基于 TCP长连接(默认端口 50000)。在容器化部署中,Agent Pod 通常经过 NodePort、Ingress 或云厂商的 NAT 网关与 Master 通信。
许多 NAT 网关/防火墙对空闲 TCP 连接有严格的存活期限制(如 5 分钟或更短),若无数据传输会静默丢弃(Drop)连接,且不发送 RST。
Jenkins 默认的 PingThread 检测周期是 4 分钟。当构建任务处于长时间的纯本地编译(如 make -j16)且没有向 Master 输出日志时,TCP 连接会被 NAT 掐断。此时 Master 仍在等待 Ping 回应,直到超时报错终止构建。随后 Master 尝试销毁 Pod,但由于上述的 API 限流,Delete 请求失败,直接产生大量“孤儿 Pod”。
3. Pipeline CPS 转换引发的 Master CPU 燃烧
部分研发在 Pipeline 的共享库(Shared Library)中编写了复杂的 for/while 循环或对大体积 JSON 进行了反序列化,且未加 @NonCPS 注解。Jenkins Pipeline 的 Continuation Passing Style (CPS) 引擎会将这些逻辑转换成成百上千个小的状态机对象存储到 Heap 中。大量的状态变更叠加 Agent 断连引发的异常处理逻辑,导致 Master 的 CPU 被 GC 线程和 CPS 引擎彻底吃光。
极客实战:防御性配置与底层调优
拒绝修修补补,直接从网络协议、K8S 客户端参数和不可变基础设施层面彻底重构。
调优 1:废弃 TCP JNLP,全面启用 WebSocket 通道
WebSocket 基于 HTTP/HTTPS 进行协议升级,复用 80/443 端口。标准 L7 Ingress/LB 对 WebSocket 的保活支持远好于裸 TCP 端口,有效穿透各类严格的防火墙。
需要在 Jenkins System 中开启 WebSocket 并在 K8S Agent 模板中强制指定。通过 JCasC (Jenkins Configuration as Code) 固化配置如下:
jenkins:
cloud:
kubernetes:
name: "kubernetes"
serverUrl: "https://kubernetes.default"
# 开启 WebSocket 连接
webSocket: true
containerCapStr: "200" # 限制最大并发 Pod 数,防止打爆集群
templates:
- name: "base-agent"
label: "base-agent"
nodeUsageMode: EXCLUSIVE
containers:
- name: "jnlp"
image: "jenkins/inbound-agent:3148.v532a_7e715ee3-1"
# JNLP 容器的防御性资源限制
resourceRequestCpu: "500m"
resourceLimitCpu: "1000m"
resourceRequestMemory: "512Mi"
resourceLimitMemory: "1024Mi"
调优 2:暴力破解 K8S 客户端并发限制
直接通过 JVM 启动参数(System Properties),向 Kubernetes 客户端注入高并发阈值配置,并缩短 JNLP 的 Ping 超时窗口以尽早发现死连接。
在 Jenkins Master 的 Deployment/StatefulSet 中注入以下 JAVA_OPTS:
# 提升 fabric8 k8s client 并发上限 (根据 API Server 承载能力调整)
-Dorg.csanchez.jenkins.plugins.kubernetes.clients.Qps=50
-Dorg.csanchez.jenkins.plugins.kubernetes.clients.Burst=100
# 优化 JNLP Ping 机制:2分钟 Ping 一次,超时时间设为 1 分钟 (默认 4 分钟太迟钝)
-Dhudson.remoting.PingThread.pingIntervalSecs=120
-Dhudson.remoting.PingThread.pingTimeoutSecs=60
# 优化 GC:大内存下启用 G1GC 并开启字符串去重 (缓解 CPS 转换导致的字符串常量泛滥)
-XX:+UseG1GC -XX:+UseStringDeduplication -Xms8g -Xmx8g
调优 3:Pipeline 共享库死锁的防御拦截
针对耗时的 JSON 解析和复杂的集合遍历,强制在共享库代码层面引入 @NonCPS 注解,将计算任务剥离出 Jenkins Master 的状态机保存机制,交由原生 JVM 栈执行:
import groovy.json.JsonSlurper
import com.cloudbees.groovy.cps.NonCPS
// 错误示范:在 CPS 块中解析大 JSON,极易导致 Master OOM 或 CPU 100%
// def parseJson(String text) { return new JsonSlurper().parseText(text) }
// 正确实战:防御性声明,计算完毕后直接返回结果,不保留中间状态
@NonCPS
def parseJsonFast(String text) {
def slurper = new JsonSlurper()
return slurper.parseText(text)
}
常见问题 (FAQ)
Q1:Pipeline 卡在 “Waiting for next available executor”,但 K8S 集群明明有充足的 CPU/Memory 资源?
A: 检查 Jenkins Master 是否达到了 containerCap 上限(默认 100)。即使集群有资源,Jenkins Kubernetes 插件也会拒绝发起新的 Pod 创建请求。另外,确认 Agent 模板中的 label 是否与 Pipeline 中声明的一致,拼写错误会导致无限期等待。
Q2:通过 JCasC 更新了共享库 (Shared Library) 的分支,为什么重新构建时没有立刻生效?
A: Jenkins 针对 Shared Library 默认开启了基于 Workspace 的缓存机制。如果在短时间内连续触发构建,可能会复用上一次 clone 的旧版本代码。可以在共享库配置中勾选 Include @Library changes in job recent changes 或在 JCasC 中显式关闭库的深度缓存(调整 retriever 的 timeout 策略),同时确认 Jenkins 服务器本地时间与 Git 仓库时间没有出现钟摆漂移。
Q3:Pipeline 运行中抛出 java.io.NotSerializableException: java.util.regex.Matcher 报错,如何排查?
A: 这是极其典型的 CPS 污染问题。Jenkins Pipeline 遇到 sh、sleep 等步骤时,会将当前所有的局部变量序列化保存到磁盘。如果在上述步骤前定义了不可序列化的对象(如 Regex Matcher、Socket 连接、I/O 流),序列化就会崩溃。
解法: 将对 Matcher 的操作封装到一个使用 @NonCPS 修饰的函数中执行,或者在使用完该对象后立即将其设为 null,确保其在跨越 Node/Agent 边界或进入挂起状态前被抛弃。