在实际维护 Jenkins 流水线时,“重试”和“最多构建次数”经常被当成一回事,但其实两者作用在不同层面。如果你的目的是避免某次失败后无限重跑,那么要限制的是构建触发的次数,而不是单个步骤里循环多少次。下面分享一种可以落地的判断顺序:先区分重试范围,再决定用 Pipeline 内置 retry 还是外部脚本来卡次数。
先区分重试范围和构建次数
在Jenkins Pipeline中,retry步骤用于包裹可能失败的阶段,但retry只对当前阶段有效,如果整个流水线需要多阶段重试,需要将retry放在pipeline外层。常见误区是认为retry会重新调度整个构建,实际上它只是重新执行闭包内的步骤。要设置最多构建次数,需要结合构建触发器或外部脚本控制并发构建数。retry(n)中的n表示重试次数,加上第一次尝试,总共最多执行n+1次。若想限制总构建次数,可以在构建开始前检查当前构建号是否超过阈值,若超过则抛出异常停止构建。
这段话的意思是,先问自己:你要限制的是“同一个构建内的任务重跑”,还是“整个构建被重复调度”?如果是前者,retry(n) 足够。如果是后者,你必须把检查放在构建入口,否则 Jenkins 的触发器(比如轮询 SCM 或 Webhook)仍然会按事件启动新构建。
声明式与脚本式 Pipeline 的配置取舍
在声明式Pipeline中,可以使用options块设置retry,例如options { retry(3) },这会对整个流水线进行重试。如果使用脚本式Pipeline,可以用retry(n)包裹特定阶段。但需注意,重试会重新执行闭包内的所有步骤,如果闭包内包含长时间运行的任务,会增加整体耗时。建议根据任务的实际稳定性和耗时来调整重试次数,不要盲目设置过大值。同时,配合timeout选项,可以避免因重试导致的长时间阻塞。
实际操作时,我通常建议优先用 options { retry(3) } 这种整管线重试。因为如果只在某个编译阶段包一层 retry,一旦后续部署阶段因为网络抖动失败,编译阶段的 retry 并不会生效,反而容易让人误以为整个流水线已经重试过。配合 timeout 时,要注意 timeout 和 retry 的嵌套顺序:timeout 放在 retry 外面,会把整个重试过程的总时长包住;放在里面,则每次重试都各自计时。这个区别会直接影响“最多构建次数”下的最坏耗时,需要结合你的版本和任务时长确认。
重试次数不是越大越好
使用重试策略时,重试次数并非越多越好。每次重试都会占用Jenkins执行器,如果并发构建较多,会拖慢整个队列。特别在资源有限的情况下,过大的重试次数可能引发雪崩效应。另外,若管道中还有多个阶段各自重试,总执行次数可能呈指数增长。此时可以考虑使用lock资源或限制并发数来控制风险,但也要避免过度限制导致必要的重试无法进行。合理设置超时和重试的乘积,确保最坏情况下的时间可接受。
这里的“乘积”是一个需要实际计算的边界:比如每次重试最长 10 分钟,retry(3) 意味着最多 4 次尝试,那么这一段的阻塞上限是 40 分钟。如果执行器很紧张,40 分钟会让队列里其他任务积压。你可以通过在系统日志中观察构建耗时来估算,而不是拍脑袋定一个值。
插件重试要防止无限循环
使用Naginator这类重试插件时,它默认会按规则自动重新调度构建,如果未正确配置最大重试次数,可能造成无限构建循环。配置时需要设置'最大重试次数'和'重试延迟',并注意该插件的计数是基于单个构建任务,而不是全局。另外,如果多个任务使用同一套配置,重试次数会分别累计,不会互相抵消。建议在系统日志中观察构建历史,确认没有异常的大量重复构建。
这里要额外检查一个点:Naginator 的“最大重试次数”是“额外重试次数”,不是“总构建次数”。如果你设置最大重试次数为 3,那么一个构建任务失败后,会额外再调度 3 次,总共最多 4 次。如果你的“最多构建次数”是 3,那这里要填 2。另外,Naginator 是任务级别的,如果你的多分支流水线有 20 个分支,每个分支都配置了同样的重试规则,那么每个分支独立计算,总构建次数会变成 20 个分支各自的次数相加,而不是全局限住在某个值。
如何验证设置是否生效
要验证重试设置是否生效,可以手动触发一次失败的构建,然后在Jenkins的构建历史中查看是否有多次执行记录。更精确的做法是调用Jenkins API,比如访问/job/任务名/api/json,查看builds列表中的构建号和次数。如果希望程序化控制最大构建次数,可以在构建脚本中判断当前构建号,若超过预设的阈值则直接跳过后续步骤。注意,构建号在并发模式下可能不会严格顺序递增,需要结合时间戳来判断。
我自己的检查习惯是:先故意让一个任务失败,看构建历史里出现了几条记录。如果记录数超过预期,基本可以确定是触发器或者插件层面的重复调度。此时再去查看系统日志中的“Started by ...”和“Re-run”信息,定位是 retry 还是 Naginator 触发。如果发现构建号跳号,不要慌,结合构建开始时间来看顺序,因为并发场景下后启动的构建可能拿到更小的号。
最后想提一种外部控制方式:如果 Pipeline 逻辑已经很复杂,不妨在上游脚本里维护一个计数文件,每次触发前检查计数是否超过阈值。这种方式比在 Jenkins 内部判断更直观,也更容易写集成测试。但要注意并发写入计数文件时的竞争条件,建议用简单的文件锁或数据库原子操作,否则计数会不准确。