最佳实践
本章汇总 Jenkins Pipeline 的最佳实践,帮助你编写高效、可维护、安全的 Pipeline。这些建议来自 Jenkins 官方文档和社区经验总结。
Pipeline 设计原则
Groovy 作为胶水代码
Pipeline 中的 Groovy 代码应该作为"胶水"来连接各种操作,而不是作为 Pipeline 的主要功能。换句话说,不要依赖 Pipeline 功能(Groovy 或 Pipeline 步骤)来驱动构建过程,而是使用单个步骤(如 sh)来完成构建的多个部分。
原因:随着 Pipeline 复杂度的增加(Groovy 代码量、步骤数量等),需要更多的控制器资源(CPU、内存、存储)。将 Pipeline 视为完成构建的工具,而不是构建的核心。
错误示例:使用大量 Groovy 代码处理构建逻辑
// 不推荐:大量 Groovy 代码在控制器上执行
pipeline {
agent any
stages {
stage('Build') {
steps {
script {
def files = findFiles(glob: '**/*.java')
files.each { f ->
echo "Processing ${f.name}"
// 更多 Groovy 处理...
}
}
}
}
}
}
正确示例:将逻辑放到 shell 脚本中
// 推荐:使用 shell 步骤处理复杂逻辑
pipeline {
agent any
stages {
stage('Build') {
steps {
sh '''
# 所有逻辑在 Agent 上执行
find . -name "*.java" | while read file; do
echo "Processing $file"
# 处理逻辑...
done
# 使用 Maven 驱动整个构建过程
mvn clean deploy
'''
}
}
}
}
减少步骤重复
尽可能将 Pipeline 步骤合并为单个步骤,以减少 Pipeline 执行引擎本身产生的开销。例如,如果连续运行三个 shell 步骤,每个步骤都需要启动和停止,需要在 Agent 和控制器上创建和清理连接和资源。但如果将所有命令放入单个 shell 步骤中,则只需要启动和停止单个步骤。
错误示例:
// 不推荐:多个独立的步骤
stages {
stage('Build') {
steps {
echo 'Starting build...'
sh 'mvn clean compile'
echo 'Running tests...'
sh 'mvn test'
echo 'Packaging...'
sh 'mvn package'
echo 'Build complete'
}
}
}
正确示例:
// 推荐:合并为单个步骤
stages {
stage('Build') {
steps {
sh '''
echo 'Starting build...'
mvn clean compile
echo 'Running tests...'
mvn test
echo 'Packaging...'
mvn package
echo 'Build complete'
'''
}
}
}
避免 Groovy 性能陷阱
避免 JsonSlurper
JsonSlurper(以及类似的 XmlSlurper、readFile)可用于从磁盘读取文件,将文件中的数据解析为 JSON 对象,并使用类似 JsonSlurper().parseText(readFile("$LOCAL_FILE")) 的命令将该对象注入 Pipeline。这个命令会将本地文件加载到控制器内存中两次,如果文件很大或命令执行频繁,将需要大量内存。
解决方案:使用 shell 步骤并返回标准输出:
// 不推荐:在控制器上解析大文件
def config = new groovy.json.JsonSlurper().parseText(readFile('large-config.json'))
// 推荐:在 Agent 上使用 jq 处理
def config = sh(
script: 'cat large-config.json | jq ".database"',
returnStdout: true
).trim()
避免 HttpRequest
HttpRequest 命令通常用于从外部源获取数据并存储在变量中。这种做法不理想,因为请求直接来自控制器(如果控制器没有加载证书,HTTPS 请求可能会产生错误结果),而且请求的响应会被存储两次。
解决方案:使用 shell 步骤从 Agent 执行 HTTP 请求:
// 不推荐:从控制器发送 HTTP 请求
def response = httpRequest 'https://api.example.com/data'
// 推荐:从 Agent 发送请求
def response = sh(
script: 'curl -s https://api.example.com/data',
returnStdout: true
).trim()
避免复杂的 Groovy 代码
Pipeline 中的 Groovy 代码总是在控制器上执行,这意味着会使用控制器资源(内存和 CPU)。因此,减少 Pipeline 执行的 Groovy 代码量至关重要。
常见问题代码:
// 不推荐:使用 Groovy 处理大量数据
script {
def lines = readFile('large-log.txt').readLines()
lines.each { line ->
if (line.contains('ERROR')) {
echo "Found error: ${line}"
}
}
}
改进方案:
// 推荐:使用 shell 命令处理
sh '''
grep "ERROR" large-log.txt | while read line; do
echo "Found error: $line"
done
'''
避免调用 Jenkins.getInstance
在 Pipeline 或共享库中使用 Jenkins.instance 或其访问器方法表明代码被误用了。从非沙箱的共享库使用 Jenkins API 意味着共享库既是共享库,也是一种 Jenkins 插件。从 Pipeline 与 Jenkins API 交互时需要非常小心,以避免严重的安全和性能问题。
错误示例:
// 危险:直接访问 Jenkins API
import jenkins.model.Jenkins
script {
def instance = Jenkins.getInstance()
def allJobs = instance.getAllItems()
allJobs.each { job ->
echo "Job: ${job.fullName}"
}
}
正确方案:如果必须访问 Jenkins API,建议创建一个 Jenkins 插件,使用 Pipeline 的 Step API 实现安全包装器。
序列化与 NotSerializableException
Pipeline 代码经过 CPS(Continuation-Passing Style)转换,以便 Pipeline 能够在 Jenkins 重启后恢复执行。这意味着 Pipeline 能够在脚本运行时,即使关闭 Jenkins 或失去与 Agent 的连接,当 Jenkins 恢复时,它会记住正在执行的操作,Pipeline 脚本会继续执行,就像从未中断过一样。
确保变量可序列化
本地变量作为 Pipeline 状态的一部分在序列化期间被捕获。这意味着在 Pipeline 执行期间将不可序列化的对象存储在变量中会导致抛出 NotSerializableException。
错误示例:
// 可能导致 NotSerializableException
script {
def matcher = "hello world" =~ /hello/
echo "Match: ${matcher.find()}"
// 如果在此处暂停,matcher 对象不可序列化
sleep(time: 10, unit: 'SECONDS')
echo "After sleep"
}
解决方案:
// 方案 1:不将不可序列化对象赋值给变量
script {
def result = ("hello world" =~ /hello/).find()
echo "Match: ${result}"
sleep(time: 10, unit: 'SECONDS')
}
// 方案 2:使用 @NonCPS 注解
@NonCPS
def findMatch(String text) {
return (text =~ /hello/).find()
}
script {
def result = findMatch("hello world")
echo "Match: ${result}"
}
使用 @NonCPS 注解
如果必要,可以使用 @NonCPS 注解为特定方法禁用 CPS 转换,该方法的主体如果经过 CPS 转换将无法正确执行。但请注意,这也意味着 Groovy 函数必须完全重新启动,因为它没有被转换。
@NonCPS
def parseXml(String xml) {
def parser = new XmlSlurper()
return parser.parseText(xml)
}
pipeline {
agent any
stages {
stage('Parse') {
steps {
script {
def result = parseXml('<root><item>test</item></root>')
echo "Parsed: ${result.item}"
}
}
}
}
}
并发处理
避免共享工作空间
不要在多个 Pipeline 执行或多个不同的 Pipeline 之间共享工作空间。这种做法可能导致每个 Pipeline 中出现意外的文件修改或工作空间重命名。
最佳实践:
// 推荐:使用独特的容器构建
pipeline {
agent {
docker {
image 'maven:3.9-eclipse-temurin-21'
// 每次构建使用新的工作空间
reuseNode false
}
}
stages {
stage('Build') {
steps {
sh 'mvn clean package'
}
}
}
}
使用 Lockable Resources
如果必须在多个构建之间共享资源,使用 Lockable Resources Plugin:
pipeline {
agent any
stages {
stage('Deploy') {
steps {
lock(resource: 'shared-server', quantity: 1) {
sh './deploy.sh'
}
}
}
}
}
禁用并发构建
对于不适合并发执行的任务:
pipeline {
agent any
options {
// 禁用并发构建
disableConcurrentBuilds()
// 或中止之前的构建
disableConcurrentBuilds(abortPrevious: true)
}
stages {
stage('Deploy') {
steps {
sh './deploy.sh'
}
}
}
}
构建清理
配置构建丢弃策略
作为 Jenkins 管理员,删除旧的或不需要的构建可以保持 Jenkins 控制器高效运行。如果不删除旧构建,当前和相关构建可用的资源就会减少。
pipeline {
agent any
options {
// 构建历史保留策略
buildDiscarder(logRotator(
numToKeepStr: '20', // 保留最近 20 个构建
artifactNumToKeepStr: '5', // 保留最近 5 个构建的产物
daysToKeepStr: '30', // 保留 30 天内的构建
artifactDaysToKeepStr: '7' // 保留 7 天内的构建产物
))
}
stages {
stage('Build') {
steps {
sh 'mvn clean package'
}
}
}
}
清理工作空间
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean package'
}
}
}
post {
always {
// 清理工作空间
cleanWs()
}
}
}
条件清理
post {
// 仅在失败时保留工作空间用于调试
success {
cleanWs()
}
failure {
echo "Workspace preserved for debugging at ${env.WORKSPACE}"
}
}
共享库最佳实践
不要覆盖内置 Pipeline 步骤
尽可能避免自定义或覆盖 Pipeline 步骤。覆盖内置 Pipeline 步骤是使用共享库覆盖标准 Pipeline API(如 sh 或 timeout)的过程。这个过程是危险的,因为 Pipeline API 可能随时更改,导致自定义代码崩溃或产生与预期不同的结果。
当自定义代码因 Pipeline API 更改而崩溃时,故障排除很困难,因为即使自定义代码没有更改,API 更新后可能无法正常工作。
避免大型全局变量声明文件
拥有大型变量声明文件可能需要大量内存却几乎没有好处,因为无论是否需要变量,每个 Pipeline 都会加载该文件。建议创建仅包含与当前执行相关的变量的小型变量文件。
不推荐:
// vars/common.groovy - 过大的全局变量文件
def PROD_SERVERS = ['server1', 'server2', 'server3', ...]
def STAGING_SERVERS = ['staging1', 'staging2', ...]
def DEV_SERVERS = ['dev1', 'dev2', 'dev3', ...]
def ALL_CONFIGS = [/* 大量配置 */]
// ... 更多变量
推荐:
// vars/prodConfig.groovy - 按需加载
def call() {
return [
servers: ['server1', 'server2'],
database: 'prod-db',
region: 'us-east-1'
]
}
// vars/stagingConfig.groovy
def call() {
return [
servers: ['staging1'],
database: 'staging-db',
region: 'us-west-2'
]
}
资源管理
合理设置超时
pipeline {
agent any
options {
// Pipeline 级别超时
timeout(time: 1, unit: 'HOURS')
}
stages {
stage('Build') {
options {
// 阶段级别超时
timeout(time: 30, unit: 'MINUTES')
}
steps {
sh 'mvn clean package'
}
}
stage('Long Running Task') {
steps {
timeout(time: 10, unit: 'MINUTES') {
sh './long-task.sh'
}
}
}
}
}
使用重试机制
pipeline {
agent any
stages {
stage('Flaky Test') {
steps {
retry(3) {
sh './run-flaky-tests.sh'
}
}
}
}
}
添加时间戳
pipeline {
agent any
options {
// 为日志添加时间戳
timestamps()
}
stages {
stage('Build') {
steps {
echo 'Starting build...'
sh 'mvn clean package'
}
}
}
}
环境与配置管理
使用环境变量
pipeline {
agent any
environment {
// 静态值
APP_NAME = 'my-app'
// 使用凭据
DB_PASSWORD = credentials('db-password')
// 动态值
BUILD_VERSION = "${env.BUILD_NUMBER}-${env.GIT_COMMIT_SHORT}"
}
stages {
stage('Build') {
steps {
echo "Building ${env.APP_NAME} version ${env.BUILD_VERSION}"
}
}
}
}
使用 withCredentials
stage('Deploy') {
steps {
withCredentials([
usernamePassword(
credentialsId: 'deploy-credentials',
usernameVariable: 'DEPLOY_USER',
passwordVariable: 'DEPLOY_PASS'
)
]) {
sh '''
sshpass -p "$DEPLOY_PASS" ssh $DEPLOY_USER@server "deploy.sh"
'''
}
}
}
安全最佳实践
使用凭据而非硬编码
// 错误:硬编码密码
sh 'mysql -u root -pMyPassword123'
// 正确:使用凭据
environment {
DB_CREDS = credentials('mysql-credentials')
}
steps {
sh 'mysql -u $DB_CREDS_USR -p$DB_CREDS_PSW'
}
控制器不执行构建
// 推荐:总是使用 Agent
pipeline {
agent none // 不在控制器上执行
stages {
stage('Build') {
agent { label 'linux' }
steps {
sh 'mvn clean package'
}
}
}
}
Pipeline 耐久性设置
耐久性级别选择
Jenkins 提供不同的耐久性级别来平衡性能和数据安全:
pipeline {
agent any
options {
// 最高耐久性(默认):适合生产环境
durabilityHint('MAX_SURVIVABILITY')
// 性能优化:适合开发/测试环境
// durabilityHint('PERFORMANCE_OPTIMIZED')
// 无耐久性:适合最快速执行
// durabilityHint('NONE')
}
stages {
stage('Build') {
steps {
sh 'mvn clean package'
}
}
}
}
耐久性级别说明:
| 级别 | 说明 | 适用场景 |
|---|---|---|
MAX_SURVIVABILITY | 最高耐久性,所有状态都持久化 | 生产环境 |
SURVIVABLE_NONATOMIC | 平衡耐久性和性能 | 一般项目 |
PERFORMANCE_OPTIMIZED | 性能优先,减少持久化操作 | 开发/测试 |
NONE | 不进行持久化 | 快速执行的一次性任务 |
完整最佳实践示例
pipeline {
agent {
label 'linux && docker'
}
environment {
APP_NAME = 'my-app'
REGISTRY = 'registry.example.com'
IMAGE_TAG = "${env.BUILD_NUMBER}"
}
options {
// 构建历史管理
buildDiscarder(logRotator(numToKeepStr: '20'))
// 超时设置
timeout(time: 1, unit: 'HOURS')
// 禁用并发构建
disableConcurrentBuilds()
// 添加时间戳
timestamps()
// 耐久性设置
durabilityHint('MAX_SURVIVABILITY')
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build & Test') {
steps {
sh '''
# 合并多个命令为单个 shell 步骤
mvn clean package
# 运行测试
mvn test
'''
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
}
}
stage('Docker Build') {
steps {
sh "docker build -t ${REGISTRY}/${APP_NAME}:${IMAGE_TAG} ."
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
timeout(time: 10, unit: 'MINUTES') {
input message: 'Deploy to production?'
}
sh '''
docker push ${REGISTRY}/${APP_NAME}:${IMAGE_TAG}
kubectl set image deployment/${APP_NAME} \
${APP_NAME}=${REGISTRY}/${APP_NAME}:${IMAGE_TAG}
'''
}
}
}
post {
always {
cleanWs()
}
failure {
mail to: '[email protected]',
subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Check console output: ${env.BUILD_URL}"
}
}
}
最佳实践清单
Pipeline 设计
- 使用 Groovy 作为胶水代码,而非主要逻辑
- 合并相似步骤减少开销
- 避免 JsonSlurper 和 HttpRequest
- 避免复杂的 Groovy 代码
资源管理
- 配置构建丢弃策略
- 清理工作空间
- 设置合理的超时
- 使用重试机制处理不稳定操作
安全
- 使用凭据管理敏感信息
- 控制器不执行构建
- 避免硬编码密码
性能
- 避免共享工作空间
- 使用并行执行
- 选择合适的耐久性级别
- 监控资源使用
共享库
- 不要覆盖内置 Pipeline 步骤
- 避免大型全局变量文件
- 保持共享库简洁