某次排查过程中,核心业务线的 CI/CD 流水线彻底瘫痪,Jenkins 任务队列(Queue)积压突破 500。与此同时,底层 Kubernetes 集群告警群炸锅,API Server 出现严重的请求限流(Throttling),P99 延迟飙升至 3 秒以上。
最终排查结论:架构团队在做 Jenkins 迁移与高可用改造时,仅配置了 Layer 7 的 Ingress 规则,却遗漏了 Jenkins Remoting 通信依赖的 Layer 4 TCP(50000)端口。导致 K8S 动态 Agent Pod 启动后无法与 Master 建立 JNLP 连接。Jenkins Kubernetes 插件因此陷入了致命的“申请 Pod -> Agent 注册超时 -> 销毁 Pod -> 无限重试”死循环,硬生生把集群 API Server 给打穿了。
把 Jenkins 当成一个普通的无状态 Web 服务去搞云原生改造,而不去深究其底层 Master-Agent 的心跳与通信模型,这种粗暴的操作在生产环境中是极其致命的。
案发现场:失控的调度器与死亡循环
接到报障后,第一时间登录集群查看资源状态。终端里的现象令人窒息:
$ kubectl get pods -n jenkins | grep jnlp-agent | wc -l
842
$ kubectl get pods -n jenkins | grep jnlp-agent | head -n 5
jnlp-agent-8f73b-5x9qp 0/1 ContainerCreating 0 12s
jnlp-agent-8f73b-9m2kx 1/1 Terminating 0 1m45s
jnlp-agent-8f73b-p2v1l 0/1 ContainerCreating 0 8s
jnlp-agent-8f73b-x8c4d 1/1 Terminating 0 1m45s
数百个 Agent Pod 处于 ContainerCreating 或 Terminating 状态。再去查看 Jenkins Master 的系统日志,满屏都是类似下面的报错:
INFO: Kubernetes pod jnlp-agent-8f73b-9m2kx started
WARNING: Failed to connect to agent jnlp-agent-8f73b-9m2kx within 100 seconds.
INFO: Terminating node jnlp-agent-8f73b-9m2kx
INFO: Queue task #4023 still pending, provisioning a new agent...
转头查看其中一个 Agent Pod 的内部日志,终于抓到了真凶:
INFO: Locating server among [https://jenkins.company.com/]
WARNING: Failed to connect to https://jenkins.company.com/tcpSlaveAgentListener/: Connection refused
java.net.ConnectException: Connection refused
at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
...
INFO: Retrying in 10 seconds
深度剖析:为什么缺少一个端口会导致雪崩?
要理解这个故障,必须理清 Jenkins Kubernetes Plugin 的工作状态机。这绝不只是一个“网络不通”的简单 Bug,而是一个典型的分布式状态机不同步导致的雪崩。
-
Remoting 协议的固执:Jenkins Master 与 Agent 之间的通信基于 Jenkins Remoting 协议,这是一个重度依赖序列化与长连接的 Java 二进制协议。默认情况下,Agent 启动后,会先通过 HTTP(S) 请求 Master 的主入口,获取
X-Jenkins-CLI-Port或相关 TCP 端口信息(通常是 50000),随后尝试建立直连 TCP 通道。 -
L7 Ingress 的拦截:改造期间,Jenkins Master 被放到了 Nginx Ingress 后端。Ingress 默认只处理 HTTP/HTTPS 协议(L7)。当 Agent 尝试向
jenkins.company.com:50000建立 TCP 握手时,流量直接在网关层被丢弃或拒绝。 -
致命的机制错位(State Mismatch):
- K8S 视角:Pod 已经成功拉起,容器状态是
Running,K8S 认为任务完成。 - Jenkins 视角:向 K8S 发送了 Pod 创建请求,且等待 Agent 进程发起 JNLP 注册回调。
- 死循环触发:等待 100 秒后(默认超时时间),Jenkins Master 依然没收到 Agent 的 JNLP 注册心跳。它不仅不会认为是自己的网络配置问题,反而会固执地判定:“这个 Pod 死掉了,为了满足队列里等待的构建任务,我必须销毁它,并向 K8S 申请一个新的 Pod。”
当并发构建任务达到 50 个,每个任务都在触发这种“申请 -> 等待 -> 销毁 -> 再申请”的循环时,K8S 的 kube-apiserver 就成了重灾区。大量的 POST /api/v1/namespaces/jenkins/pods 和 DELETE 请求瞬间填满了 API Server 的队列,触发限流,进而影响整个集群内其他核心业务 Pod 的调度与扩缩容。
解决方案与防御性配置
针对此类问题,修复网络通信只是第一步,更重要的是在架构层面加上防御性兜底限制。
1. 拥抱 WebSocket,抛弃底层 TCP 直连
既然 L4 暴露配置繁琐且容易在各种负载均衡器上踩坑,最优雅的做法是直接让 JNLP 流量复用 HTTP(S) 的 L7 通道。从 Jenkins 2.217 开始,Remoting 已经原生支持 WebSocket。
在 JCasC (Jenkins Configuration as Code) 的配置中,必须在 K8S Cloud 配置项里显式开启 webSocket: true。
jenkins:
clouds:
- kubernetes:
name: "kubernetes"
# 直接走集群内部 DNS 通信,绕过外部 Ingress,降低网络开销与故障点
serverUrl: "https://kubernetes.default"
namespace: "jenkins-agents"
jenkinsUrl: "http://jenkins-master.jenkins.svc.cluster.local:8080"
# 开启 WebSocket,彻底解决 TCP 50000 端口穿透问题
webSocket: true
# 【防御性编程核心】设置全局容量上限,哪怕死循环也不会打穿 API Server
containerCapStr: "100"
2. 配置 Kubernetes Plugin 的防雪崩限制
永远不要假设外部系统会乖乖按预期工作。必须给 Jenkins 向 K8S 索要资源的行为加上硬性枷锁:
-
containerCapStr: 限制整个 K8S Cloud 并发存活的 Agent 总数。 -
在每个
podTemplate级别设置instanceCap:防止单一异常的 Pipeline 把所有集群资源耗尽。
3. 剥离通信链路(Cluster Internal Routing)
如果你只是在同一个 K8S 集群内部署 Jenkins Master 和调度 Agent,Agent 连接 Master 绝对不应该 绕一圈跑到外网 Ingress 再进来。不仅增加延迟,还多引入了一层网络设备的故障风险。
强制在 jenkinsUrl 中使用 K8S 内部的 FQDN:http://。
排查清单与同类问题速查
如果你也遇到了 Jenkins Agent 疯狂重启或一直在 Pending/Terminating 之间横跳,请核对以下清单:
-
排查 JNLP 握手阻断:查看 Agent Pod 的日志。如果出现
Connection refused或Connection timed out,且指向 Master 的 50000 端口,立刻检查安全组、网络策略 (NetworkPolicy) 或 LoadBalancer 的 L4 暴露情况,或者直接开启 WebSocket。 -
检查 Jenkins Master URL 配置:如果
Manage Jenkins -> System -> Jenkins URL配置错误,Agent 会拿到一个无法解析的地址。在 K8S 环境下,尽量在 Cloud 配置的jenkinsUrl中覆盖并强制指定 ClusterIP 或内部 DNS。 -
监控 ContainerCap 触顶情况:如果在 Jenkins 侧看到任务一直卡在
‘Jenkins’ doesn’t have label ‘xxx’或者Waiting for next available executor,但没有看到新 Pod 创建,检查系统日志确认是否触发了containerCap上限。 -
防御性兜底检查:确认有没有恶意的 Groovy 脚本在无限触发重试。检查 Pipeline 里的
retry()块逻辑是否包含了环境构建阶段,避免因业务代码逻辑错误引发基础设施级别的 Ddos 攻击。