分类: DevOps/SRE

  • 深入 Jenkins 动态 Agent 调度延迟:K8S Pod 启动风暴引发的 JNLP 连接超时与 Master 线程耗尽排查实战

    高并发 CI/CD 场景下,Jenkins K8S 动态 Agent 极易因 Pod 启动风暴引发雪崩。本文核心结论:当并发构建量突增时,基于传统的 TCP 50000 端口进行 JNLP 通信会导致大量半连接和路由超时;通过将 Remoting 协议切换为 WebSocket,并调优 fabric8 客户端并发数与 K8S Cloud 的 containerCap,可彻底根治 Agent 频繁掉线与 Master 线程耗尽问题。

    故障现场:Agent 陷入“创建-离线-销毁”的死循环

    某次核心业务线进行大版本多分支并发验证,短时间内触发了超过 300 个 Pipeline 构建任务。监控大盘显示,Jenkins Master(版本 2.426.3-lts)的 Load Average 瞬间飙升至 40+,大量构建任务处于 Pending 状态。

    观察 K8S 集群发现,Kubernetes Plugin 确实在疯狂下发 Pod 创建请求,但现象极为诡异:

    1. Pod 能够被 K8S 调度并启动,进入 Running 状态。

    2. Pod 内的 jnlp 容器存活约 100 秒后,打印 Terminated 异常并自动退出。

    3. Jenkins Master 认为 Agent 离线,再次向 K8S 申请新建 Pod。

    4. 整个集群陷入了毫无意义的资源消耗死循环,API Server QPS 异常突增。

    提取出错 Agent Pod 内 jnlp 容器的日志:

    INFO: Locating server among [http://jenkins-master.cicd.svc.cluster.local:8080/]
    INFO: Trying protocol: JNLP4-connect
    WARNING: Could not connect to jenkins-master.cicd.svc.cluster.local:50000
    java.net.ConnectException: Connection timed out (Connection timed out)
        at java.base/sun.nio.ch.Net.connect0(Native Method)
        at hudson.remoting.Engine.connect(Engine.java:544)
        at hudson.remoting.Engine.innerRun(Engine.java:375)
    

    深度追踪:为什么 K8S Agent 能够正常拉起,却始终无法完成 JNLP 注册?

    从日志来看,这是一个典型的网络连通性报错,但问题并没有那么表面。Jenkins 的 Master-Agent 架构依赖 Remoting 协议,其传统的握手流程如下:

    1. Agent 启动时,通过 HTTP(S) 请求 Master 的 TCP port API,获取 JNLP 加密凭证(Secret)和专用的 TCP 通信端口(默认 50000)。

    2. Agent 与 Master 的 50000 端口建立长连接,维持心跳并接收 Pipeline 执行指令。

    1. 传统 TCP 50000 端口的架构缺陷

    在 K8S 环境中,Master 通常隐藏在 Ingress 或 Service 之后。如果仅仅暴露 HTTP 8080 端口,而没有在 Ingress 上透传 50000 端口的 TCP 流(需配置 Ingress Nginx 的 tcp-services ConfigMap),Agent 在第二步就会直接被拒绝。

    即便 Service 层开放了 50000 端口,当数百个 Agent 同时发起 TCP 握手时,若底层网络 CNI 插件(如 Calico 或 Cilium)遇到 iptables/eBPF 规则更新延迟,也会导致 SYN 报文被 Drop,进而引发 Connection timed out

    2. Jenkins Master 线程池耗尽

    排查过程中,直接在 Jenkins Master 宿主机抓取 jstack,发现大量 Jetty HTTP 线程处于 BLOCKED 状态:

    "qtp12345678-100" prio=10 tid=0x00007f8a1c000000 nid=0x1a2b waiting for monitor entry [0x00007f8a11234000]
       java.lang.Thread.State: BLOCKED (on object monitor)
        at org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud.provision(KubernetesCloud.java:650)
        - waiting to lock <0x00000007a1b2c3d0> (a java.lang.Object)
        at hudson.model.NodeProvisioner.update(NodeProvisioner.java:310)
    

    Kubernetes Plugin(版本 4136.vca_b_3203a_5103)底层使用 fabric8 K8S 客户端。默认情况下,fabric8 的 HTTP 客户端(OkHttp)对同一 Host 的并发连接数有严格限制。当并发创建 Pod 请求积压时,不仅阻塞了 Jenkins NodeProvisioner 的调度线程,更拖垮了 Master 响应 Agent HTTP JNLP 请求的能力,导致即使网络是通的,Agent 也因 Master 响应超时而注册失败。

    防御性架构重构与 JCasC 落地

    要从根本上解决高并发下的 Agent 调度雪崩,必须切断对独立 TCP 端口的依赖,并对 K8S Plugin 进行限流防爆。

    1. 抛弃独立 TCP 端口,全面启用 WebSocket

    Jenkins 2.222+ 已原生支持通过 WebSocket 传输 Remoting 协议。启用后,Agent 的通信将直接复用 HTTP(S) 的 8080/443 端口,无需额外配置 TCP 转发,完美穿透 Ingress 与负载均衡器,且极大降低了网络组件的连接跟踪(Conntrack)压力。

    2. JCasC (Jenkins Configuration as Code) 最佳实践

    通过 JCasC 固化 Kubernetes Cloud 的防御性配置。以下为排查后的标准配置片段,重点关注 webSocket 与容量控制参数:

    jenkins:
      clouds:
        - kubernetes:
            name: "k8s-cluster"
            serverUrl: "https://kubernetes.default"
            # 强制启用 WebSocket 复用 HTTP 端口
            webSocket: true 
            # Master 并发创建 Agent 的上限,避免 API Server 与 fabric8 线程池被击穿
            containerCapStr: "100" 
            # 连接超时与读取超时调优
            connectTimeout: 5
            readTimeout: 15
            templates:
              - name: "base-agent"
                namespace: "jenkins-agents"
                label: "k8s-agent"
                # 故障排查关键:任务失败后保留 Pod 10分钟,以便抓取现场日志
                podRetention: "OnFailure" 
                containers:
                  - name: "jnlp"
                    image: "jenkins/inbound-agent:3148.v532a_7e715ee3-1"
                    workingDir: "/home/jenkins/agent"
                    resourceRequestCpu: "500m"
                    resourceLimitCpu: "1000m"
                    resourceRequestMemory: "512Mi"
                    resourceLimitMemory: "1024Mi"
    

    3. JVM 与底层客户端参数调优

    为了防止 fabric8 客户端在极端并发下卡死,需要在 Jenkins Master 的启动参数(JAVA_OPTS)中注入以下调优指令,突破 OkHttp 的并发瓶颈:

    # 提升 Kubernetes Client 对单个后端(API Server)的并发连接数限制
    -Dkubernetes.client.maxConcurrentRequests=200
    -Dkubernetes.client.maxConcurrentRequestsPerHost=100
    # 禁用 Jenkins 旧版 Remoting 协议,减少安全面攻击和不必要的协议回退
    -Djenkins.slaves.JnlpSlaveAgentProtocol3.enabled=false
    -Djenkins.slaves.JnlpSlaveAgentProtocol4.enabled=true
    

    常见问题 (FAQ)

    Q1:Pipeline 执行时频繁报 NotSerializableException,如何解决? 这是由于 Jenkins 的 CPS(Continuation Passing Style)引擎在持久化 Pipeline 状态时,遇到了无法序列化的 Java 对象(如 java.util.regex.Matcher、数据库 Connection、或是非序列化的自定义类)。 解决: 永远不要在 nodestage 闭包跨越处传递这类对象;如果必须在代码块中使用复杂逻辑,请将该逻辑抽取为独立函数,并打上 @NonCPS 注解,让其在标准 JVM 堆栈中执行,而非被 CPS 引擎拦截。

    Q2:更新了 Jenkins Shared Library 的代码,但在已缓存的 Job 中不生效,必须重启 Jenkins 吗? 不需要。如果是隐式加载(Global Shared Libraries),Jenkins 默认会开启基于分支/标签的缓存。如果在 JCasC 中配置了 Library,务必检查 implicit: truedefaultVersion: "master" 的设置。如果是通过 @Library('[email protected]') _ 显式加载,建议采用基于 Git Tag 或 Commit Hash 的不可变版本号,而不是依赖分支名(如 master),以彻底规避 Classloader 缓存未刷新的问题。

    Q3:通过 JCasC 动态 Reload 配置时,会导致正在运行的 Pipeline 中断吗? 绝大多数配置(如 Views, Jobs 模板, Cloud 设置)的 Reload 是平滑的。但如果你在 JCasC 中修改了 securityRealm(安全域认证机制)或 authorizationStrategy,Jenkins 会销毁当前所有的安全上下文,这会直接导致正在执行的 Remoting Channel 被强行终止,引发 Agent 断联和任务报错。强规则: 绝对禁止在有核心业务构建运行时热重载安全相关配置。