标签: Groovy

  • 深入 Jenkins 陷阱排查:共享库全局变量滥用引发的并行构建串量与生产发布雪崩实战

    近期处理了一起极其荒谬且破坏力极强的 P0 级生产事故。核心业务团队在做 Monorepo(单体代码仓库)微服务大版本发布时,15 个微服务通过 Jenkins Pipeline parallel 并发构建部署,结果上线后核心链路全面崩溃,Nginx Ingress 瞬间爆出海量 502。

    排查发现,根本不是 K8s 调度问题或 Harbor 镜像源拉取故障,而是 订单服务(Order)的 Pod 里,居然跑着用户服务(User)的镜像。最终定位的罪魁祸首:Jenkins Shared Library(共享库)中滥用了脚本级全局变量,在 parallel 并发执行下触发了典型的线程安全问题,导致变量相互覆盖(串量)。

    一句话结论:在 Jenkins vars/ 共享库脚本中定义方法外部的全局变量,其实例在同一个 Pipeline 运行期的 parallel 阶段是完全共享的。并发读写这些变量必然导致数据污染。修复方案:必须将所有状态变量严格限定在 call() 方法内部,强制实现共享库的无状态化。

    事故现场与离谱的报错

    排查过程中,我们查看了 K8s 集群的实时状况。订单服务(Order)的 Pod 陷入了 CrashLoopBackOff:

    $ kubectl logs order-deployment-5f6b9c9d-8x2a -n prod-ns
    Error: Invalid or missing Spring Boot configuration. 
    Application 'user-service' failed to start.
    

    日志显示这个 Pod 内部启动的居然是 user-service。去检视 Jenkins 构建日志,发现了令人窒息的一幕:

    [Pipeline] // 并行分支: build-order-service
    [Pipeline] sh (docker build)
    + docker build -t harbor.corp.com/prod/order-service:v1.2.3-45 .
    ...
    [Pipeline] // 并行分支: build-user-service
    [Pipeline] sh (docker build)
    + docker build -t harbor.corp.com/prod/user-service:v1.2.3-45 .
    ...
    [Pipeline] sh (helm upgrade) # 此时 order 分支的 helm 部署命令
    + helm upgrade order-service ./chart --set image=harbor.corp.com/prod/user-service:v1.2.3-45
    

    注意看最后一行,order-service 的分支在执行 Helm 部署时,注入的 Image 参数竟然变成了 user-service 的镜像!

    原理剖析:全局变量是万恶之源

    去翻看负责 CI/CD 脚本的开发人员写的 Jenkins Shared Library 代码,简直触目惊心:

    // 错误代码示例:vars/standardBuilder.groovy
    def currentImage = ""       // 【致命错误】脚本级全局变量
    def deployYaml = ""         // 【致命错误】脚本级全局变量
    
    def call(Map config) {
        currentImage = "${config.imageName}:${config.tag}"
        deployYaml = "${config.appName}-deployment.yaml"
    
        // 模拟耗时构建操作
        sh "docker build -t ${currentImage} ."
        sh "docker push ${currentImage}"
    
        // 部署阶段
        sh "helm upgrade ${config.appName} ./chart --set image=${currentImage}"
    }
    

    而在他们的 Jenkinsfile 中,调用方式是这样的:

    parallel(
        'order': { standardBuilder(appName: 'order', imageName: 'order-service', tag: 'v1.2') },
        'user':  { standardBuilder(appName: 'user',  imageName: 'user-service',  tag: 'v1.2') }
    )
    

    为什么这绝对不可原谅?

    很多刚接触 Jenkins Pipeline 的人把它当成简单的 Bash 脚本写,缺乏基本的“防御性编程”思维。在 Jenkins CPS(Continuation Passing Style)引擎的底层实现中,vars/standardBuilder.groovy 并不是每次调用都重新实例化的纯函数。 整个文件在编译后会成为继承自 CpsScript 的一个类。在同一个 Pipeline Run 中,vars/ 下的单例对象是被缓存的,文件顶层的 def currentImage 变成了这个单例对象的一个实例属性(Field)。

    当 parallel 启动多个分支(在 Jenkins 中是底层的绿色线程 Green Threads)并发调用 standardBuilder() 时,执行时序就变成了:

    1. 分支 A (Order):写入 currentImage = "order-service:v1.2"

    2. 分支 B (User):写入 currentImage = "user-service:v1.2" (此时实例属性被覆盖)

    3. 分支 A (Order):执行完 docker push,准备执行 helm upgrade,读取 currentImage,读到了 "user-service:v1.2"。

    这不仅导致了严重的生产环境发版错乱,如果是带有危险操作(如 DB 变更脚本)的流水线,这种串量能直接把数据库干废。

    进阶踩坑:CPS 序列化风暴与 OOM

    除了上述的并发污染问题,在脚本级定义变量还会引发另一个经典灾难:NotSerializableException 报错与 Jenkins Master 内存泄漏。

    如果有人在这个全局作用域放了一些非序列化对象(比如 JSON 解析结果、HTTP Client 实例):

    import groovy.json.JsonSlurper
    
    def parsedConfig = null // 脚本级变量
    
    def call() {
        def slurper = new JsonSlurper() // JsonSlurper 返回的是非序列化的 LazyMap
        parsedConfig = slurper.parseText(readFile('config.json'))
        sh "echo 'Building...'"
    }
    

    Jenkins 的 CPS 引擎为了保证在 Master 节点重启后能够恢复 Pipeline 的执行状态,会在遇到每一个 Pipeline Steps(如 sh, echo, sleep)时,暂停当前线程,并将整个 CpsScript 对象及其绑定的变量树完整序列化并落盘(存储到 build.xml 中)。 如果 parsedConfig 是一个不可序列化的对象,流水线会当场 Crash。如果是一个体积庞大的 Map,并发数一高,每次执行 sh 都要把几 MB 的数据做深拷贝和 XML 序列化,会导致 Jenkins Master 的 CPU 瞬间打满,触发频繁的 GC 停顿甚至 OOM(OutOfMemoryError)。

    最佳实践与防御性加固

    对于重度依赖 Jenkins 驱动核心 CI/CD 的团队,必须在架构层面掐断这种低级失误。

    1. 强制无状态化共享库(Pure Functions)

    所有共享库 vars/ 下的方法,必须是纯函数,严禁在 call() 之外定义任何非 final 变量。 修正后的代码:

    // 正确代码:vars/standardBuilder.groovy
    def call(Map config) {
        // 变量作用域严格限制在方法内部(闭包内本地变量),每次调用独立分配栈空间
        def currentImage = "${config.imageName}:${config.tag}"
        def deployYaml = "${config.appName}-deployment.yaml"
    
        sh "docker build -t ${currentImage} ."
        sh "docker push ${currentImage}"
        sh "helm upgrade ${config.appName} ./chart --set image=${currentImage}"
    }
    

    2. IO 操作与数据解析严格剥离(@NonCPS)

    如果必须进行复杂的 JSON 解析或遍历操作,必须封装为独立的函数并打上 @NonCPS 注解,确保这部分逻辑由 JVM 原生线程直接执行,不参与 CPS 的序列化。 注意: @NonCPS 方法内部绝对不能调用任何 Jenkins Pipeline 原生步骤(如 sh, readFile)。

    @NonCPS
    def parseMyConfig(String jsonStr) {
        // 纯 Groovy/Java 原生执行,无需考虑序列化问题
        return new groovy.json.JsonSlurperClassic().parseText(jsonStr) 
    }
    
    def call() {
        def jsonContent = readFile('config.json')
        def configMap = parseMyConfig(jsonContent)
        // 继续后续流程...
    }
    

    3. K8s Dynamic Agent 的工作空间硬隔离

    除了内存里的变量,物理层面的 Workspace 同样容易发生并发冲突。在 parallel 执行阶段,尽量确保每个并行任务申请独立的 K8s Pod (Agent),或者显式使用 dir 指令分配子目录,避免同一节点上的 I/O 踩踏:

    parallel(
        'order': { 
            node('k8s-agent') { // 分配独立的 Pod
                checkout scm
                standardBuilder(...)
            }
        }
    )
    

    同类问题速查(排查清单)

    1. Pipeline 并行分支行为异常/串数据:立刻检查 vars/ 目录下的共享库文件,全局搜索是否存在未被包含在方法内的 def、String、Map、List 定义,或误用了 @Field。

    2. 构建日志报 java.io.NotSerializableException:检查脚本中是否将 Matcher(正则匹配对象)、JsonSlurper 返回结果、Connection 等非序列化对象保留在了方法外层,或跨越了 sh/echo 等 Pipeline Step 的边界。

    3. Jenkins Master CPU 极高或 Full GC 频发:检查 Jenkins 系统管理中的线程转储(Thread Dump),搜索 CpsThread 和 xstream 相关的堆栈,大概率是 Pipeline 定义了超大数组/Map并在执行 Steps 时引发了海量序列化开销。

    4. 并发构建文件损坏/拉错代码:检查 parallel 代码块,是否多个并发逻辑复用了同一个 node 并且没有使用 dir('sub-dir') 隔离 Workspace。

  • 深入 Jenkins 陷阱排查:Shared Library 隐式类加载泄漏引发的 Metaspace OOM 与 K8S Agent 孤儿 Pod 实战

    Jenkins Controller 频繁发生 Metaspace OOM 且伴随 K8s 出现大量孤儿 Agent Pod,根因往往是 Shared Library 中非标准化 Groovy 脚本引发 GroovyClassLoader 内存泄漏,导致 Remoting 线程卡死。修复方案:精简共享库闭包逻辑,限制 -XX:MaxMetaspaceSize=2G,并在 K8s Pod Template 中强制配置 activeDeadlineSeconds 进行防御性兜底。

    近期在排查一个日均承载数万次 Pipeline 构建的核心 Jenkins 集群(版本 2.440.1,Kubernetes Plugin 4136.v40097782b,JDK 17)时,遇到一个极其顽固的级联故障。

    监控大盘显示,Jenkins Controller 的 P99 响应时间周期性飙升至 30 秒以上,Load Average 突破 50。更棘手的是,K8s 集群中遗留了数百个状态为 Running 的 JNLP Agent Pod,但 Jenkins 控制台却显示这些节点处于 Offline 状态。典型的“脑裂”现场。

    现场取证与链路溯源

    登录 Controller 宿主机,首先抓取进程指标。通过 jstat 观察 JVM 内存分布,直接锁定了肇事者:

    $ jstat -gcutil $(pgrep -f jenkins.war) 1000 5
      S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
      0.00   0.00   2.34  89.45  99.99  98.12   4123  124.321   145  432.145  556.466
      0.00   0.00   1.21  89.45  99.99  98.12   4123  124.321   146  435.210  559.531
    

    注意 M(Metaspace)这一列,使用率死死卡在 99.99%,FGC(Full GC)次数在短时间内疯狂上涨,且单次 Full GC 耗时极长。Controller 已经陷入了 Full GC 死亡螺旋。

    查看 Jenkins 的 catalina.out 或系统日志,证实了推断: java.lang.OutOfMemoryError: Metaspace

    为什么 Metaspace OOM 会导致 K8s 孤儿 Pod 泛滥?

    Jenkins 依赖 TCP/WebSocket 上的 Remoting 协议与 K8s Agent 里的 agent.jar 通信。当 Controller 陷入持续的 Full GC 停顿时,所有的工作线程(包括负责心跳检测的 Channel Reader/Writer 线程)全部被挂起。

    1. JNLP 心跳超时,Controller 单方面标记 Agent 为 Offline。

    2. 由于 Controller 处于假死状态,Kubernetes Plugin 无法及时向 K8s APIServer 发出 DELETE Pod 的请求。

    3. K8s 侧的 Pod 因为没有设置超时机制,其内部的 agent.jar 陷入无限等待,最终导致集群计算资源被僵尸 Pod 占满,引发调度雪崩。

    为什么 Shared Library 的高频调用会引发 Metaspace 膨胀?

    Jenkins Pipeline 的底层核心是 Groovy CPS(Continuation Passing Style)引擎。在处理 Shared Library 时,Jenkins 使用 CpsGroovyShell 将 Groovy 代码编译为 Java 字节码。

    通常情况下,JVM 会在类加载器失去所有引用时回收其加载的类(Class Unloading)。但在 Jenkins 的复杂环境里,容易踩到以下暗坑:

    1. AST 转换与动态生成类:如果在 Shared Library 中重度使用闭包(Closures)、动态方法拦截(methodMissing)或隐式类型转换,Groovy 编译器会在运行时生成大量的匿名内部类(例如 CallSiteArray)。

    2. CPS 序列化链条泄漏:Jenkins 会在每次 Pipeline 暂停(如执行 sh、sleep 等 block 步骤)时,将整个执行上下文序列化到磁盘(program.dat)。如果你的 Groovy 脚本在全局作用域持有大对象引用,或者通过 @Field 定义了静态缓存,不仅会导致序列化文件暴增,还会导致对应的 GroovyClassLoader 实例被 CPS 引擎死死抓住,无法被 GC Roots 释放。

    每一次新的 Pipeline 构建,都会触发新的类加载。随着构建量增加,Metaspace 被这些永远无法卸载的“一次性”类塞满,最终引爆 OOM。

    架构级防御与修复方案

    运维和架构的职责不仅是找到 Bug,更要在系统层面构建防御纵深。面对这类问题,需要从 JVM、配置管理(JCasC)和流水线规范三个维度进行整改。

    1. JVM 边界隔离与调优

    绝对不能让 JVM 的 Metaspace 无限增长(JDK 8+ 默认 MaxMetaspaceSize 是无上限的)。必须给 Controller 划定严格的物理边界,并开启 G1 的类卸载特性。

    修改 Jenkins 启动参数(针对 JDK 17):

    JAVA_OPTS="-server \
      -Xms8G -Xmx8G \
      -XX:MetaspaceSize=1024m \
      -XX:MaxMetaspaceSize=2048m \
      -XX:+UseG1GC \
      -XX:MaxGCPauseMillis=200 \
      -XX:+UnlockExperimentalVMOptions \
      -XX:+ParallelRefProcEnabled \
      -Xlog:gc*=info,gc+metaspace=trace:file=/var/log/jenkins/gc.log:time,uptime,level,tags:filecount=10,filesize=50M \
      -Dgroovy.use.classvalue=true" 
    

    注:groovy.use.classvalue=true 是针对 Groovy 2.4+ 的一个关键开关,能有效缓解内存泄漏,但在某些旧版 Jenkins 插件中可能引发兼容性问题,需在预发环境验证。

    2. K8s Pod Template 兜底机制(JCasC 防御性配置)

    绝对不要信任外部系统的状态同步。在通过 JCasC 定义 Kubernetes Pod Template 时,必须加入 activeDeadlineSeconds 参数。这样即使 Jenkins Controller 完全宕机,K8s 也会在超时后由 Kubelet 亲手超度这些孤儿 Pod。

    核心 jenkins.yaml JCasC 片段提取:

    jenkins:
      clouds:
        - kubernetes:
            name: "kubernetes"
            serverUrl: "https://kubernetes.default"
            namespace: "jenkins-agents"
            jenkinsUrl: "http://jenkins-master.jenkins.svc.cluster.local:8080"
            templates:
              - name: "golang-builder"
                label: "golang"
                nodeUsageMode: EXCLUSIVE
                # 核心防御1:无论构建成功与否,强制清理 Pod
                podRetention: "never"
                # 核心防御2:Pod 在 K8s 侧的绝对存活上限(例如 7200 秒/2小时)
                activeDeadlineSeconds: 7200
                # 核心防御3:如果 Controller 失去心跳,Agent 主动退出
                idleMinutes: 10
                containers:
                  - name: "jnlp"
                    image: "jenkins/inbound-agent:3192.v713e3b_039fb_e-4"
                    args: "^${computer.jnlpmac} ^${computer.name}"
                    envVars:
                      - envVar:
                          key: "JENKINS_AGENT_WORKDIR"
                          value: "/home/jenkins/agent"
    

    3. Pipeline 代码降维打击

    Jenkins Pipeline 的定位是“调度器”,而不是“计算引擎”。 强制推行规范:严禁在 Shared Library 中编写复杂的纯 Groovy 数据处理逻辑(如大规模 JSON 解析、复杂的集合遍历、正则轮询)。 反模式:在 Groovy 中使用 JsonSlurper 解析 10MB 的依赖分析报告。 最佳实践:使用 sh 步骤调用预置在容器镜像里的 Python/Go 脚本进行解析,仅通过标准输出(stdout)将极简的结果(如 Exit Code 或一个轻量级状态字符串)回传给 Pipeline 控制流。

    常见问题 (Q&A)

    Q1:K8s 动态 Agent Pod 经常在拉起时握手超时(JNLP connection timeout),但 Pod 本身是 Running 状态,如何排查? 通常有两个原因:一是 K8s APIServer 到 Jenkins Master 的网络存在高延迟或 Ingress 代理配置了不合理的 Timeout;二是 Controller 侧的 TCP Backlog 已满。建议在 JCasC 中开启 WebSocket 协议(websocket: true),替代传统的基于单独 TCP 端口的 JNLP 协议,复用 HTTP(S) 链路可穿透绝大多数网络屏障,并在 Ingress 侧配置 WebSocket 长连接支持。

    Q2:在使用 Shared Library 时,加上 @NonCPS 注解能解决内存泄漏吗? 不能直接解决 Metaspace 泄漏,但能解决序列化报错(NotSerializableException)。@NonCPS 告诉 Jenkins 引擎不要对该方法进行 CPS 转换和挂起状态记录。但必须极其小心:绝不能在 @NonCPS 方法内部调用任何 CPS 步骤(如 sh, echo, build),否则会破坏 CPS 状态机,导致 Pipeline 莫名其妙地卡死(Hang)在执行节点,且没有任何报错日志。

    Q3:通过 JCasC 动态重新加载配置(Reload Configuration)时,会中断正在运行的 K8s 动态 Agent 吗? 不会直接中断。JCasC 的 Reload 过程在内存中重建了 Cloud 和 PodTemplate 对象。已经调度并在运行中的 Pod 会维持与 Master 的 Remoting 连接继续执行完毕。但如果此时修改了 JCasC 中的 Jenkins URL 或 WebSocket 设置,依赖这些配置进行重连的离线 Agent 恢复机制可能会失效。

  • Jenkins 生产环境雪崩排查实战:Groovy CPS 陷阱引发的 Metaspace 溢出与 K8S Agent 调度风暴

    结论先行:Jenkins Pipeline 复杂的 Groovy 闭包会导致 CPS(Continuation Passing Style)频繁进行 AST 转换,耗尽 Master Metaspace 触发 OOM。同时,K8S 插件在 Master 假死断连时产生的 Agent 创建风暴,会瞬间击穿 K8S API Server。本文通过重构 Shared Library 剥离 CPS 逻辑,并引入 JCasC 固化 K8S 动态 Agent 限流配置,彻底解决百级别并发构建下的系统雪崩问题。

    1. 故障现场:Master 假死与 K8S API Server 告警

    排查过程中接到告警,CI/CD 集群 P99 构建排队时间从平时的 5 秒飙升至 30 分钟以上。登录控制台发现 Jenkins UI 响应极其缓慢,部分页面直接 502。 联动监控大盘,发现了两个极度异常的指标:

    1. Jenkins Master JVM:Metaspace 使用率在两小时内呈阶梯式上涨,直至 100% 触发 Full GC,单次 GC 停顿(STW)超过 12 秒。

    2. K8S 控制平面:API Server QPS 突增,尤其是针对 namespaces/jenkins/pods 的 POST 和 DELETE 请求,导致 API Server CPU 飙升,etcd 出现选主告警。

    进入 Jenkins Master 容器抓取现场:

    # 查看 JVM 内存状态
    jstat -gcutil $(pgrep java) 1000 5
      S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT    GCT   
      0.00 100.00  32.14  89.45  99.98  98.71  23412  145.312  425  312.411  457.723
    
    # 生成 Heap Dump 和 Thread Dump(保留案发现场)
    jcmd $(pgrep java) GC.heap_dump /tmp/jenkins_oom.hprof
    jcmd $(pgrep java) Thread.print > /tmp/jenkins_threads.txt
    

    日志中大量抛出 java.lang.OutOfMemoryError: Metaspace,同时伴随着 Kubernetes client: failed to create pod ... Read timeout。很明显,JVM 已经处于频繁 GC 的濒死状态。

    2. 为什么 Groovy CPS 机制会吃光 Master 的 Metaspace?

    把 Heap Dump 拖到 MAT(Memory Analyzer Tool)里分析,发现 ClassLoader 数量异常庞大,且绝大多数是由 com.cloudbees.groovy.cps.NonCPS 和 Pipeline 脚本动态生成的类。

    Jenkins Pipeline 的底层运行机制基于 CPS(Continuation Passing Style)。为了让 Pipeline 在 Jenkins Master 重启后还能从断点恢复继续执行,Jenkins 必须能够将当前执行的堆栈状态序列化到磁盘。 这就导致了一个致命陷阱:你在 Jenkinsfile 里写的每一行看似普通的 Groovy 代码,都会被 CPS 转换引擎解析重写为可以被序列化的 AST(抽象语法树)对象。

    在某次业务线提交的 Shared Library 中,发现了一段类似这样的代码:

    // 反面教材:在 CPS 方法中进行大量不可序列化对象的循环操作
    def processComplexJson(String jsonStr) {
        def jsonSlurper = new groovy.json.JsonSlurperClassic()
        def data = jsonSlurper.parseText(jsonStr)
        // 这里的 data 树结构非常复杂,且在循环中调用了 pipeline step
        data.items.each { item ->
            if (item.name.matches(".*-service-.*")) { // 正则 Matcher 不可序列化
                echo "Processing ${item.name}"       // 调用了 CPS 步骤
                // 复杂的处理逻辑...
            }
        }
    }
    

    原理解析:

    1. JsonSlurper 解析出的复杂对象模型、java.util.regex.Matcher 等对象是不可序列化的。

    2. 当闭包 .each {} 内部混合调用了 Pipeline 原生 step(如 echo, sh)时,Jenkins 会尝试保存整个上下文。

    3. 每次执行构建,CPS 引擎为了处理这些无法直接解析的代码,会动态生成大量的匿名类加载到 Metaspace 中。由于这些类持有 Pipeline 的执行上下文(强引用),无法被 GC 快速回收。

    4. 并发一高,Metaspace 迅速被打爆。Master 发生长达十几秒的 STW。

    雪崩链条: Master STW -> JNLP Agent (运行在 K8S Pod 中) 的心跳超时 -> Jenkins 认为 Agent 已死,触发重连或重新分配 -> K8S Plugin 疯狂向 API Server 发起创建 Pod 请求 -> API Server 被打满 -> 旧 Agent 还在跑,新 Pod 不断创建 -> K8S 节点资源耗尽。

    3. 核心修复:Shared Library 与 K8S Agent 调优实践

    针对上述问题,我们从代码重构和配置加固两方面进行落地。当前环境为 Jenkins 2.414.3 LTS,Kubernetes Plugin 4136.v464303c7379d。

    3.1 剥离 CPS:使用 @NonCPS 与纯粹的 Java 类

    对于 Shared Library 中的数据处理逻辑,必须将纯粹的代码计算与Pipeline 执行步骤隔离开。使用 @NonCPS 注解,让 Jenkins 跳过 AST 转换,按标准 JVM 字节码执行。

    import com.cloudbees.groovy.cps.NonCPS
    import groovy.json.JsonSlurperClassic
    
    // 1. 将耗时的、涉及不可序列化对象的纯计算逻辑标记为 @NonCPS
    @NonCPS
    List<String> getServicesToProcess(String jsonStr) {
        def services = []
        def jsonSlurper = new JsonSlurperClassic()
        def data = jsonSlurper.parseText(jsonStr)
    
        for (item in data.items) {
            if (item.name.matches(".*-service-.*")) {
                services.add(item.name)
            }
        }
        return services // 只返回可序列化的基本类型或标准集合
    }
    
    // 2. 在 Pipeline 步骤中通过标准 for 循环调用(不要用 .each 闭包混合 pipeline step)
    def call(String jsonStr) {
        List<String> targetServices = getServicesToProcess(jsonStr)
        for (int i = 0; i < targetServices.size(); i++) {
            def svc = targetServices[i]
            echo "Processing ${svc}"
            // 执行实际的 pipeline steps...
        }
    }
    

    3.2 阻断雪崩:JCasC 固化 K8S Agent 限流配置

    为了防止 Jenkins 在网络抖动或自身 GC 时向 K8S 发起 API DDOS 攻击,必须严格配置 K8S Plugin 的容量上限,并改用 WebSocket 代替 TCP JNLP 端口直连。 我们通过 JCasC (Jenkins Configuration as Code) 强制注入以下安全配置:

    jenkins:
      clouds:
        - kubernetes:
            name: "kubernetes"
            serverUrl: "https://kubernetes.default"
            namespace: "jenkins"
            jenkinsUrl: "http://jenkins-master.jenkins.svc.cluster.local:8080"
            # 【核心防御】开启 WebSocket,复用 HTTP 端口,避免 K8S LoadBalancer 断流导致心跳丢失
            webSocket: true 
            # 【核心防御】限制全局并发 Pod 数,保护 K8S API Server 和节点资源
            containerCapStr: "200"
            # 限制 API 请求超时时间
            readTimeout: 15
            connectTimeout: 5
            maxRequestsPerHostStr: "32"
            templates:
              - name: "base-maven"
                namespace: "jenkins"
                label: "maven-agent"
                # 限制单种模板的最大并发数
                instanceCapStr: "50" 
                containers:
                  - name: "jnlp"
                    image: "jenkins/inbound-agent:3148.v532a_7e715ee3-1"
                    workingDir: "/home/jenkins/agent"
                    resourceRequestCpu: "500m"
                    resourceLimitCpu: "2"
                    resourceRequestMemory: "1Gi"
                    resourceLimitMemory: "2Gi"
    

    同时,调整 Jenkins Master 启动参数,增大 Metaspace 并限制其无序扩张: JAVA_OPTS="-Xms8G -Xmx8G -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=1G -XX:+UseG1GC"

    应用上述修复后,P99 排队时间回落至 3 秒,Master 内存泄漏彻底消除,API Server 平稳运行。

    4. 常见问题 (FAQ)

    Q1: K8S 动态 Agent 频繁出现 JNLP connection timeout 或 offline,是什么原因? 通常有两个原因:一是中间的 Ingress/LoadBalancer 对长连接(默认 50000 TCP 端口)有 idle timeout 清理机制,导致静默断连;二是 Master 的 CPU 或内存被跑满,无法及时响应心跳。 建议解决: 启用 Kubernetes 插件的 webSocket: true 选项,让 Agent 通过 标准的 HTTP 8080 端口使用 WebSocket 与 Master 通讯,这样不仅穿透性好,还能复用 HTTP 的负载均衡和 KeepAlive 策略。

    Q2: 在动态 K8S Agent 中构建 Docker 镜像,推荐 DinD (Docker in Docker) 还是 Kaniko? 坚决抵制在 K8S 生产环境中大规模使用 DinD。DinD 需要开启 Pod 的 privileged: true 特权模式,这在任何有底线的运维体系中都是不被允许的,极易引发容器逃逸。 建议解决: 使用 Google 提供的 Kaniko。它完全在用户态执行,无需特权,直接通过解析 Dockerfile 在容器内层层构建镜像文件系统,最后 push 到 Harbor。

    Q3: 如何安全地在 JCasC YAML 中管理集群密码和 Secret? 禁止在 JCasC 的 yaml 文件里明文写 Token! 建议解决: 利用 Jenkins 的 Secret 机制结合 K8S 环境变量。在 JCasC 中使用 ${MY_SECRET} 占位符,然后在 Jenkins Master 的 Deployment 中通过 K8S Secret 挂载到环境变量。启动时 JCasC 会自动将其替换,实现配置与凭据解耦。