目录
- 概述
- [JENKINS_HOME 目录结构](#JENKINS_HOME 目录结构)
- [Master 安全停机](#Master 安全停机)
- 数据备份方案
- 4.1 备份策略选择
- 4.2 推荐备份方案
- 4.3 备份时是否需要停机?(核心问题) ⭐
- 4.4 备份时的注意事项(详细清单) ⭐
- 4.5 备份内容详细说明
- Linux实时同步技术详解 ⭐新增
- 5.1 技术选型对比
- 5.2 rsync基础同步
- 5.3 inotify+rsync实时同步
- 5.4 lsyncd实时同步守护进程(推荐)
- 5.5 条件过滤与高级配置
- 5.6 实时同步+定时备份完整方案
- 自动化备份脚本
- 数据恢复
- 高可用场景下的维护
- 最佳实践与检查清单
一、概述
1.1 为什么需要学习停机与备份
┌─────────────────────────────────────────────────────────────────┐
│ 停机与备份的重要性: │
│ │
│ 场景1:系统升级/插件更新 → 需要安全停机 │
│ 场景2:硬件故障/系统崩溃 → 需要快速恢复 │
│ 场景3:数据误操作/配置错误 → 需要回滚到之前状态 │
│ 场景4:灾难恢复 → 需要完整的备份和恢复流程 │
│ 场景5:环境迁移 → 需要完整的配置和数据迁移 │
│ │
│ ⚠️ 核心原则: │
│ ├── 停机前必须确保构建任务安全完成或可恢复 │
│ └── 备份必须包含所有关键数据和配置 │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 学习目标
| 目标 | 说明 |
|---|---|
| ✅ 掌握安全停机流程 | 了解优雅停机的步骤和注意事项 |
| ✅ 掌握完整备份方案 | 能够制定和执行备份策略 |
| ✅ 掌握数据恢复流程 | 能够从备份快速恢复Jenkins |
| ✅ 掌握自动化运维 | 编写自动备份和维护脚本 |
二、JENKINS_HOME 目录结构
2.1 完整目录结构
JENKINS_HOME (通常为 /var/lib/jenkins 或 C:\ProgramData\Jenkins\.jenkins)
│
├── config.xml # Jenkins主配置文件(最重要!)
├── *.xml # 其他全局配置文件
│
├── jobs/ # 所有Job的配置和构建历史
│ └── {job-name}/
│ ├── config.xml # Job配置
│ ├── nextBuildNumber # 下一个构建号
│ ├── builds/ # 构建记录
│ │ └── {build-number}/ # 构建详情
│ │ ├── build.xml # 构建结果
│ │ ├── changelog.xml # 变更日志
│ │ ├── log # 构建日志
│ │ └── archive/ # 构建产物
│ └── workspace/ # 工作空间(可选备份)
│
├── users/ # 用户信息和权限
│ └── {username}/
│ └── config.xml # 用户配置
│
├── secrets/ # 加密密钥(最重要!)
│ ├── master.key # 主密钥
│ ├── hudson.util.Secret # 加密密钥
│ └── ... # 其他密钥文件
│
├── credentials.xml # 凭据存储
├── secrets/ # 凭据加密密钥
│
├── plugins/ # 已安装的插件
│ └── *.jpi / *.hpi # 插件文件
│
├── tools/ # 全局工具配置
│
├── userContent/ # 用户自定义内容
│
├── fingerprints/ # 文件指纹记录
├── updates/ # 更新中心缓存
├── logs/ # Jenkins日志
│
├── workspace/ # Master工作空间
├── war/ # Jenkins WAR包缓存
│
└── *.xml # 其他配置文件
2.2 关键文件说明
| 文件/目录 | 重要性 | 说明 | 必须备份 |
|---|---|---|---|
config.xml |
⭐⭐⭐⭐⭐ | Jenkins主配置,包含核心设置 | ✅ 是 |
secrets/ |
⭐⭐⭐⭐⭐ | 加密密钥,丢失后凭据无法解密 | ✅ 是 |
credentials.xml |
⭐⭐⭐⭐⭐ | 凭据存储(密码、Token等) | ✅ 是 |
jobs/ |
⭐⭐⭐⭐⭐ | Job配置和历史 | ✅ 是 |
users/ |
⭐⭐⭐⭐ | 用户和权限配置 | ✅ 是 |
plugins/ |
⭐⭐⭐⭐ | 插件列表和配置 | ✅ 是 |
*.xml |
⭐⭐⭐ | 其他全局配置 | ✅ 是 |
workspace/ |
⭐⭐ | 工作空间(可重新生成) | ❌ 可选 |
tools/ |
⭐⭐ | 工具配置(可重新配置) | ❌ 可选 |
updates/ |
⭐ | 更新缓存(可重新下载) | ❌ 可选 |
logs/ |
⭐ | 日志文件(通常不备份) | ❌ 否 |
war/ |
⭐ | WAR包缓存(可重新下载) | ❌ 否 |
fingerprints/ |
⭐⭐ | 文件指纹(可重建) | ❌ 可选 |
2.3 备份优先级
🔴 必须备份(一级):
├── config.xml
├── secrets/
├── credentials.xml
├── jobs/*/config.xml
└── users/
🟡 重要备份(二级):
├── jobs/*/builds/ (最近N个构建)
├── plugins/
└── tools/
🟢 可选备份(三级):
├── jobs/*/workspace/
├── fingerprints/
└── userContent/
三、Master 安全停机
3.1 停机前准备清单
powershell
# PowerShell / Bash 通用检查项
# 1. 检查当前正在运行的构建数量
# Web界面:http://your-jenkins/computer/api/json?pretty=true
# 或通过API:
Invoke-RestMethod 'http://localhost:8080/api/json' | Select-Object busyExecutors, totalExecutors
# 2. 检查队列中的等待任务
Invoke-RestMethod 'http://localhost:8080/queue/api/json'
# 3. 检查在线Agent数量
Invoke-RestMethod 'http://localhost:8080/computer/api/json?tree=computer[displayName,offline]'
# 4. 查看最近的构建活动
Get-EventLog -LogName Application -Source 'Jenkins' -Newest 10
停机前准备清单:
□ 通知团队即将进行停机维护
□ 检查是否有正在运行的关键构建
□ 检查队列中是否有等待的任务
□ 记录当前系统状态(截图/导出)
□ 确认备份已完成或将在停机前执行
□ 准备回滚方案(如果升级)
□ 预估停机时长并通知相关人员
3.2 优雅停机流程(推荐)
方式一:Web界面操作(推荐新手)
步骤1:进入"准备关机"模式
┌─────────────────────────────────────────────────────────────────┐
│ 路径:Manage Jenkins → Prepare for Shutdown │
│ │
│ 效果: │
│ ├── 不再接受新的构建任务 │
│ ├── 正在运行的构建继续执行直到完成 │
│ ├── Agent不再接受新任务 │
│ └── 显示"Jenkins is preparing for shutdown"横幅 │
│ │
│ 适用场景:计划性维护、版本升级 │
└─────────────────────────────────────────────────────────────────┘
步骤2:等待正在运行的构建完成
- 监控:Build Executor Status 页面
- 可以点击 "Cancel" 取消某些非关键构建
- 等待所有构建完成(或手动取消)
步骤3:执行停机
- 方式A:Web界面 → Shut down Jenkins (when jobs complete)
- 方式B:命令行停止服务
方式二:CLI命令行(推荐熟练用户)
bash
# Linux
# 1. 进入准备关机模式
curl -X POST 'http://localhost:8080/prepareShutdown' \
--user admin:password
# 2. 检查是否还有运行中的构建
curl -s 'http://localhost:8080/api/json?tree=busyExecutors' \
--user admin:password
# 3. 当busyExecutors=0时,可以安全关闭
sudo systemctl stop jenkins
# 或
sudo service jenkins stop
# Windows PowerShell
# 1. 进入准备关机模式
Invoke-RestMethod -Uri 'http://localhost:8080/prepareShutdown' \
-Method POST -Credential (Get-Credential)
# 2. 检查运行状态
(Invoke-RestUri 'http://localhost:8080/api/json?tree=busyExecutors').busyExecutors
# 3. 停止服务
Stop-Service -Name Jenkins
方式三:使用Jenkins CLI工具
bash
# 下载CLI jar
wget http://localhost:8080/jnlpJars/jenkins-cli.jar
# 进入准备关机模式
java -jar jenkins-cli.jar -s http://localhost:8080 prepare-shutdown \
--username admin --password password
# 等待构建完成后关闭
java -jar jenkins-cli.jar -s http://localhost:8080 safe-shutdown \
--username admin --password password
# 立即关闭(不等待构建完成,危险!)
java -jar jenkins-cli.jar -s http://localhost:8080 shutdown \
--username admin --password password
3.3 停机方式对比
| 停机方式 | 等待构建 | 新任务 | 适用场景 | 风险等级 |
|---|---|---|---|---|
| Prepare for Shutdown | ✅ 完成 | ❌ 拒绝 | 计划维护 | 🟢 低 |
| Safe Shutdown | ✅ 完成 | ❌ 拒绝 | 计划关机 | 🟢 低 |
| Cancel Shutdown | ❌ 取消 | ❌ 拒绝 | 紧急关机 | 🟡 中 |
| Immediate Shutdown | ❌ 中断 | ❌ 拒绝 | 系统故障 | 🔴 高 |
| kill -9 / Force Stop | ❌ 杀死 | ❌ 无 | 极端情况 | 🔴 极高 |
3.4 正在运行的Job处理策略
┌─────────────────────────────────────────────────────────────────┐
│ 停机时正在运行的Job处理策略 │
│ │
│ 策略1:等待完成(推荐) │
│ ├── 使用Prepare for Shutdown模式 │
│ ├── 设置合理的超时时间 │
│ └── 监控构建进度 │
│ │
│ 策略2:允许部分中断 │
│ ├── 关键Job(生产部署)→ 等待完成 │
│ ├── 非关键Job(测试构建)→ 允许中断 │
│ └── Pipeline中添加checkpoint机制 │
│ │
│ 策略3:全部中断(紧急情况) │
│ ├── 立即停机 │
│ ├── Job会标记为ABORTED │
│ └── 重启后需要手动触发重新构建 │
│ │
└─────────────────────────────────────────────────────────────────┘
Pipeline中处理中断的最佳实践:
groovy
pipeline {
agent any
options {
timeout(time: 30, unit: 'MINUTES')
retry(1)
}
stages {
stage('Build') {
steps {
echo 'Building...'
}
}
stage('Deploy') {
when {
expression {
// 检查是否处于关机模式,避免在关机时部署生产
def status = sh(script: 'curl -sf http://localhost:8080/api/json', returnStdout: true)
return !status.contains('"preparingShutdown":true')
}
}
steps {
echo 'Deploying to production...'
}
}
}
post {
aborted {
echo '构建被中断!可能由于Jenkins停机'
// 发送通知
mail to: 'team@example.com',
subject: "${env.JOB_NAME} #${env.BUILD_NUMBER} ABORTED",
body: "构建被中断,请检查是否需要重新触发"
}
}
}
3.5 停机期间的新请求处理
┌─────────────────────────────────────────────────────────────────┐
│ 停机期间各功能状态 │
│ │
│ 功能 │ Prepare模式 │ 完全停机后 │
│ ──────────────────────┼─────────────┼──────────── │
│ 触发新构建 │ ❌ 拒绝 │ ❌ 无法连接 │
│ Webhook接收 │ ❌ 拒绝 │ ❌ 无法连接 │
│ 查看构建历史 │ ✅ 可用 │ ❌ 无法连接 │
│ 查看构建日志 │ ✅ 可用 │ ❌ 无法连接 │
│ API访问 │ ⚠️ 只读 │ ❌ 无法连接 │
│ Agent连接 │ ❌ 拒绝 │ ❌ 无法连接 │
│ │
│ 建议: │
│ 1. 在负载均衡器上显示维护页面 │
│ 2. 配置友好的503错误页面 │
│ 3. 提前通知开发团队停机时间窗口 │
│ 4. 对于重要项目,考虑配置备用CI系统 │
│ │
└─────────────────────────────────────────────────────────────────┘
四、数据备份方案
4.1 备份策略选择
┌─────────────────────────────────────────────────────────────────┐
│ 备份策略对比 │
│ │
│ 策略 │ 频率 │ RPO │ 复杂度 │ 适用场景 │
│ ─────────────────┼─────────┼──────────┼─────────┼────────── │
│ 全量备份 │ 每日 │ 24小时 │ 低 │ 小规模 │
│ 增量备份 │ 每小时 │ 1小时 │ 中 │ 中等规模 │
│ 实时同步 │ 实时 │ 近实时 │ 高 │ 大规模 │
│ 快照备份 │ 定时 │ 取决频率 │ 低 │ 云环境 │
│ │
│ RPO (Recovery Point Objective): 可容忍的数据丢失时间 │
│ RTO (Recovery Time Objective): 可容忍的恢复时间 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 推荐备份方案(适用于大多数场景)
方案一:全量备份 + 增量备份(推荐)
bash
#!/bin/bash
# backup_jenkins.sh - Jenkins全量+增量备份脚本
# 适用于Linux环境
JENKINS_HOME="/var/lib/jenkins"
BACKUP_DIR="/backup/jenkins"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
# 创建备份目录
mkdir -p "$BACKUP_DIR/daily/$DATE"
mkdir -p "$BACKUP_DIR/incremental"
echo "[$(date)] 开始备份 Jenkins..."
# ============ 全量备份(每周日凌晨2点执行)============
if [ "$(date +%u)" -eq 7 ]; then
echo "[$(date)] 执行全量备份..."
# 1. 备份核心配置(最重要)
tar -czf "$BACKUP_DIR/daily/$DATE/config.tar.gz" \
-C "$JENKINS_HOME" \
config.xml \
credentials.xml \
secrets/ \
users/ \
*.xml
# 2. 备份Jobs配置(不含workspace和旧构建)
find "$JENKINS_HOME/jobs" -name "config.xml" -o -name "nextBuildNumber" | \
tar -czf "$BACKUP_DIR/daily/$DATE/jobs-config.tar.gz" -T -
# 3. 备份最近7天的构建历史
find "$JENKINS_HOME/jobs" -path "*/builds/*" -mtime -7 | \
tar -czf "$BACKUP_DIR/daily/$DATE/jobs-recent-builds.tar.gz" -T -
# 4. 备份插件列表
tar -czf "$BACKUP_DIR/daily/$DATE/plugins.tar.gz" \
-C "$JENKINS_HOME" plugins/
echo "[$(date)] 全量备份完成: $BACKUP_DIR/daily/$DATE"
# ============ 增量备份(每天凌晨2点执行)============
else
echo "[$(date)] 执行增量备份..."
# 使用rsync做增量备份到最新全量备份
LATEST_FULL=$(ls -td "$BACKUP_DIR"/daily/*/ 2>/dev/null | head -1)
if [ -n "$LATEST_FULL" ]; then
rsync -avz \
--delete \
--exclude='workspace/' \
--exclude='*/workspace/*' \
--exclude='*.log' \
--exclude='logs/' \
--exclude='war/' \
--exclude='updates/' \
--exclude='caches/' \
--exclude='fingerprints/' \
"$JENKINS_HOME/" "$BACKUP_DIR/incremental/latest/"
# 创建增量快照(硬链接,节省空间)
cp -al "$BACKUP_DIR/incremental/latest/" "$BACKUP_DIR/incremental/$DATE/"
echo "[$(date)] 增量备份完成: $BACKUP_DIR/incremental/$DATE"
else
echo "[ERROR] 未找到全量备份,请先执行全量备份"
exit 1
fi
fi
# ============ 清理过期备份 ============
echo "[$(date)] 清理 $RETENTION_DAYS 天前的备份..."
find "$BACKUP_DIR/daily" -maxdepth 1 -type d -mtime +$RETENTION_DAYS -exec rm -rf {} \;
find "$BACKUP_DIR/incremental" -maxdepth 1 -type d -mtime +$RETENTION_DAYS -exec rm -rf {} \;
# ============ 备份验证 ============
echo "[$(date)] 验证备份完整性..."
for file in "$BACKUP_DIR/daily/$DATE"/*.tar.gz; do
if [ -f "$file" ]; then
if gzip -t "$file"; then
echo "✓ $(basename $file) - OK"
else
echo "✗ $(basename $file) - CORRUPTED!"
fi
fi
done
echo "[$(date)] 备份完成!"
echo "备份位置: $BACKUP_DIR"
du -sh "$BACKUP_DIR/daily/$DATE" 2>/dev/null || du -sh "$BACKUP_DIR/incremental/$DATE"
方案二:Windows PowerShell备份脚本
powershell
# backup_jenkins.ps1 - Jenkins备份脚本(Windows版)
$ErrorActionPreference = "Stop"
$jenkinsHome = "C:\ProgramData\Jenkins\.jenkins"
$backupRoot = "D:\Backup\Jenkins"
$date = Get-Date -Format "yyyyMMdd_HHmmss"
$retentionDays = 30
Write-Host "[$(Get-Date)] 开始备份 Jenkins..." -ForegroundColor Green
# 创建备份目录
$backupDir = Join-Path $backupRoot "daily\$date"
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
try {
# 1. 备份核心配置(最重要)
Write-Host "[$(Get-Date)] 备份核心配置..." -ForegroundColor Cyan
$coreFiles = @(
"config.xml",
"credentials.xml"
)
foreach ($file in $coreFiles) {
$srcPath = Join-Path $jenkinsHome $file
if (Test-Path $srcPath) {
Copy-Item -Path $srcPath -Destination $backupDir -Force
Write-Host " ✓ $file" -ForegroundColor Green
}
}
# 2. 备份secrets目录(加密密钥,至关重要!)
Write-Host "[$(Get-Date)] 备份secrets目录..." -ForegroundColor Cyan
$secretsSrc = Join-Path $jenkinsHome "secrets"
if (Test-Path $secretsSrc) {
Copy-Item -Path $secretsSrc -Destination $backupDir -Recurse -Force
Write-Host " ✓ secrets/" -ForegroundColor Green
}
# 3. 备份users目录
Write-Host "[$(Get-Date)] 备份users目录..." -ForegroundColor Cyan
$usersSrc = Join-Path $jenkinsHome "users"
if (Test-Path $usersSrc) {
Copy-Item -Path $usersSrc -Destination $backupDir -Recurse -Force
Write-Host " ✓ users/" -ForegroundColor Green
}
# 4. 备份Jobs配置(只备份config.xml和最近构建)
Write-Host "[$(Get-Date)] 备份Jobs配置..." -ForegroundColor Cyan
$jobsSrc = Join-Path $jenkinsHome "jobs"
if (Test-Path $jobsSrc) {
$jobsDest = Join-Path $backupDir "jobs"
New-Item -ItemType Directory -Path $jobsDest -Force | Out-Null
# 复制每个job的config.xml
Get-ChildItem -Path $jobsSrc -Directory | ForEach-Object {
$jobConfig = Join-Path $_.FullName "config.xml"
if (Test-Path $jobConfig) {
$destJobDir = Join-Path $jobsDest $_.Name
New-Item -ItemType Directory -Path $destJobDir -Force | Out-Null
Copy-Item -Path $jobConfig -Destination $destJobDir -Force
# 复制nextBuildNumber
$nextBuild = Join-Path $_.FullName "nextBuildNumber"
if (Test-Path $nextBuild) {
Copy-Item -Path $nextBuild -Destination $destJobDir -Force
}
# 复制最近7天的构建(最多保留20个)
$buildsDir = Join-Path $_.FullName "builds"
if (Test-Path $buildsDir) {
$recentBuilds = Get-ChildItem -Path $buildsDir -Directory |
Sort-Object LastWriteTime -Descending |
Select-Object -First 20
$destBuildsDir = Join-Path $destJobDir "builds"
New-Item -ItemType Directory -Path $destBuildsDir -Force | Out-Null
foreach ($build in $recentBuilds) {
Copy-Item -Path $build.FullName -Destination $destBuildsDir -Recurse -Force
}
}
}
}
Write-Host " ✓ jobs/ (config + recent builds)" -ForegroundColor Green
}
# 5. 备份插件信息
Write-Host "[$(Get-Date)] 备份插件..." -ForegroundColor Cyan
$pluginsSrc = Join-Path $jenkinsHome "plugins"
if (Test-Path $pluginsSrc) {
# 只备份插件清单和配置,不备份jpi/hpi文件(可重新下载)
$pluginsDest = Join-Path $backupDir "plugins"
New-Item -ItemType Directory -Path $pluginsDest -Force | Out-Null
Get-ChildItem -Path $pluginsSrc -Filter "*.xml" | ForEach-Object {
Copy-Item -Path $_.FullName -Destination $pluginsDest -Force
}
# 导出已安装插件列表(用于恢复时安装)
Get-ChildItem -Path $pluginsSrc -Filter "*.jpi" |
Select-Object @{N='Plugin';E={$_.BaseName}} |
Export-Csv -Path (Join-Path $pluginsDest "installed-plugins.csv") -NoTypeInformation
Write-Host " ✓ plugins/ (manifest only)" -ForegroundColor Green
}
# 6. 创建备份元数据
$metadata = @{
BackupTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
JenkinsVersion = (Get-Content (Join-Path $jenkinsHome "config.xml") | Select-String "version")
TotalSizeGB = [math]::Round((Get-ChildItem -Path $backupDir -Recurse | Measure-Object -Property Length -Sum).Sum / 1GB, 2)
Hostname = hostname
} | ConvertTo-Json
$metadata | Out-File -FilePath (Join-Path $backupDir "metadata.json") -Encoding UTF8
Write-Host "`n[$(Get-Date)] ✓ 备份成功完成!" -ForegroundColor Green
Write-Host "备份位置: $backupDir" -ForegroundColor Yellow
Write-Host "备份大小: $($metadata.TotalSizeGB) GB" -ForegroundColor Yellow
} catch {
Write-Host "`n[$(Get-Date)] ✗ 备份失败!" -ForegroundColor Red
Write-Host "错误: $_" -ForegroundColor Red
exit 1
}
# 清理过期备份
Write-Host "`n[$(Get-Date)] 清理 $retentionDays 天前的备份..." -ForegroundColor Gray
Get-ChildItem -Path (Join-Path $backupRoot "daily") -Directory |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$retentionDays) } |
Remove-Item -Recurse -Force
Write-Host "[$(Get-Date)] 备份任务完成!" -ForegroundColor Green
4.3 备份时是否需要停机?(核心问题)
┌─────────────────────────────────────────────────────────────────┐
│ ⚠️ 这是运维中最常被问到的问题! │
│ │
│ 简短答案: │
│ ├── 推荐停机备份(最安全) │
│ ├── 可以在线热备(有条件) │
│ └── 绝对不能做的操作(会导致数据损坏) │
│ │
└─────────────────────────────────────────────────────────────────┘
4.3.1 三种备份模式对比
| 备份模式 | 是否停机 | 数据一致性 | 安全性 | 适用场景 |
|---|---|---|---|---|
| 冷备份(离线备份) | ✅ 停机 | ⭐⭐⭐⭐⭐ 完全一致 | 🟢 最安全 | 推荐!日常备份 |
| 温备份(准备关机模式) | ⚠️ 准备关机 | ⭐⭐⭐⭐ 基本一致 | 🟡 较安全 | 大规模Jenkins |
| 热备份(在线运行) | ❌ 不停机 | ⭐⭐ 可能不一致 | 🔴 有风险 | 仅限特定场景 |
4.3.2 为什么推荐停机备份?
┌─────────────────────────────────────────────────────────────────┐
│ 不停机备份的风险: │
│ │
│ 风险1:文件不一致 │
│ ├── 备份config.xml时是版本A │
│ ├── 备份jobs/xxx/config.xml时已变成版本B │
│ └── 恢复后配置不匹配 → Jenkins启动失败或行为异常 │
│ │
│ 风险2:构建数据损坏 │
│ ├── 正在写入build.xml时被复制 │
│ ├── 得到的是半写完的文件 │
│ └── 恢复后该构建记录损坏,无法查看 │
│ │
│ 风险3:凭据加密不同步 │
│ ├── credentials.xml引用了secrets中的密钥 │
│ ├── 如果两者备份时间点不同 │
│ └── 凭据无法解密!所有密码/Token失效 │
│ │
│ 风险4:插件状态不一致 │
│ ├── plugins目录和config.xml中的插件列表不同步 │
│ └── 启动时插件加载失败 │
│ │
│ 风险5:nextBuildNumber冲突 │
│ ├── 备份时某个Job正在构建(#100进行中) │
│ ├── nextBuildNumber=101 │
│ └── 但builds/100还没完成写入 → 构建号断裂 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.3.3 什么情况下可以在线热备?
只有在满足以下所有条件时,才可以考虑在线热备:
✅ 条件1:使用文件系统快照技术
├── LVM Snapshot (Linux)
├── ZFS Snapshot (ZFS文件系统)
├── VSS Snapshot (Windows)
└── 云存储快照 (AWS EBS/Azure Disk)
→ 快照可以在瞬间创建一致性的时间点副本
✅ 条件2:只备份静态配置文件
├── 只备份 config.xml, credentials.xml, secrets/
├── 不备份 jobs/builds/ (动态变化)
└── 构建历史可以丢失,但配置必须完整
✅ 条件3:业务允许一定数据丢失
├── RPO要求不高(可接受丢失几小时数据)
├── 构建历史可以重建
└── 只是用于灾难恢复,不是日常备份
❌ 以下情况绝对不能在线热备:
├── 使用普通的 cp/tar/rsync 命令
├── Jenkins有正在运行的构建
├── 高峰期(频繁的构建触发)
├── 对凭据一致性要求高
└── 生产环境的关键Jenkins实例
4.3.4 推荐的最佳实践:停机窗口备份
bash
#!/bin/bash
# safe_backup_with_downtime.sh - 带停机窗口的安全备份脚本
# 这是最推荐的备份方式!
JENKINS_URL="http://localhost:8080"
ADMIN_USER="admin"
ADMIN_PASS="password"
JENKINS_SERVICE="jenkins"
BACKUP_SCRIPT="/usr/local/bin/backup_jenkins.sh"
MAX_WAIT_MINUTES=30
echo "========================================="
echo " Jenkins 安全备份流程(带停机窗口)"
echo "========================================="
# ========== Phase 1: 进入准备关机模式 ==========
echo ""
echo "【Phase 1/4】进入准备关机模式..."
echo " → Jenkins将不再接受新任务"
echo " → 正在运行的任务继续执行"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$JENKINS_URL/prepareShutdown" \
--user "$ADMIN_USER:$ADMIN_PASS")
if [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "403" ]; then
echo " ✓ 已进入准备关机模式 (HTTP $HTTP_CODE)"
else
echo " ⚠️ 返回码: $HTTP_CODE (可能已在关机模式或权限问题)"
fi
# ========== Phase 2: 等待构建完成 ==========
echo ""
echo "【Phase 2/4】等待正在运行的构建完成..."
echo " → 最长等待: ${MAX_WAIT_MINUTES}分钟"
WAITED=0
INTERVAL=10
MAX_SECONDS=$((MAX_WAIT_MINUTES * 60))
while [ $WAITED -lt $MAX_SECONDS ]; do
# 获取当前繁忙执行器数量
RESPONSE=$(curl -s "$JENKINS_URL/api/json?tree=busyExecutors,totalExecutors" \
--user "$ADMIN_USER:$ADMIN_PASS" 2>/dev/null)
if [ -z "$RESPONSE" ]; then
echo " ⚠️ 无法连接Jenkins API,继续等待..."
else
BUSY=$(echo "$RESPONSE" | grep -o '"busyExecutors":[0-9]*' | grep -o '[0-9]*')
TOTAL=$(echo "$RESPONSE" | grep -o '"totalExecutors":[0-9]*' | grep -o '[0-9]*')
if [ "$BUSY" = "0" ] || [ -z "$BUSY" ]; then
echo " ✓ 所有构建已完成! (执行器: $BUSY/$TOTAL)"
break
fi
echo " ⏳ 正在运行: $BUSY/$TOTAL 个构建... (已等待 ${WAITED}s)"
fi
sleep $INTERVAL
WAITED=$((WAITED + INTERVAL))
done
if [ $WAITED -ge $MAX_SECONDS ]; then
echo ""
echo " ⚠️ 超过最大等待时间!"
echo " 请选择:"
echo " 1) 继续等待 (输入 1)"
echo " 2) 取消剩余构建并立即备份 (输入 2)"
read -p " 选择 [1/2]: " choice
if [ "$choice" = "2" ]; then
echo " → 正在取消队列中的任务..."
curl -s -X POST "$JENKINS_URL/queue/cancelAll" \
--user "$ADMIN_USER:$ADMIN_PASS" > /dev/null 2>&1
echo " ✓ 已取消排队任务"
fi
fi
# ========== Phase 3: 执行备份(Jenkins仍在线但无新任务)==========
echo ""
echo "【Phase 3/4】执行数据备份..."
echo " → 此时Jenkins在线但无活跃构建"
echo " → 数据基本静止,备份一致性高"
# 调用备份脚本
$BACKUP_SCRIPT
BACKUP_RESULT=$?
if [ $BACKUP_RESULT -eq 0 ]; then
echo " ✓ 备份成功完成"
else
echo " ✗ 备份失败! 错误码: $BACKUP_RESULT"
# 尝试取消关机模式让Jenkins恢复正常
echo " → 尝试恢复Jenkins正常状态..."
curl -s -X POST "$JENKINS_URL/cancelShutdown" \
--user "$ADMIN_USER:$ADMIN_PASS" > /dev/null 2>&1
exit 1
fi
# ========== Phase 4: 恢复正常模式 ==========
echo ""
echo "【Phase 4/4】取消关机模式,恢复正常..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$JENKINS_URL/cancelShutdown" \
--user "$ADMIN_USER:$ADMIN_PASS")
if [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "200" ]; then
echo " ✓ 已退出关机模式,Jenkins恢复正常运行"
else
echo " ℹ️ 返回码: $HTTP_CODE"
echo " 如果Jenkins之前已停止,启动后会自动恢复正常"
fi
echo ""
echo "========================================="
echo " ✅ 安全备份流程完成!"
echo "========================================="
echo ""
echo " 备份统计:"
ls -lh $(ls -td /backup/jenkins/daily/*/ 2>/dev/null | head -1)/*.tar.gz 2>/dev/null || \
ls -lh $(ls -td /backup/jenkins/incremental/*/ 2>/dev/null | head -1) 2>/dev/null
4.3.5 在线热备的正确做法(如果必须不停机)
bash
#!/bin/bash
# hot_backup_with_snapshot.sh - 使用LVM快照的在线热备
# 只有满足条件才使用此方法!
JENKINS_HOME="/var/lib/jenkins"
SNAPSHOT_NAME="jenkins_backup_$(date +%Y%m%d_%H%M%S)"
SNAPSHOT_SIZE="5G" # 快照大小,根据预计变更量设置
MOUNT_POINT="/mnt/jenkins_snap"
BACKUP_DIR="/backup/jenkins/hot"
echo "=== LVM快照在线热备 ==="
echo "⚠️ 注意:此方法要求JENKINS_HOME在LVM逻辑卷上"
# Step 1: 创建LVM快照(瞬间完成,不影响服务)
echo "[1/5] 创建LVM快照..."
lvcreate -L $SNAPSHOT_SIZE -s -n $SNAPSHOT_NAME /dev/vg00/jenkins_lv
if [ $? -ne 0 ]; then
echo "✗ LVM快照创建失败!请检查:"
echo " 1. JENKINS_HOME是否在LVM卷上 (df -T $JENKINS_HOME)"
echo " 2. 是否有足够的空闲空间 (vgdisplay)"
exit 1
fi
echo "✓ 快照创建成功: $SNAPSHOT_NAME"
# Step 2: 挂载快照
echo "[2/5] 挂载快照..."
mkdir -p $MOUNT_POINT
mount /dev/vg00/$SNAPSHOT_NAME $MOUNT_POINT
echo "✓ 快照已挂载到: $MOUNT_POINT"
# Step 3: 从快照备份数据(此时数据是一致的)
echo "[3/5] 从快照执行备份..."
mkdir -p "$BACKUP_DIR/$(date +%Y%m%d_%H%M%S)"
tar -czf "$BACKUP_DIR/$(date +%Y%m%d_%H%M%S)/jenkins-snapshot-backup.tar.gz" \
-C $MOUNT_POINT \
--exclude='workspace' \
--exclude='*/workspace/*' \
--exclude='*.log' \
--exclude='logs' \
--exclude='war' \
--exclude='updates' \
--exclude='caches' \
.
echo "✓ 备份完成"
# Step 4: 卸载并删除快照
echo "[4/5] 清理快照..."
umount $MOUNT_POINT
lvremove -f /dev/vg00/$SNAPSHOT_NAME
rmdir $MOUNT_POINT
echo "✓ 快照已清理"
# Step 5: 验证备份
echo "[5/5] 验证备份..."
BACKUP_FILE=$(ls -t "$BACKUP_DIR"/*.tar.gz | head -1)
if gzip -t "$BACKUP_FILE"; then
echo "✓ 备份验证通过"
ls -lh "$BACKUP_FILE"
else
echo "✗ 备份文件损坏!"
exit 1
fi
echo ""
echo "=== 在线热备完成 ==="
echo "注意:此备份与Jenkins运行状态在快照时刻一致"
4.3.6 不同场景的备份策略选择
┌─────────────────────────────────────────────────────────────────┐
│ 场景化备份策略选择 │
│ │
│ 场景A:每日自动备份(凌晨2点) │
│ ├── 推荐:Prepare for Shutdown + 全量备份 │
│ ├── 时间窗口:30分钟(含等待构建完成) │
│ └── 影响:夜间几乎无影响 │
│ │
│ 场景B:升级前的备份 │
│ ├── 推荐:完全停机 + 全量备份 │
│ ├── 时间窗口:升级维护窗口内 │
│ └── 影响:计划性停机 │
│ │
│ 场景C:高频备份(每小时) │
│ ├── 推荐:LVM快照 + 增量备份 │
│ ├── 或者:rsync + 文件锁(有风险) │
│ └── 影响:最小化 │
│ │
│ 场景D:紧急备份(故障前) │
│ ├── 推荐:尽快进入Prepare模式 + 备份 │
│ ├── 或:直接复制核心配置文件(config+secrets+credentials) │
│ └── 影响:可能中断部分构建 │
│ │
│ 场景E:迁移到新服务器 │
│ ├── 推荐:停机 + 全量备份 + 验证 │
│ ├── 时间窗口:维护窗口 │
│ └── 影响:需协调停机时间 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.4 备份时的注意事项(详细清单)
4.4.1 备份前必做检查
□ 必做检查清单:
【系统层面】
□ 磁盘空间充足(备份目标位置至少有JENKINS_HOME 1.5倍空间)
□ 网络连接稳定(如果备份到远程)
□ 系统负载正常(top/htop检查CPU、内存)
□ 备份目标存储可用(df -h / mount检查)
【Jenkins层面】
□ 当前没有长时间运行的构建(>1小时)
□ 没有正在进行的配置更改
□ 没有正在安装/更新插件
□ 没有正在进行的大规模数据导入/导出
【业务层面】
□ 已通知相关人员即将备份
□ 确认不在发布窗口期
□ 确认没有紧急构建需求
□ 记录当前Jenkins版本号(用于恢复验证)
4.4.2 备份过程中的禁忌
❌ 绝对不要做的事:
1. 备份过程中重启Jenkins
后果:备份文件可能不完整或不一致
2. 备份过程中修改Job配置
后果:config.xml和实际状态不匹配
3. 备份过程中安装/卸载插件
后果:plugins目录和plugin配置不一致
4. 备份过程中手动删除构建
后果:nextBuildNumber和实际builds不匹配
5. 备份过程中修改凭据
后果:credentials.xml和secrets不同步
6. 只备份部分文件就认为安全
后果:缺少关键依赖文件导致无法恢复
7. 备份到同一块物理磁盘
后果:磁盘损坏时同时丢失源和备份
8. 不验证就直接认为备份成功
后果:真正需要恢复时发现备份损坏
4.4.3 备份完整性验证
bash
#!/bin/bash
# comprehensive_verify.sh - 全面备份验证脚本
BACKUP_DIR="$1" # 传入备份目录路径
if [ -z "$BACKUP_DIR" ]; then
echo "用法: $0 <备份目录>"
exit 1
fi
echo "========================================="
echo " Jenkins 备份全面验证"
echo " 备份目录: $BACKUP_DIR"
echo "========================================="
ERROR_COUNT=0
WARN_COUNT=0
# 验证1: 核心文件存在性
echo ""
echo "【验证1】核心文件存在性检查..."
CORE_FILES=(
"config.xml"
"credentials.xml"
"secrets/master.key"
"secrets/hudson.util.Secret"
"secrets/master.key.pub"
)
for file in "${CORE_FILES[@]}"; do
if [ -f "$BACKUP_DIR/$file" ]; then
size=$(stat -c%s "$BACKUP_DIR/$file")
if [ $size -gt 0 ]; then
echo " ✓ $file (${size} bytes)"
else
echo " ✗ $file - 文件为空!"
((ERROR_COUNT++))
fi
else
echo " ✗ $file - 缺失!"
((ERROR_COUNT++))
fi
done
# 验证2: XML格式有效性
echo ""
echo "【验证2】XML文件格式验证..."
find "$BACKUP_DIR" -name "*.xml" -type f | while read xml_file; do
relative=${xml_file#$BACKUP_DIR/}
if xmllint --noout "$xml_file" 2>/dev/null; then
echo " ✓ $relative"
else
echo " ✗ $relative - XML格式错误!"
((ERROR_COUNT++))
fi
done
# 验证3: 密钥文件配对验证
echo ""
echo "【验证3】密钥文件配对验证..."
MASTER_KEY="$BACKUP_DIR/secrets/master.key"
SECRET_KEY="$BACKUP_DIR/secrets/hudson.util.Secret"
if [ -f "$MASTER_KEY" ] && [ -f "$SECRET_KEY" ]; then
# master.key应该是16字节的二进制或Base64
MK_SIZE=$(stat -c%s "$MASTER_KEY")
SK_SIZE=$(stat -c%s "$SECRET_KEY")
if [ $MK_SIZE -gt 0 ] && [ $SK_SIZE -gt 0 ]; then
echo " ✓ 密钥文件存在且非空"
echo " master.key: ${MK_SIZE} bytes"
echo " hudson.util.Secret: ${SK_SIZE} bytes"
# 检查master.key是否看起来像有效密钥
if [ $MK_SIZE -eq 16 ] || [ $MK_SIZE -eq 24 ] || [ $MK_SIZE -eq 32 ]; then
echo " ✓ master.key大小符合预期(AES密钥)"
else
echo " ⚠️ master.key大小异常(可能是Base64编码)"
((WARN_COUNT++))
fi
else
echo " ✗ 密钥文件为空!"
((ERROR_COUNT++))
fi
else
echo " ✗ 密钥文件缺失!"
((ERROR_COUNT++))
fi
# 验证4: Job配置完整性
echo ""
echo "【验证4】Job配置完整性..."
JOB_COUNT=$(find "$BACKUP_DIR/jobs" -maxdepth 1 -type d 2>/dev/null | wc -l)
JOB_CONFIG_COUNT=$(find "$BACKUP_DIR/jobs" -name "config.xml" 2>/dev/null | wc -l)
if [ $JOB_CONFIG_COUNT -gt 0 ]; then
echo " 发现 $((JOB_COUNT-1)) 个Job目录"
echo " 发现 $JOB_CONFIG_COUNT 个Job配置文件"
# 检查是否有Job缺少config.xml
JOBS_WITHOUT_CONFIG=0
find "$BACKUP_DIR/jobs" -maxdepth 2 -mindepth 1 -type d | while read job_dir; do
if [ ! -f "$job_dir/config.xml" ]; then
job_name=$(basename "$job_dir")
echo " ⚠️ Job [$job_name] 缺少config.xml"
((JOBS_WITHOUT_CONFIG++))
fi
done
else
echo " ⚠️ 未找到任何Job配置"
((WARN_COUNT++))
fi
# 验证5: 用户配置
echo ""
echo "【验证5】用户配置检查..."
USER_COUNT=$(find "$BACKUP_DIR/users" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l)
if [ $USER_COUNT -gt 0 ]; then
echo " ✓ 发现 $((USER_COUNT-1)) 个用户"
else
echo " ⚠️ 未找到用户目录(全新安装?)"
((WARN_COUNT++))
fi
# 验证6: 插件信息
echo ""
echo "【验证6】插件信息检查..."
if [ -d "$BACKUP_DIR/plugins" ]; then
PLUGIN_XML_COUNT=$(find "$BACKUP_DIR/plugins" -name "*.xml" 2>/dev/null | wc -l)
PLUGIN_JPI_COUNT=$(find "$BACKUP_DIR/plugins" -name "*.jpi" 2>/dev/null | wc -l)
echo " 插件XML配置: $PLUGIN_XML_COUNT 个"
echo " 插件JPI文件: $PLUGIN_JPI_COUNT 个"
if [ $PLUGIN_JPI_COUNT -eq 0 ] && [ $PLUGIN_XML_COUNT -eq 0 ]; then
echo " ⚠️ 插件目录为空"
((WARN_COUNT++))
fi
else
echo " ⚠️ plugins目录不存在"
((WARN_COUNT++))
fi
# 验证7: 备份大小合理性
echo ""
echo "【验证7】备份大小检查..."
TOTAL_SIZE=$(du -sm "$BACKUP_DIR" | cut -f1)
echo " 总大小: ${TOTAL_SIZE} MB"
if [ $TOTAL_SIZE -lt 10 ]; then
echo " ✗ 备份太小 (<10MB),可能严重缺失!"
((ERROR_COUNT++))
elif [ $TOTAL_SIZE -lt 50 ]; then
echo " ⚠️ 备份偏小 (<50MB),请确认是否包含必要数据"
((WARN_COUNT++))
else
echo " ✓ 备份大小合理"
fi
# 验证8: 文件时间戳一致性
echo ""
echo "【验证8】文件时间戳检查..."
OLDEST_FILE=$(find "$BACKUP_DIR" -type f -name "*.xml" -printf '%T@ %p\n' 2>/dev/null | sort -n | head -1 | cut -d' ' -f2-)
NEWEST_FILE=$(find "$BACKUP_DIR" -type f -name "*.xml" -printf '%T@ %p\n' 2>/dev/null | sort -n -r | head -1 | cut -d' ' -f2-)
if [ -n "$OLDEST_FILE" ] && [ -n "$NEWEST_FILE" ]; then
OLDEST_TIME=$(stat -c%y "$OLDEST_FILE" 2>/dev/null | cut -d'.' -f1)
NEWEST_TIME=$(stat -c%y "$NEWEST_FILE" 2>/dev/null | cut -d'.' -f1)
echo " 最早文件: $OLDEST_TIME"
echo " 最新文件: $NEWEST_TIME"
# 检查时间跨度是否合理(不应超过1天,除非是增量备份)
# 这里只是提醒,不算错误
fi
# 输出结果
echo ""
echo "========================================="
if [ $ERROR_COUNT -eq 0 ]; then
if [ $WARN_COUNT -eq 0 ]; then
echo " ✅ 备份验证全部通过!"
echo " 错误: 0 | 警告: 0"
else
echo " ⚠️ 备份验证通过(有警告)"
echo " 错误: 0 | 警告: $WARN_COUNT"
echo " 请检查上述警告项"
fi
else
echo " ❌ 备份验证失败!"
echo " 错误: $ERROR_COUNT | 警告: $WARN_COUNT"
echo " 请修复错误项后重新备份"
fi
echo "========================================="
exit $ERROR_COUNT
4.4.4 备份常见错误及解决
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
tar: file changed as we read it |
备份时文件被修改 | 停机后再备份或使用LVM快照 |
credentials.xml无法解密 |
secrets/未一起备份或版本不匹配 | 必须同时备份credentials.xml和整个secrets/目录 |
| 恢复后Job配置丢失 | 未备份jobs/*/config.xml | 确保备份包含所有job的config.xml |
| 恢复后插件报错 | plugins目录不完整 | 备份整个plugins/目录或记录插件列表 |
| nextBuildNumber冲突 | 备份时有构建正在进行 | 进入Prepare模式等待构建完成再备份 |
| 备份文件过大 | 包含了workspace/logs/war | 排除这些目录 |
| 备份太慢 | 文件数量多且网络慢 | 使用rsync增量或排除不必要的文件 |
| 磁盘空间不足 | 目标盘空间不够 | 清理旧备份或扩大存储 |
4.5 备份内容详细说明
必须备份的内容(一级)
bash
# 1. config.xml - Jenkins主配置
cp $JENKINS_HOME/config.xml $BACKUP_DIR/
# 2. secrets/ - 加密密钥(绝对不能丢!)
cp -r $JENKINS_HOME/secrets/ $BACKUP_DIR/secrets/
# 3. credentials.xml - 凭据存储
cp $JENKINS_HOME/credentials.xml $BACKUP_DIR/
# 4. users/ - 用户配置
cp -r $JENKINS_HOME/users/ $BACKUP_DIR/users/
# 5. jobs/*/config.xml - 所有Job配置
find $JENKINS_HOME/jobs -name "config.xml" -exec cp --parents {} $BACKUP_DIR/ \;
# 6. jobs/*/nextBuildNumber - 构建序号
find $JENKINS_HOME/jobs -name "nextBuildNumber" -exec cp --parents {} $BACKUP_DIR/ \;
建议备份的内容(二级)
bash
# 7. 最近N天的构建历史(根据磁盘空间调整)
find $JENKINS_HOME/jobs -path "*/builds/*" -mtime -7 | xargs cp --parents -t $BACKUP_DIR/
# 8. 插件清单(用于恢复时重装)
ls $JENKINS_HOME/plugins/*.jpi > $BACKUP_DIR/plugin-list.txt
# 9. 全局工具配置
cp $JENKINS_HOME/tools/*.xml $BACKUP_DIR/tools/ 2>/dev/null
4.4 不建议备份的内容
以下内容不建议备份(可在恢复后重建或重新生成):
│
├── workspace/ → 工作空间(代码检出目录,可重新checkout)
├── builds/archive/ → 旧的构建产物(占用大量空间)
├── logs/ → 日志文件(通常很大且价值有限)
├── war/ → WAR包缓存(可从官网重新下载)
├── updates/ → 更新中心缓存(可重新获取)
├── caches/ → 各种缓存(可重建)
└── fingerprints/ → 文件指纹记录(可重建,但耗时)
4.5 备份存储建议
| 存储方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 本地磁盘 | 速度快,成本低 | 单点故障 | 小规模,临时备份 |
| NAS/NFS | 共享方便,容量大 | 网络依赖 | 中等规模 |
| 对象存储(S3/OSS) | 高可靠,低成本 | 需要网络 | 推荐!云原生 |
| 异地备份 | 灾难恢复 | 成本高 | 生产环境必选 |
bash
# 示例:备份到阿里云OSS
# 安装ossutil64后:
ossutil64 cp -r $BACKUP_DIR oss://your-bucket/jenkins-backup/$DATE/ -f
# 示例:备份到AWS S3
aws s3 sync $BACKUP_DIR/ s3://your-bucket/jenkins-backup/$DATE/ --delete
# 示例:备份到远程服务器
rsync -avz -e ssh $BACKUP_DIR/ user@backup-server:/backup/jenkins/
五、Linux实时同步技术详解
核心问题:如何让JENKINS_HOME实时同步到另一个目录,有条件地过滤文件,再定时打包备份?
5.1 技术选型对比
┌─────────────────────────────────────────────────────────────────┐
│ Linux 文件夹实时同步技术对比 │
├──────────────┬────────┬────────┬────────┬────────┬──────────────┤
│ 技术 │ 实时性 │ 条件过滤│ 配置难度│ 资源占用│ 推荐场景 │
├──────────────┼────────┼────────┼────────┼────────┼──────────────┤
│ rsync │ 手动 │ ⭐⭐⭐⭐ │ 简单 │ 低 │ 定时备份 │
│ inotify+rsync│ 实时 │ ⭐⭐⭐ │ 中等 │ 中 │ 自定义方案 │
│ lsyncd │ 实时 │ ⭐⭐⭐⭐ │ 中等 │ 低 │ ⭐推荐! │
│ Unison │ 实时 │ ⭐⭐⭐ │ 复杂 │ 高 │ 双向同步 │
│ fswatch+rsync│ 实时 │ ⭐⭐⭐ │ 中等 │ 低 │ macOS/Linux │
│ csync2 │ 实时 │ ⭐⭐ │ 复杂 │ 高 │ 多节点同步 │
└──────────────┴────────┴────────┴────────┴────────┴──────────────┘
推荐方案(按优先级):
1. lsyncd(最推荐)- 成熟稳定,配置灵活,资源占用低
2. inotify+rsync - 灵活可控,适合自定义需求
3. rsync定时任务 - 最简单,适合对实时性要求不高的场景
5.2 rsync基础同步(定时触发)
bash
#!/bin/bash
# rsync_basic.sh - rsync基础同步示例
# 适用:定时任务触发(如每5分钟/每小时)
SOURCE="/var/lib/jenkins"
TARGET="/backup/jenkins/sync_mirror"
LOG_FILE="/var/log/jenkins-sync.log"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始rsync同步..."
# 基础同步命令(带条件过滤)
rsync -avz \
--delete \
--progress \
# === 有条件过滤开始 ===
# 只同步特定类型的文件
--include='*.xml' \ # 包含XML配置文件
--include='*.key' \ # 包含密钥文件
--include='*/' \ # 包含所有目录结构
--exclude='*' \ # 排除其他所有文件
# 或者使用排除法(更常用)
# --exclude='workspace/' \ # 排除工作空间
# --exclude='*/workspace/*' \ # 排除子目录的工作空间
# --exclude='*.log' \ # 排除日志文件
# --exclude='logs/' \ # 排除日志目录
# --exclude='war/' \ # 排除WAR缓存
# --exclude='updates/' \ # 排除更新缓存
# --exclude='caches/' \ # 排除缓存目录
# --exclude='fingerprints/' \ # 排除指纹记录
# --exclude='*.jar' \ # 排除JAR文件
# --exclude='*.hpi' \ # 排除插件二进制(保留xml即可)
# --exclude='*.jpi' \ # 排除插件二进制
# --max-size='10M' \ # 只同步小于10MB的文件
# --min-size='1b' \ # 排除空文件
# --bwlimit=1000 \ # 限制带宽为1000KB/s
# === 有条件过滤结束 ===
"$SOURCE/" "$TARGET/"
RSYNC_EXIT=$?
if [ $RSYNC_EXIT -eq 0 ]; then
echo "[$(date)] ✓ 同步成功"
else
echo "[$(date)] ✗ 同步失败 (退出码: $RSYNC_EXIT)"
fi
# 记录日志
echo "---" >> "$LOG_FILE"
du -sh "$TARGET" >> "$LOG_FILE"
exit $RSYNC_EXIT
设置定时同步:
bash
# 方式1: Cron定时任务(每5分钟同步一次)
crontab -e
*/5 * * * * /usr/local/bin/rsync_jenkins.sh >> /var/log/jenkins-sync.log 2>&1
# 方式2: 使用systemd timer(更现代的方式)
cat > /etc/systemd/system/jenkins-sync.service << 'EOF'
[Unit]
Description=Jenkins RSync Sync Service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/rsync_jenkins.sh
EOF
cat > /etc/systemd/system/jenkins-sync.timer << 'EOF'
[Unit]
Description=Run Jenkins sync every 5 minutes
[Timer]
OnBootSec=3min
OnUnitActiveSec=5min
AccuracySec=1min
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable jenkins-sync.timer
systemctl start jenkins-sync.timer
5.3 inotify+rsync实时同步
bash
#!/bin/bash
# inotify_rsync_sync.sh - inotify+rsync实时同步
# 原理:监控文件变化事件 → 触发rsync增量同步
SOURCE="/var/lib/jenkins"
TARGET="/backup/jenkins/sync_mirror"
LOG_FILE="/var/log/jenkins-inotify-sync.log"
# 需要安装inotify-tools
# yum install inotify-tools (CentOS/RHEL)
# apt install inotify-tools (Ubuntu/Debian)
echo "=== Jenkins inotify+rsync 实时同步 ==="
echo "源目录: $SOURCE"
echo "目标目录: $TARGET"
echo "日志文件: $LOG_FILE"
echo ""
# 创建目标目录
mkdir -p "$TARGET"
# 定义同步函数
do_sync() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] 检测到变化,开始同步..."
rsync -avz \
--delete \
# ====== 条件过滤配置 ======
--exclude='workspace/' \
--exclude='*/workspace/*' \
--exclude='*.log' \
--exclude='logs/' \
--exclude='war/' \
--exclude='updates/' \
--exclude='caches/' \
--exclude='fingerprints/' \
--exclude='*.hpi' \
--exclude='*.jpi' \
--exclude='*.jar' \
--exclude='temp/' \
--exclude='.cache/' \
# ====== 高级选项 ======
--chmod=D755,F644 \ # 设置统一权限
--checksum \ # 使用校验和比较(更准确但稍慢)
# --bwlimit=2048 \ # 限制带宽
"$SOURCE/" "$TARGET/"
local exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "[$timestamp] ✓ 同步完成"
else
echo "[$timestamp] ✗ 同步失败 (错误码: $exit_code)"
fi
}
# 防抖动:短时间内多次变化只触发一次同步
DEBOUNCE_SECONDS=5
last_sync_time=0
# 监控事件类型:
# modify - 文件被修改
# create - 文件被创建
# delete - 文件被删除
# move_to - 文件移入
# attrib - 属性变更
echo "[*] 开始监控文件变化... (Ctrl+C停止)"
inotifywait -mr \
--timefmt '%Y-%m-%d %H:%M:%S' \
--format '%T [%e] %w%f' \
-e modify,create,delete,move_to,attrib \
$SOURCE | while read line; do
current_time=$(date +%s)
# 防抖动检查
if [ $((current_time - last_sync_time)) -ge $DEBOUNCE_SECONDS ]; then
last_sync_time=$current_time
# 输出事件信息
echo "$line" >> "$LOG_FILE"
# 执行同步(后台运行避免阻塞)
do_sync >> "$LOG_FILE" 2>&1 &
fi
done
后台运行管理:
bash
# 创建systemd服务实现开机自启
cat > /etc/systemd/system/jenkins-inotify-sync.service << 'EOF'
[Unit]
Description=Jenkins Inotify Real-time Sync
After=network.target jenkins.service
Requires=jenkins.service
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/inotify_rsync_sync.sh
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable jenkins-inotify-sync.service
systemctl start jenkins-inotify-sync.service
# 查看状态
systemctl status jenkins-inotify-sync.service
journalctl -u jenkins-inotify-sync.service -f
5.4 lsyncd实时同步守护进程(⭐强烈推荐)
lua
-- /etc/lsyncd/lsyncd.conf.lua - lsyncd配置文件
-- lsyncd是基于inotify的实时同步守护进程,配置简单且功能强大
settings {
logfile = "/var/log/lsyncd/lsyncd.log",
statusFile = "/var/log/lsyncd/lsyncd.status",
statusInterval = 10,
nodaemon = false,
maxProcesses = 4, -- 最大并发进程数
maxDelays = 5, -- 最大延迟秒数(防抖动)
}
-- ========== Jenkins主同步 ==========
sync {
default.rsync,
-- 源和目标
source = "/var/lib/jenkins",
target = "/backup/jenkins/sync_mirror",
-- ====== 核心配置 ======
delay = 5, -- 延迟5秒后同步(防抖动)
delete = true, -- 删除源文件时也删除目标
rsync = {
-- ====== 条件过滤(重点!)======
_extra = {
-- 排除不需要的目录
"--exclude=workspace/",
"--exclude=*/workspace/*",
"--exclude=*.log",
"--exclude=logs/",
"--exclude=war/",
"--exclude=updates/",
"--exclude=caches/",
"--exclude=fingerprints/",
"--exclude=temp/",
"--exclude=.cache/",
-- 排除大型二进制文件(插件二进制可从插件中心重新下载)
"--exclude=*.hpi",
"--exclude=*.jpi",
"--exclude=*.jar",
"--exclude=*.zip",
"--exclude=*.tar.gz",
-- 文件大小限制
"--max-size=50M", -- 不同步超过50MB的文件
"--min-size=1b", -- 排除空文件
-- 其他优化选项
"--checksum", -- 使用校验和比较
"--chmod=D755,F644", -- 统一权限
"--group", -- 保持组
"--owner", -- 保持所有者
"--times", -- 保持时间戳
"--verbose", -- 详细输出
"--bwlimit=2048", -- 限制带宽2MB/s(可选)
},
-- rsync SSH配置(如果目标是远程服务器)
-- rsh = "/usr/bin/ssh -p 22 -i /root/.ssh/id_rsa",
},
}
-- ========== 可选:同时同步到远程备份服务器 ==========
-- sync {
-- default.rsync,
-- source = "/var/lib/jenkins",
-- target = "backup-user@192.168.1.100:/backup/jenkins/remote_mirror",
-- delay = 30, -- 远程同步延迟长一些
-- delete = true,
-- rsync = {
-- _extra = {
-- "--exclude=workspace/",
-- "--exclude=logs/",
-- "--exclude=war/",
-- "--exclude=updates/",
-- "--exclude=caches/",
-- "--exclude=*.hpi",
-- "--exclude=*.jpi",
-- "--max-size=50M",
-- },
-- rsh = "/usr/bin/ssh -p 22 -i /root/.ssh/backup_key",
-- },
-- }
安装和启动lsyncd:
bash
# 1. 安装lsyncd
# CentOS/RHEL:
yum install epel-release -y
yum install lsyncd -y
# Ubuntu/Debian:
apt update
apt install lsyncd -y
# 2. 创建必要的目录
mkdir -p /var/log/lsyncd
mkdir -p /etc/lsyncd
mkdir -p /backup/jenkins/sync_mirror
# 3. 编辑配置文件
vim /etc/lsyncd/lsyncd.conf.lua
# (粘贴上面的配置内容)
# 4. 测试配置语法
lsyncd -nodaemon /etc/lsyncd/lsyncd.conf.lua
# Ctrl+C 停止测试
# 5. 启动服务
systemctl enable lsyncd
systemctl start lsyncd
# 6. 检查状态
systemctl status lsyncd
tail -f /var/log/lsyncd/lsyncd.log
# 7. 查看同步状态
cat /var/log/lsyncd/lsyncd.status
5.5 条件过滤与高级配置详解
5.5.1 常用过滤规则
bash
# ====== rsync/lsyncd 过滤规则速查表 ======
【排除特定扩展名】
--exclude='*.log' # 所有日志文件
--exclude='*.tmp' # 临时文件
--exclude='*.bak' # 备份文件
--exclude='*.swp' # Vim交换文件
--exclude='*.hpi' # Jenkins插件二进制
--exclude='*.jpi' # Jenkins插件二进制
【排除整个目录】
--exclude='workspace/' # 工作空间
--exclude='logs/' # 日志目录
--exclude='war/' # WAR缓存
--exclude='updates/' # 更新缓存
--exclude='caches/' # 缓存目录
【排除子目录中的同名目录】
--exclude='*/workspace/*' # 所有层级下的workspace
【包含模式】(配合--exclude='*'使用)
--include='*.xml' # 只包含XML文件
--include='*.key' # 只包含密钥文件
--include='*/' # 必须包含目录结构!
【文件大小限制】
--max-size='10M' # 不同步大于10MB的文件
--min-size='1b' # 排除空文件
【时间限制】
--max-age=7 # 不同步7天前的文件
--min-age=1 # 不同步1天内的新文件(用于冷数据归档)
【高级过滤】
--filter='- */workspace/' # 更强大的过滤语法
--filter='+ *.xml'
--filter='- *'
【正则表达式过滤】(需要配合其他工具)
# find + rsync 组合
find /source -type f \( -name "*.xml" -o -name "*.key" \) | rsync --files-from=- ...
5.5.2 Jenkins专用过滤配置
lua
-- lsyncd Jenkins最佳过滤配置
sync {
default.rsync,
source = "/var/lib/jenkins",
target = "/backup/jenkins/sync_mirror",
delay = 10,
delete = true,
rsync = {
_extra = {
-- 【一级:绝对排除(大且无用)】
"--exclude=workspace/",
"--exclude=*/workspace/*",
"--exclude=logs/",
"--exclude=war/",
"--exclude=updates/",
"--exclude=caches/",
-- 【二级:建议排除(可重建)】
"--exclude=fingerprints/",
"--exclude=temp/",
"--exclude=.cache/",
"--exclude=modules/",
-- 【三级:可选排除(节省空间)】
"--exclude=*.hpi", # 插件二进制(约500MB+)
"--exclude=*.jpi", # 插件二进制
"--exclude=*.jar", # JAR包
"--exclude=*.zip",
"--exclude=*.tar.gz",
"--exclude=*.gz",
-- 【四级:大小限制】
"--max-size=20M", # 单文件不超过20MB
-- 【优化参数】
"--checksum",
"--compress", # 传输压缩
"--human-readable",
},
},
}
5.5.3 多目标同步策略
lua
-- 场景:本地镜像 + 远程备份 + 定时归档
-- 目标1:本地实时镜像(最快恢复)
sync {
default.rsync,
source = "/var/lib/jenkins",
target = "/backup/jenkins/mirror_local",
delay = 5,
delete = true,
rsync = { _extra = {
"--exclude=workspace/", "--exclude=logs/",
"--exclude=war/", "--exclude=updates/",
"--exclude=caches/", "--exclude=*.hpi",
"--max-size=50M",
}},
}
-- 目标2:远程服务器(异地容灾)
sync {
default.rsync,
source = "/var/lib/jenkins",
target = "backup@remote-server:/data/jenkins_backup",
delay = 60, -- 远程同步间隔长一点
delete = true,
rsync = {
_extra = {
"--exclude=workspace/", "--exclude=logs/",
"--exclude=war/", "--exclude=updates/",
"--exclude=caches/", "--exclude=*.hpi",
"--max-size=50M",
"--compress-level=9", # 远程高压缩
"--partial", -- 支持断点续传
"--progress",
},
rsh = "/usr/bin/ssh -i /home/backup/.ssh/id_rsa -p 22",
},
}
5.6 完整方案:实时同步 + 定时备份
┌─────────────────────────────────────────────────────────────────┐
│ 推荐架构:实时同步 + 定时备份 │
│ │
│ ┌─────────────┐ │
│ │ JENKINS_HOME│ │
│ │ (源目录) │ │
│ └──────┬──────┘ │
│ │ │
│ │ lsyncd 实时同步(延迟5秒,带过滤) │
│ ▼ │
│ ┌─────────────┐ 定时任务(每天凌晨2点) │
│ │ 本地镜像 │ ──────────────────► ┌─────────────┐ │
│ │ /backup/ │ │ 归档备份 │ │
│ │ mirror/ │ │ /backup/daily│ │
│ └─────────────┘ │ /20260425.tar │ │
│ ↑ └──────┬──────┘ │
│ │ │ │
│ │ 恢复时直接复制(秒级) │ 异地上传 │
│ │ rsync/scp/OSS │
└─────────┼────────────────────────────────────┼───────────────────┘
│ │
▼ ▼
秒级恢复RTO 灾难恢复RPO
5.6.1 完整部署脚本
bash
#!/bin/bash
# deploy_jenkins_sync_system.sh - 一键部署完整同步+备份系统
set -e
echo "========================================="
echo " Jenkins 实时同步 + 定时备份系统部署"
echo "========================================="
# ========== 配置区 ==========
JENKINS_HOME="/var/lib/jenkins"
MIRROR_DIR="/backup/jenkins/sync_mirror"
ARCHIVE_DIR="/backup/jenkins/archive"
LOG_DIR="/var/log/jenkins-backup"
RETENTION_DAYS=30
# ========== Step 1: 安装依赖 ==========
echo ""
echo "[Step 1/6] 安装必要软件..."
if command -v yum &>/dev/null; then
yum install -y epel-release lsyncd rsync inotify-tools
elif command -v apt &>/dev/null; then
apt update && apt install -y lsyncd rsync inotify-tools
else
echo "不支持的包管理器"
exit 1
fi
echo "✓ 软件安装完成"
# ========== Step 2: 创建目录结构 ==========
echo ""
echo "[Step 2/6] 创建目录结构..."
mkdir -p "$MIRROR_DIR"
mkdir -p "$ARCHIVE_DIR/daily"
mkdir -p "$ARCHIVE_DIR/weekly"
mkdir -p "$LOG_DIR"
mkdir -p /var/log/lsyncd
mkdir -p /etc/lsyncd
echo "✓ 目录创建完成"
# ========== Step 3: 配置lsyncd实时同步 ==========
echo ""
echo "[Step 3/6] 配置lsyncd实时同步..."
cat > /etc/lsyncd/lsyncd.conf.lua << 'LSYNCD_CONF'
settings {
logfile = "/var/log/lsyncd/lsyncd.log",
statusFile = "/var/log/lsyncd/lsyncd.status",
statusInterval = 10,
nodaemon = false,
maxProcesses = 4,
maxDelays = 5,
}
sync {
default.rsync,
source = "/var/lib/jenkins",
target = "/backup/jenkins/sync_mirror",
delay = 5,
delete = true,
rsync = {
_extra = {
"--exclude=workspace/",
"--exclude=*/workspace/*",
"--exclude=*.log",
"--exclude=logs/",
"--exclude=war/",
"--exclude=updates/",
"--exclude=caches/",
"--exclude=fingerprints/",
"--exclude=temp/",
"--exclude=.cache/",
"--exclude=*.hpi",
"--exclude=*.jpi",
"--exclude=*.jar",
"--exclude=*.zip",
"--exclude=*.tar.gz",
"--max-size=50M",
"--min-size=1b",
"--checksum",
"--chmod=D755,F644",
},
},
}
LSYNCD_CONF
echo "✓ lsyncd配置完成"
# ========== Step 4: 创建定时归档脚本 ==========
echo ""
echo "[Step 4/6] 创建定时归档备份脚本..."
cat > /usr/local/bin/jenkins_archive_backup.sh << 'ARCHIVE_SCRIPT'
#!/bin/bash
# jenkins_archive_backup.sh - 从本地镜像创建归档备份
SOURCE="/backup/jenkins/sync_mirror"
ARCHIVE_DIR="/backup/jenkins/archive"
DATE=$(date +%Y%m%d_%H%M%S)
DAY_OF_WEEK=$(date +%u) # 1-7, 7=周日
RETENTION_DAYS=30
RETENTION_WEEKS=8
echo "[$(date)] 开始归档备份..."
if [ ! -d "$SOURCE" ] || [ -z "$(ls -A $SOURCE)" ]; then
echo "[ERROR] 镜像目录为空或不存在!"
exit 1
fi
# 每周日创建全量备份,其他时间创建增量备份
if [ "$DAY_OF_WEEK" = "7" ]; then
BACKUP_TYPE="full"
TARGET="$ARCHIVE_DIR/weekly/$DATE"
mkdir -p "$(dirname $TARGET)"
tar -czf "$TARGET-full.tar.gz" -C "$SOURCE" .
echo "✓ 全量备份完成: $TARGET-full.tar.gz"
else
BACKUP_TYPE="incremental"
TARGET="$ARCHIVE_DIR/daily/$DATE"
mkdir -p "$TARGET"
# 使用rsync做增量快照(硬链接)
LATEST=$(ls -td $ARCHIVE_DIR/daily/*/ 2>/dev/null | head -1)
if [ -n "$LATEST" ]; then
cp -al "$LATEST" "$TARGET"
rsync -az --delete "$SOURCE/" "$TARGET/"
else
rsync -az "$SOURCE/" "$TARGET/"
fi
echo "✓ 增量备份完成: $TARGET"
fi
# 清理过期备份
find "$ARCHIVE_DIR/daily" -maxdepth 1 -type d -mtime +$RETENTION_DAYS -exec rm -rf {} \; 2>/dev/null
find "$ARCHIVE_DIR/weekly" -name "*.tar.gz" -mtime +$((RETENTION_WEEKS * 7)) -delete 2>/dev/null
# 统计信息
TOTAL_SIZE=$(du -sh $ARCHIVE_DIR | cut -f1)
echo "[$(date)] 归档完成。总大小: $TOTAL_SIZE"
ARCHIVE_SCRIPT
chmod +x /usr/local/bin/jenkins_archive_backup.sh
echo "✓ 归档脚本创建完成"
# ========== Step 5: 配置定时任务 ==========
echo ""
echo "[Step 5/6] 配置定时任务..."
# 归档备份:每天凌晨2点
(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/jenkins_archive_backup.sh >> $LOG_DIR/archive.log 2>&1") | crontab -
# 清理旧日志:每周日凌晨3点
(crontab -l 2>/dev/null; echo "0 3 * * 0 find $LOG_DIR -name '*.log' -mtime +30 -delete") | crontab -
# lsyncd状态检查:每小时
(crontab -l 2>/dev/null; echo "0 * * * * systemctl is-active lsyncd >> $LOG_DIR/lsyncd-status.log 2>&1 || (echo 'lsyncd down at $(date)' | mail -s 'ALERT: lsyncd down' admin@example.com)") | crontab -
echo "✓ 定时任务配置完成"
crontab -l | grep -E "(jenkins_archive|lsyncd)"
# ========== Step 6: 启动服务 ==========
echo ""
echo "[Step 6/6] 启动服务..."
systemctl enable lsyncd
systemctl restart lsyncd
sleep 2
if systemctl is-active --quiet lsyncd; then
echo "✓ lsyncd已启动并运行"
else
echo "✗ lsyncd启动失败!"
journalctl -u lsyncd -n 20 --no-pager
exit 1
fi
# ========== 完成 ==========
echo ""
echo "========================================="
echo " ✅ 部署完成!"
echo "========================================="
echo ""
echo "系统架构:"
echo " 源目录: $JENKINS_HOME"
echo " 实时镜像: $MIRROR_DIR (lsyncd, 5秒延迟)"
echo " 归档备份: $ARCHIVE_DIR (每天凌晨2点)"
echo " 日志目录: $LOG_DIR"
echo ""
echo "常用命令:"
echo " 查看同步状态: tail -f /var/log/lsyncd/lsyncd.log"
echo " 查看镜像大小: du -sh $MIRROR_DIR"
echo " 手动触发归档: /usr/local/bin/jenkins_archive_backup.sh"
echo " 重启lsyncd: systemctl restart lsyncd"
echo ""
echo "恢复流程(秒级):"
echo " 1. systemctl stop jenkins"
echo " 2. rsync -avz $MIRROR_DIR/ $JENKINS_HOME/"
echo " 3. chown -R jenkins:jenkins $JENKINS_HOME"
echo " 4. systemctl start jenkins"
5.6.2 运维监控脚本
bash
#!/bin/bash
# monitor_sync_status.sh - 同步系统健康检查
MIRROR_DIR="/backup/jenkins/sync_mirror"
SOURCE="/var/lib/jenkins"
LOG_FILE="/var/log/jenkins-backup/monitor.log"
echo "=== Jenkins 同步系统健康检查 ==="
echo "检查时间: $(date)"
echo ""
ALL_OK=true
# 检查1: lsyncd服务状态
echo "[1/5] lsyncd服务状态..."
if systemctl is-active --quiet lsyncd; then
STATUS=$(systemctl show lsyncd -p ActiveState --value)
UPTIME=$(systemctl show lsyncd -p ActiveEnterTimestamp --value | cut -d' ' -f1-4)
echo " ✓ 状态: $STATUS (运行时长: $UPTIME)"
else
echo " ✗ lsyncd未运行!"
ALL_OK=false
fi
# 检查2: 最近同步时间
echo ""
echo "[2/5] 最近同步活动..."
if [ -f /var/log/lsyncd/lsyncd.log ]; then
LAST_SYNC=$(tail -1 /var/log/lsyncd/lsyncd.log 2>/dev/null | grep -oP '\d{4}-\d{2}-\d{2}.*')
if [ -n "$LAST_SYNC" ]; then
echo " ✓ 最后同步: $LAST_SYNC"
else
echo " ⚠️ 无法获取最后同步时间"
fi
else
echo " ⚠️ 日志文件不存在"
fi
# 检查3: 目录大小对比
echo ""
echo "[3/5] 目录大小对比..."
SRC_SIZE=$(du -sm $SOURCE 2>/dev/null | cut -f1)
MIR_SIZE=$(du -sm $MIRROR_DIR 2>/dev/null | cut -f1)
echo " 源目录: ${SRC_SIZE}MB"
echo " 镜像: ${MIR_SIZE}MB"
if [ -n "$SRC_SIZE" ] && [ -n "$MIR_SIZE" ]; then
DIFF=$((SRC_SIZE - MIR_SIZE))
DIFF_ABS=${DIFF#-} # 取绝对值
PCT=$((DIFF_ABS * 100 / SRC_SIZE))
if [ $PCT -lt 5 ]; then
echo " ✓ 大小一致 (差异: ${PCT}%)"
elif [ $PCT -lt 20 ]; then
echo " ⚠️ 存在一定差异 (${PCT}%),可能正在同步中"
else
echo " ✗ 差异过大 (${PCT}%)! 可能同步异常"
ALL_OK=false
fi
fi
# 检查4: 关键文件存在性
echo ""
echo "[4/5] 关键文件检查..."
for f in config.xml credentials.xml secrets/master.key; do
if [ -f "$MIRROR_DIR/$f" ] || [ -d "$MIRROR_DIR/$f" ]; then
echo " ✓ $f"
else
echo " ✗ $f 缺失!"
ALL_OK=false
fi
done
# 检查5: 归档备份情况
echo ""
echo "[5/5] 归档备份情况..."
LATEST_ARCHIVE=$(ls -td /backup/jenkins/archive/daily/*/ 2>/dev/null | head -1)
if [ -n "$LATEST_ARCHIVE" ]; then
ARCH_TIME=$(stat -c%y $LATEST_ARCHIVE | cut -d'.' -f1)
ARCH_SIZE=$(du -sm $LATEST_ARCHIVE | cut -f1)
echo " ✓ 最新归档: $ARCH_TIME (${ARCH_SIZE}MB)"
else
echo " ⚠️ 未找到归档备份"
fi
# 结果汇总
echo ""
if $ALL_OK; then
echo "========================================="
echo " ✅ 同步系统运行正常"
echo "========================================="
EXIT_CODE=0
else
echo "========================================="
echo " ❌ 发现问题,请检查上述项目!"
echo "========================================="
EXIT_CODE=1
fi
# 记录日志
echo "--- $(date) ---" >> "$LOG_FILE"
echo "结果: $EXIT_CODE" >> "$LOG_FILE"
exit $EXIT_CODE
六、自动化备份脚本
6.1 设置定时备份任务
Linux Cron
bash
# 编辑crontab
crontab -e
# 添加定时任务:
# 每天凌晨2点执行备份
0 2 * * * /usr/local/bin/backup_jenkins.sh >> /var/log/jenkins-backup.log 2>&1
# 每周日凌晨2点执行全量备份(脚本内部判断)
0 2 * * 0 /usr/local/bin/backup_jenkins.sh --full >> /var/log/jenkins-backup.log 2>&1
Windows 计划任务
powershell
# 创建每日备份计划任务
$action = New-ScheduledTaskAction `
-Execute 'powershell.exe' `
-Argument '-ExecutionPolicy Bypass -File C:\Scripts\backup_jenkins.ps1'
$trigger = New-ScheduledTaskTrigger -Daily -At '02:00AM'
$settings = New-ScheduledTaskSettingsSet `
-StartWhenAvailable `
-DontStopIfGoingOnBatteries
Register-ScheduledTask `
-TaskName 'JenkinsDailyBackup' `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-User 'SYSTEM' `
-Description 'Jenkins daily backup at 2 AM' `
-Force
6.2 备份前自动安全停机
bash
#!/bin/bash
# safe_backup.sh - 带安全停机的备份脚本
JENKINS_URL="http://localhost:8080"
ADMIN_USER="admin"
ADMIN_PASS="password"
BACKUP_SCRIPT="/usr/local/bin/backup_jenkins.sh"
echo "=== Jenkins 安全备份流程 ==="
# 步骤1:进入准备关机模式
echo "[1/4] 通知Jenkins进入准备关机模式..."
curl -s -X POST "$JENKINS_URL/prepareShutdown" \
--user "$ADMIN_USER:$ADMIN_PASS" \
-o /dev/null -w "%{http_code}"
echo ""
# 步骤2:等待构建完成(最多等待30分钟)
echo "[2/4] 等待正在运行的构建完成..."
timeout=1800 # 30分钟
elapsed=0
interval=30
while [ $elapsed -lt $timeout ]; do
busy=$(curl -s "$JENKINS_URL/api/json?tree=busyExecutors" \
--user "$ADMIN_USER:$ADMIN_PASS" | grep -o '"busyExecutors":[0-9]*' | grep -o '[0-9]*')
if [ "$busy" -eq 0 ]; then
echo " 所有构建已完成!"
break
fi
echo " 还有 $busy 个构建正在运行... (已等待 ${elapsed}s)"
sleep $interval
elapsed=$((elapsed + interval))
done
if [ $elapsed -ge $timeout ]; then
echo " ⚠️ 超时!仍有构建在运行,强制继续备份..."
fi
# 步骤3:执行备份
echo "[3/4] 执行备份..."
$BACKUP_SCRIPT
# 步骤4:取消关机模式(如果Jenkins还在运行)
echo "[4/4] 取消关机模式..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$JENKINS_URL/cancelShutdown" \
--user "$ADMIN_USER:$ADMIN_PASS")
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ]; then
echo " ✓ 已取消关机模式,Jenkins恢复正常"
else
echo " ℹ️ Jenkins可能已经停止,启动后需手动确认"
fi
echo "=== 备份流程完成 ==="
6.3 备份验证脚本
bash
#!/bin/bash
# verify_backup.sh - 备份验证脚本
BACKUP_DIR="/backup/jenkins"
echo "=== Jenkins 备份验证 ==="
# 检查1:必需文件是否存在
echo ""
echo "[检查1] 必需文件完整性..."
required_files=(
"config.xml"
"credentials.xml"
"secrets/master.key"
"secrets/hudson.util.Secret"
)
all_ok=true
for file in "${required_files[@]}"; do
if [ -f "$BACKUP_DIR/latest/$file" ] || [ -d "$BACKUP_DIR/latest/$file" ]; then
echo " ✓ $file"
else
echo " ✗ $file - 缺失!"
all_ok=false
fi
done
# 检查2:XML格式有效性
echo ""
echo "[检查2] XML文件格式验证..."
for xml_file in $(find "$BACKUP_DIR/latest" -name "*.xml" -type f); do
if xmllint --noout "$xml_file" 2>/dev/null; then
echo " ✓ $(basename $xml_file)"
else
echo " ✗ $(basename $xml_file) - 格式错误!"
all_ok=false
fi
done
# 检查3:密钥文件大小
echo ""
echo "[检查3] 密钥文件大小检查..."
master_key="$BACKUP_DIR/latest/secrets/master.key"
if [ -f "$master_key" ]; then
size=$(stat -c%s "$master_key")
if [ $size -gt 0 ]; then
echo " ✓ master.key (${size} bytes)"
else
echo " ✗ master.key - 文件为空!"
all_ok=false
fi
fi
# 检查4:备份大小合理性
echo ""
echo "[检查4] 备份大小检查..."
total_size=$(du -sm "$BACKUP_DIR/latest" | cut -f1)
if [ $total_size -gt 10 ]; then
echo " ✓ 总大小: ${total_size}MB (正常)"
elif [ $total_size -gt 0 ]; then
echo " ⚠️ 总大小: ${total_size}MB (偏小,请检查)"
else
echo " ✗ 备份为空!"
all_ok=false
fi
# 输出结果
echo ""
if $all_ok; then
echo "========================================="
echo "✓ 备份验证通过!备份完整有效。"
echo "========================================="
exit 0
else
echo "========================================="
echo "✗ 备份验证失败!请检查上述问题。"
echo "========================================="
exit 1
fi
七、数据恢复
7.1 从备份恢复的标准流程
┌─────────────────────────────────────────────────────────────────┐
│ 数据恢复标准流程 │
│ │
│ 前提条件: │
│ ├── 已有有效的完整备份 │
│ ├── 已安装相同版本的Jenkins │
│ └── 已准备好JENKINS_HOME目录 │
│ │
│ 步骤: │
│ │
│ Step 1: 停止Jenkins服务 │
│ systemctl stop jenkins │
│ │
│ Step 2: 备份当前JENKINS_HOME(防止误操作) │
│ mv $JENKINS_HOME $JENKINS_HOME.bak.$(date) │
│ │
│ Step 3: 从备份恢复数据 │
│ rsync -av $BACKUP_DIR/latest/ $JENKINS_HOME/ │
│ │
│ Step 4: 修复权限 │
│ chown -R jenkins:jenkins $JENKINS_HOME │
│ chmod -R 755 $JENKINS_HOME │
│ │
│ Step 5: 启动Jenkins │
│ systemctl start jenkins │
│ │
│ Step 6: 验证恢复结果 │
│ - 检查Job配置是否完整 │
│ - 验证凭据是否可用 │
│ - 测试一个简单构建 │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 详细恢复脚本
bash
#!/bin/bash
# restore_jenkins.sh - Jenkins数据恢复脚本
set -e # 遇错即停
# ========== 配置区 ==========
JENKINS_HOME="/var/lib/jenkins"
BACKUP_SOURCE="/backup/jenkins/daily/20260425_020000" # 修改为实际备份路径
JENKINS_SERVICE="jenkins"
JENKINS_USER="jenkins"
# ========== 预检 ==========
echo "=== Jenkins 数据恢复 ==="
echo ""
echo "[预检1] 检查备份源..."
if [ ! -d "$BACKUP_SOURCE" ]; then
echo "✗ 备份目录不存在: $BACKUP_SOURCE"
exit 1
fi
echo "✓ 备份目录存在: $BACKUP_SOURCE"
echo "[预检2] 检查Jenkins服务状态..."
if systemctl is-active --quiet $JENKINS_SERVICE; then
echo "⚠️ Jenkins正在运行,将自动停止..."
else
echo "✓ Jenkins已停止"
fi
echo "[预检3] 检查磁盘空间..."
available_gb=$(df -BG $JENKINS_HOME | tail -1 | awk '{print $4}' | tr -d 'G')
backup_size_gb=$(du -sm $BACKUP_SOURCE | cut -f1)
backup_size_gb=$(( (backup_size_gb + 1023) / 1024 )) # 向上取整到GB
echo " 可用空间: ${available_gb}GB, 需要: ~${backup_size_gb}GB"
if [ "$available_gb" -lt "$backup_size_gb" ]; then
echo "✗ 磁盘空间不足!"
exit 1
fi
echo "✓ 磁盘空间充足"
read -p "确认要恢复到此备份吗?(yes/no): " confirm
if [ "$confirm" != "yes" ]; then
echo "已取消恢复操作。"
exit 0
fi
# ========== 执行恢复 ==========
echo ""
echo "========== 开始恢复 =========="
# Step 1: 停止Jenkins
echo "[Step 1/6] 停止Jenkins服务..."
systemctl stop $JENKINS_SERVICE
sleep 5
if systemctl is-active --quiet $JENKINS_SERVICE; then
echo "⚠️ Jenkins未完全停止,尝试强制停止..."
systemctl kill $JENKINS_SERVICE
sleep 3
fi
echo "✓ Jenkins已停止"
# Step 2: 备份当前数据(安全措施)
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
CURRENT_BACKUP="${JENKINS_HOME}.bak.${TIMESTAMP}"
echo "[Step 2/6] 备份当前JENKINS_HOME到 ${CURRENT_BACKUP}..."
mv "$JENKINS_HOME" "$CURRENT_BACKUP"
echo "✓ 当前数据已备份"
# Step 3: 创建新的JENKINS_HOME
echo "[Step 3/6] 创建新的JENKINS_HOME..."
mkdir -p "$JENKINS_HOME"
echo "✓ 目录已创建"
# Step 4: 恢复备份数据
echo "[Step 4/6] 从备份恢复数据(这可能需要一些时间)..."
rsync -av --progress "$BACKUP_SOURCE/" "$JENKINS_HOME/"
echo "✓ 数据恢复完成"
# Step 5: 修复权限
echo "[Step 5/6] 修复文件权限..."
chown -R ${JENKINS_USER}:${JENKINS_USER} "$JENKINS_HOME"
chmod -R u+rwX,g+rX,o-rwx "$JENKINS_HOME"
chmod -R o+r "$JENKINS_HOME/jobs" # jobs需要可读以便Agent访问
echo "✓ 权限已修复"
# Step 6: 启动Jenkins
echo "[Step 6/6] 启动Jenkins服务..."
systemctl start $JENKINS_SERVICE
sleep 10
if systemctl is-active --quiet $JENKINS_SERVICE; then
echo "✓ Jenkins启动成功"
else
echo "✗ Jenkins启动失败!请检查日志: journalctl -u jenkins -f"
exit 1
fi
# ========== 后验 ==========
echo ""
echo "========== 恢复后验证 =========="
echo "[验证1] 检查核心配置..."
for f in config.xml credentials.xml; do
if [ -f "$JENKINS_HOME/$f" ]; then
echo " ✓ $f 存在"
else
echo " ✗ $f 缺失!"
fi
done
echo "[验证2] 检查密钥..."
if [ -f "$JENKINS_HOME/secrets/master.key" ]; then
echo " ✓ master.key 存在"
else
echo " ✗ master.key 缺失! 凭据可能无法解密"
fi
echo "[验证3] 统计Job数量..."
job_count=$(find "$JENKINS_HOME/jobs" -maxdepth 1 -type d | wc -l)
echo " 共恢复 $job_count 个Job"
echo ""
echo "========================================="
echo "✓ 数据恢复完成!"
echo "========================================="
echo ""
echo "下一步操作:"
echo " 1. 浏览器打开 Jenkins Web界面"
echo " 2. 检查Job配置是否正确"
echo " 3. 验证凭据是否可用"
echo " 4. 运行一个测试构建"
echo ""
echo "如遇问题,原数据备份在: $CURRENT_BACKUP"
echo " 回滚命令: mv $CURRENT_BACKUP $JENKINS_HOME && systemctl restart jenkins"
7.3 部分恢复(只恢复特定Job)
bash
#!/bin/bash
# restore_single_job.sh - 恢复单个Job
JOB_NAME="my-important-job"
BACKUP_SOURCE="/backup/jenkins/daily/20260425_020000"
JENKINS_HOME="/var/lib/jenkins"
echo "恢复Job: $JOB_NAME"
# 从备份中复制Job配置
if [ -d "$BACKUP_SOURCE/jobs/$JOB_NAME" ]; then
cp -r "$BACKUP_SOURCE/jobs/$JOB_NAME" "$JENKINS_HOME/jobs/"
chown -R jenkins:jenkins "$JENKINS_HOME/jobs/$JOB_NAME"
echo "✓ Job $JOB_NAME 已恢复"
# 重新加载配置(无需重启)
curl -X POST 'http://localhost:8080/reload' \
--user admin:password
echo "✓ Jenkins配置已重新加载"
else
echo "✗ 在备份中未找到Job: $JOB_NAME"
exit 1
fi
7.4 迁移到新服务器
bash
#!/bin/bash
# migrate_jenkins.sh - Jenkins迁移脚本
OLD_SERVER="old-jenkins.example.com"
NEW_SERVER="new-jenkins.example.com"
JENKINS_HOME="/var/lib/jenkins"
BACKUP_FILE="/tmp/jenkins-migration-backup.tar.gz"
echo "=== Jenkins 迁移流程 ==="
# Step 1: 在旧服务器上打包备份
echo "[1/5] 在旧服务器($OLD_SERVER)上创建备份..."
ssh $OLD_SERVER "
cd $JENKINS_HOME
tar -czf $BACKUP_FILE \
--exclude='workspace' \
--exclude='*/workspace' \
--exclude='*.log' \
--exclude='logs' \
--exclude='war' \
--exclude='updates' \
--exclude='caches' \
config.xml credentials.xml secrets users jobs plugins tools
ls -lh $BACKUP_FILE
"
# Step 2: 传输备份到新服务器
echo "[2/5] 传输备份到新服务器($NEW_SERVER)..."
scp $OLD_SERVER:$BACKUP_FILE $NEW_SERVER:$BACKUP_FILE
# Step 3: 在新服务器上解压
echo "[3/5] 在新服务器上解压备份..."
ssh $NEW_SERVER "
sudo systemctl stop jenkins
sudo mv $JENKINS_HOME ${JENKINS_HOME}.orig
sudo mkdir -p $JENKINS_HOME
sudo tar -xzf $BACKUP_FILE -C $JENKINS_HOME
sudo chown -R jenkins:jenkins $JENKINS_HOME
sudo chmod -R 755 $JENKINS_HOME
"
# Step 4: 更新配置(如有必要)
echo "[4/5] 更新配置..."
ssh $NEW_SERVER "
# 如果URL变更,需要修改config.xml中的jenkinsUrl
# sed -i 's|old-url|new-url|g' $JENKINS_HOME/config.xml
# 如果Agent连接地址变更
# sed -i 's|old-host|new-host|g' $JENKINS_HOME/config.xml
echo '配置更新完成(如需要请手动调整)'
"
# Step 5: 启动新服务器
echo "[5/5] 启动新服务器上的Jenkins..."
ssh $NEW_SERVER "sudo systemctl start jenkins && sudo systemctl status jenkins"
echo ""
echo "✓ 迁移完成!请访问新服务器验证:http://$NEW_SERVER:8080"
八、高可用场景下的维护
8.1 滚动升级(零停机)
┌─────────────────────────────────────────────────────────────────┐
│ 滚动升级流程(双Master架构) │
│ │
│ 初始状态: │
│ ┌──────────┐ ┌──────────┐ │
│ │ Master-A │◄───────►│ Master-B │ (共享存储) │
│ │ ACTIVE │ │ STANDBY │ │
│ └──────────┘ └──────────┘ │
│ │
│ 升级步骤: │
│ │
│ Step 1: 将流量切换到Master-B │
│ ├── 修改LB规则,将Master-B设为Active │
│ ├── 等待现有请求处理完毕(约30秒) │
│ └── Master-A进入Standby模式 │
│ │
│ Step 2: 升级Master-A │
│ ├── 停止Master-A服务 │
│ ├── 备份当前版本(回滚用) │
│ ├── 执行升级(WAR包/插件/JDK) │
│ └── 启动Master-A,验证健康 │
│ │
│ Step 3: 将流量切回Master-A(或保持Master-B) │
│ └── 完成滚动升级 │
│ │
│ 优势:零停机,用户体验无感知 │
│ 前提:共享存储 + 负载均衡 + 至少2个Master │
│ │
└─────────────────────────────────────────────────────────────────┘
8.1.1 核心问题:如何实现流量切换?
┌─────────────────────────────────────────────────────────────────┐
│ 流量切换的三层机制 │
│ │
│ Layer 1: 负载均衡器(LB)层面 │
│ ├── Nginx / HAProxy / F5 / AWS ALB │
│ ├── 控制外部请求路由到哪个Master │
│ └── 最外层的流量开关 │
│ │
│ Layer 2: Jenkins Prepare for Shutdown │
│ ├── 让Jenkins不再接受新构建任务 │
│ ├── 正在运行的构建继续完成 │
│ └── Agent连接自动转移到Active节点 │
│ │
│ Layer 3: 健康检查端点 │
│ ├── LB定期检查 /api/json 或 /login │
│ ├── 不健康时自动移除流量 │
│ └── 配合优雅停机使用 │
│ │
│ ⭐ 最佳实践:三层配合使用! │
│ │
└─────────────────────────────────────────────────────────────────┘
8.1.2 方案一:Nginx负载均衡 + 脚本切换(推荐)
Step 1: Nginx配置
nginx
# /etc/nginx/conf.d/jenkins-lb.conf
# Jenkins负载均衡配置 - 支持主动/被动切换
upstream jenkins_cluster {
# Master-A (默认Active)
server 192.168.1.10:8080 max_fails=3 fail_timeout=30s;
# Master-B (默认Standby)
server 192.168.1.11:8080 max_fails=3 fail_timeout=30s backup;
}
# Jenkins健康检查专用(用于监控)
upstream jenkins_health {
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
server {
listen 80;
server_name jenkins.example.com;
# 主入口 - 负载均衡到Active节点
location / {
proxy_pass http://jenkins_cluster;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 关键:超时设置(影响等待时间)
proxy_connect_timeout 10s;
proxy_send_timeout 300s; # 长构建可能需要较长时间
proxy_read_timeout 300s;
# WebSocket支持(CLI、Agent连接)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 禁用缓冲(流式日志)
proxy_buffering off;
}
# 健康检查端点(供LB和监控系统使用)
location /lb-health {
access_log off;
return 200 'OK';
add_header Content-Type text/plain;
}
}
# 管理接口(仅内网访问)
server {
listen 8081;
server_name _;
# 显示当前后端状态(调试用)
location /status {
check_status;
allow 192.168.1.0/24;
deny all;
}
}
Step 2: 流量切换脚本
bash
#!/bin/bash
# jenkins_traffic_switch.sh - Jenkins流量切换脚本
# 功能:将流量从Master-A切换到Master-B(或反向)
set -e
# ========== 配置区 ==========
MASTER_A="192.168.1.10"
MASTER_B="192.168.1.11"
NGINX_CONF="/etc/nginx/conf.d/jenkins-lb.conf"
BACKUP_DIR="/backup/jenkins/traffic-switch"
ADMIN_USER="admin"
ADMIN_PASS="password"
# 创建备份目录
mkdir -p "$BACKUP_DIR"
echo "========================================="
echo " Jenkins 流量切换工具"
echo "========================================="
# ========== 辅助函数 ==========
check_jenkins_alive() {
local host=$1
if curl -sf --max-time 5 "http://$host:8080/api/json" > /dev/null 2>&1; then
return 0
else
return 1
fi
}
get_busy_executors() {
local host=$1
curl -sf "http://$host:8080/api/json?tree=busyExecutors,totalExecutors" \
--user "$ADMIN_USER:$ADMIN_PASS" | grep -o '"busyExecutors":[0-9]*' | grep -o '[0-9]*'
}
wait_for_zero_builds() {
local host=$1
local timeout=${2:-60} # 默认等待60秒
echo " 等待正在运行的构建完成... (超时: ${timeout}s)"
local elapsed=0
local interval=5
while [ $elapsed -lt $timeout ]; do
busy=$(get_busy_executors $host)
if [ -z "$busy" ] || [ "$busy" = "0" ]; then
echo " ✓ 所有构建已完成! (耗时: ${elapsed}s)"
return 0
fi
echo " ⏳ 还有 $busy 个构建运行中... (${elapsed}s/${timeout}s)"
sleep $interval
elapsed=$((elapsed + interval))
done
echo " ⚠️ 超时!仍有构建在运行"
return 1
}
switch_nginx_active() {
local new_active=$1
local new_standby=$2
echo " 切换Nginx配置..."
# 备份当前配置
cp "$NGINX_CONF" "$BACKUP_DIR/nginx.conf.$(date +%Y%m%d_%H%M%S).bak"
# 修改配置:移除所有backup标记,给standby添加backup
sed -i "s/server $new_active:8080.*$/server $new_active:8080 max_fails=3 fail_timeout=30s;/" "$NGINX_CONF"
sed -i "s/server $new_standby:8080.*$/server $new_standby:8080 max_fails=3 fail_timeout=30s backup;/" "$NGINX_CONF"
# 测试并重载Nginx
nginx -t && nginx -s reload
echo " ✓ Nginx已重载,流量已切换"
echo " Active: $new_active"
echo " Standby: $new_standby"
}
prepare_shutdown() {
local host=$1
echo " 通知 $host 进入Prepare for Shutdown模式..."
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
-X POST "http://$host:8080/prepareShutdown" \
--user "$ADMIN_USER:$ADMIN_PASS")
if [ "$HTTP_CODE" = "302" ] || [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "403" ]; then
echo " ✓ 已进入关机准备模式"
else
echo " ℹ️ 返回码: $HTTP_CODE (可能已在关机模式)"
fi
}
cancel_shutdown() {
local host=$1
echo " 取消 $host 的关机模式..."
curl -sf -X POST "http://$host:8080/cancelShutdown" \
--user "$ADMIN_USER:$ADMIN_PASS" > /dev/null 2>&1
echo " ✓ 已取消关机模式"
}
# ========== 主流程 ==========
show_status() {
echo ""
echo "=== 当前状态 ==="
echo ""
echo "[Master-A] $MASTER_A"
if check_jenkins_alive $MASTER_A; then
BUSY=$(get_busy_executors $MASTER_A)
echo " 状态: ✓ 运行中 (繁忙执行器: ${BUSY:-未知})"
else
echo " 状态: ✗ 无法访问"
fi
echo ""
echo "[Master-B] $MASTER_B"
if check_jenkins_alive $MASTER_B; then
BUSY=$(get_busy_executors $MASTER_B)
echo " 状态: ✓ 运行中 (繁忙执行器: ${BUSY:-未知})"
else
echo " 状态: ✗ 无法访问"
fi
echo ""
echo "[Nginx LB]"
grep -E "^\\s+server.*8080" "$NGINX_CONF" | while read line; do
if echo "$line" | grep -q "backup"; then
echo " Standby: $(echo $line | awk '{print $2}' | cut -d: -f1)"
else
echo " Active: $(echo $line | awk '{print $2}' | cut -d: -f1)"
fi
done
}
switch_to_b() {
echo ""
echo "=== 执行切换: A → B ==="
# 前置检查
echo ""
echo "[前置检查]"
if ! check_jenkins_alive $MASTER_B; then
echo "✗ Master-B ($MASTER_B) 不可用!无法切换"
exit 1
fi
echo "✓ Master-B 可用"
# Step 1: 让Master-A进入Prepare for Shutdown
echo ""
echo "[Step 1/4] Master-A 进入Standby模式"
prepare_shutdown $MASTER_A
# Step 2: 等待现有请求处理完毕
echo ""
echo "[Step 2/4] 等待Master-A现有请求处理完毕"
wait_for_zero_builds $MASTER_A 60 || true
# Step 3: 切换Nginx流量
echo ""
echo "[Step 3/4] 切换流量到Master-B"
switch_nginx_active $MASTER_B $MASTER_A
# Step 4: 确认切换成功
echo ""
echo "[Step 4/4] 验证切换结果"
sleep 3
# 检查新Active是否正常响应
if curl -sf --max-time 10 "http://$MASTER_B:8080/api/json" > /dev/null 2>&1; then
echo "✓ Master-B 已接管流量"
else
echo "⚠️ Master-B 可能还未完全就绪,请手动检查"
fi
echo ""
echo "========================================="
echo " ✅ 切换完成!"
echo " Active: Master-B ($MASTER_B)"
echo " Standby: Master-A ($MASTER_A)"
echo "========================================="
echo ""
echo "下一步操作:"
echo " 1. 可以安全停止Master-A进行升级"
echo " 2. 或者保持此状态进行观察"
echo " 3. 如需切回,执行: $0 switch-to-a"
}
switch_to_a() {
echo ""
echo "=== 执行切换: B → A ==="
# 前置检查
echo ""
echo "[前置检查]"
if ! check_jenkins_alive $MASTER_A; then
echo "✗ Master-A ($MASTER_A) 不可用!无法切换"
exit 1
fi
echo "✓ Master-A 可用"
# Step 1: 让Master-B进入Prepare for Shutdown
echo ""
echo "[Step 1/4] Master-B 进入Standby模式"
prepare_shutdown $MASTER_B
# Step 2: 等待现有请求处理完毕
echo ""
echo "[Step 2/4] 等待Master-B现有请求处理完毕"
wait_for_zero_builds $MASTER_B 60 || true
# Step 3: 切换Nginx流量
echo ""
echo "[Step 3/4] 切换流量到Master-A"
switch_nginx_active $MASTER_A $MASTER_B
# Step 4: 恢复Master-B
echo ""
echo "[Step 4/4] 恢复Master-B正常模式"
cancel_shutdown $MASTER_B
echo ""
echo "========================================="
echo " ✅ 切换完成!"
echo " Active: Master-A ($MASTER_A)"
echo " Standby: Master-B ($MASTER_B)"
echo "========================================="
}
# ========== 命令路由 ==========
case "${1:-status}" in
status|st|s)
show_status
;;
switch-to-b|b)
switch_to_b
;;
switch-to-a|a)
switch_to_a
;;
*)
echo "用法:"
echo " $0 status - 查看当前状态"
echo " $0 switch-to-b - 切换流量到Master-B"
echo " $0 switch-to-a - 切换流量到Master-A"
;;
esac
使用示例:
bash
# 1. 查看当前状态
./jenkins_traffic_switch.sh status
# 2. 将流量从A切换到B(升级A前)
./jenkins_traffic_switch.sh switch-to-b
# 3. 此时可以安全升级Master-A了...
systemctl stop jenkins@master-a
# ... 执行升级 ...
systemctl start jenkins@master-a
# 4. 升级完成后,可选:切回A(或保持B为Active)
./jenkins_traffic_switch.sh switch-to-a
8.1.3 方案二:HAProxy负载均衡 + API切换
haproxy
# /etc/haproxy/haproxy.cfg
# HAProxy配置 - 支持动态切换
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
maxconn 4096
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000ms
timeout client 50000ms
timeout server 50000m # 重要:长构建需要长超时
retries 3
# 统计页面(查看实时状态)
listen stats
bind *:8400
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:password
# Jenkins集群前端
frontend jenkins_front
bind *:80
default_backend jenkins_master
backend jenkins_master
balance roundrobin
option httpchk GET /api/json
# Active节点
server master-a 192.168.1.10:8080 check inter 5s fall 3 rise 2 weight 100
# Standby节点(初始禁用)
server master-b 192.168.1.11:8080 check inter 5s fall 3 rise 2 weight 100 disabled
bash
#!/bin/bash
# haproxy_switch.sh - HAProxy动态流量切换
HAPROXY_SOCK="/run/haproxy/admin.sock"
# 使用HAProxy Socket API动态切换(无需重载配置)
switch_haproxy() {
local enable_server=$1
local disable_server=$2
echo "通过HAProxy Socket API切换..."
# 禁用旧Active
echo "disable server jenkins_master/$disable_server" | socat stdio $HAPROXY_SOCK
# 启用新Active
echo "enable server jenkins_master/$enable_server" | socat stdio $HAPROXY_SOCK
echo "✓ HAProxy已切换(即时生效,无需reload)"
}
# 示例:切换到Master-B
# switch_haproxy "master-b" "master-a"
8.1.4 方案三:DNS切换(最简单)
bash
#!/bin/bash
# dns_switch.sh - DNS方式切换(适用于云DNS)
DOMAIN="jenkins.example.com"
TTL=30 # DNS TTL设置为30秒
# AWS Route53切换
aws route53 change-resource-record-sets \
--hosted-zone-id ZXXXXXXXXXX \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "'"$DOMAIN"'",
"Type": "A",
"TTL": '$TTL',
"ResourceRecords": [{"Value": "192.168.1.11"}]
}
}]
}'
echo "DNS已切换,最多${TTL}秒生效"
8.1.5 完整的滚动升级自动化脚本
bash
#!/bin/bash
# rolling_upgrade.sh - 完整的滚动升级自动化脚本
# 功能:自动完成流量切换 → 停机升级 → 验证 → (可选)切回
set -e
# ========== 配置 ==========
MASTER_A="192.168.1.10"
MASTER_B="192.168.1.11"
SSH_KEY="/root/.ssh/id_rsa"
JENKINS_SERVICE="jenkins"
NEW_JENKINS_WAR="/opt/jenkins/jenkins.war.new"
BACKUP_DIR="/backup/jenkins/pre-upgrade"
LOG_FILE="/var/log/jenkins-rolling-upgrade.log"
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
# ========== 升级单个Master ==========
upgrade_single_master() {
local master_host=$1
local master_name=$2
log "========================================="
log "开始升级 $master_name ($master_host)"
log "========================================="
ssh -i $SSH_KEY root@$master_host << UPGRADE_SCRIPT
set -e
echo "[1/5] 停止Jenkins服务..."
systemctl stop $JENKINS_SERVICE
sleep 5
# 强制结束残留进程
if pgrep -f jenkins.war > /dev/null 2>&1; then
pkill -9 -f jenkins.war
sleep 2
fi
echo "✓ Jenkins已停止"
echo "[2/5] 备份当前版本..."
mkdir -p $BACKUP_DIR
cp /usr/lib/jenkins/jenkins.war $BACKUP_DIR/jenkins.war.\$(date +%Y%m%d_%H%M%S).bak
echo "✓ 备份完成"
echo "[3/5] 部署新版本..."
cp $NEW_JENKINS_WAR /usr/lib/jenkins/jenkins.war
chmod 644 /usr/lib/jenkins/jenkins.war
echo "✓ 新版本部署完成"
echo "[4/5] 清理缓存..."
rm -rf /var/cache/jenkins/war 2>/dev/null
echo "✓ 缓存已清理"
echo "[5/5] 启动Jenkins..."
systemctl start $JENKINS_SERVICE
# 等待启动完成
echo "等待Jenkins启动..."
for i in \$(seq 1 60); do
if curl -sf http://localhost:8080/api/json > /dev/null 2>&1; then
echo "✓ Jenkins启动成功 (耗时: \${i}x5秒)"
break
fi
if [ \$i -eq 60 ]; then
echo "✗ Jenkins启动超时!"
journalctl -u $JENKINS_SERVICE -n 20 --no-pager
exit 1
fi
sleep 5
done
# 显示版本信息
VERSION=\$(curl -sf http://localhost:8080/api/json | grep -o '"version":"[^"]*"' | head -1)
echo "当前版本: \$VERSION"
UPPGRADE_SCRIPT
log "$master_name 升级完成"
}
# ========== 主流程 ==========
main() {
log "========== 开始滚动升级 =========="
# Phase 1: 准备阶段
log ""
log "[Phase 1] 准备工作"
log "检查两个Master的状态..."
for host in $MASTER_A $MASTER_B; do
if ! ssh -i $SSH_KEY root@$host "curl -sf http://localhost:8080/api/json > /dev/null"; then
log "ERROR: $host 不可用!"
exit 1
fi
log "✓ $host 正常运行"
done
# Phase 2: 流量切换 A→B
log ""
log "[Phase 2] 切换流量到Master-B"
# 通知Master-A进入Prepare模式
log "通知Master-A进入Prepare for Shutdown..."
ssh -i $SSH_KEY root@$MASTER_A "
curl -sf -X POST http://localhost:8080/prepareShutdown \\
--user admin:password > /dev/null 2>&1 || true
"
# 等待构建完成(关键步骤!)
log "等待Master-A现有构建完成..."
ssh -i $SSH_KEY root@$MASTER_A '
TIMEOUT=120
ELAPSED=0
while [ $ELAPSED -lt $TIMEOUT ]; do
BUSY=$(curl -sf http://localhost:8080/api/json?tree=busyExecutors \\
--user admin:password | grep -o "[0-9]*")
if [ "$BUSY" = "0" ] || [ -z "$BUSY" ]; then
echo "所有构建已完成"
break
fi
echo "还有 $BUSY 个构建运行中... (\${ELAPSED}s/\${TIMEOUT}s)"
sleep 10
ELAPSED=$((ELAPSED + 10))
done
'
# 切换LB流量
log "切换负载均衡器流量..."
./jenkins_traffic_switch.sh switch-to-b
sleep 5
log "✓ 流量已切换到Master-B"
# Phase 3: 升级Master-A
log ""
log "[Phase 3] 升级Master-A"
upgrade_single_master $MASTER_A "Master-A"
# Phase 4: 验证
log ""
log "[Phase 4] 验证升级结果"
log "检查Master-A健康状态..."
ssh -i $SSH_KEY root@$MASTER_A "
curl -sf http://localhost:8080/api/json > /dev/null && echo '✓ Master-A 健康' || echo '✗ Master-A 异常'
"
# Phase 5: 可选 - 切回流量
read -p "是否将流量切回Master-A?(y/n): " SWITCH_BACK
if [ "$SWITCH_BACK" = "y" ]; then
log ""
log "[Phase 5] 切换流量回Master-A"
# 先让B进入Prepare模式
log "通知Master-B进入Prepare模式..."
ssh -i $SSH_KEY root@$MASTER_B "
curl -sf -X POST http://localhost:8080/prepareShutdown \\
--user admin:password > /dev/null 2>&1 || true
"
sleep 10
# 切换流量
./jenkins_traffic_switch.sh switch-to-a
# 取消B的Prepare模式
ssh -i $SSH_KEY root@$MASTER_B "
curl -sf -X POST http://localhost:8080/cancelShutdown \\
--user admin:password > /dev/null 2>&1 || true
"
log "✓ 流量已切回Master-A"
fi
log ""
log "========== 滚动升级完成 =========="
}
main "$@"
8.1.6 关键要点总结
┌─────────────────────────────────────────────────────────────────┐
│ 流量切换核心要点 │
│ │
│ ⏱️ 为什么需要等待30秒? │
│ ├── LB的健康检查间隔通常是5-10秒 │
│ ├── 现有HTTP请求可能还在传输中 │
│ ├── WebSocket连接(Agent/CLI)需要断开 │
│ └── 30秒是经验值,可根据实际情况调整 │
│ │
│ 🔑 三步切换法(最安全): │
│ Step 1: Prepare for Shutdown(停止接收新任务) │
│ Step 2: 等待busyExecutors=0(现有任务完成) │
│ Step 3: 修改LB规则(切换流量) │
│ │
│ ⚠️ 注意事项: │
│ ├── 切换前必须确认Standby节点健康 │
│ ├── 不要同时修改两个节点的配置 │
│ ├── 切换后观察5分钟再进行下一步 │
│ └── 保持Nginx配置的备份以便快速回滚 │
│ │
│ 🔄 回滚方案: │
│ ├── 如果新Active有问题,立即执行反向切换 │
│ ├── Nginx: nginx -s reload + 交换active/backup │
│ ├── HAProxy: socat enable/disable server │
│ └── 回滚时间应 < 1分钟 │
│ │
└─────────────────────────────────────────────────────────────────┘
8.2 维护期间的数据一致性
在高可用环境下维护时需要注意数据一致性问题:
1. 共享存储写入锁
- 同一时间只能有一个Master写入共享存储
- Standby Master只能读取
2. 配置变更时机
- 不要在两个Master同时修改配置
- 维护期间锁定配置(Manage Jenkins → Reload Configuration from Disk)
3. 构建编号冲突
- 使用外部化构建号(如数据库或共享文件)
- 或确保只有一个Master接受新构建
4. 凭据同步
- secrets/目录必须在所有Master间同步
- 凭据变更后需要重启所有Master
8.3 故障转移后的恢复
bash
#!/bin/bash
# ha_failover_recovery.sh - HA故障转移恢复脚本
ACTIVE_MASTER="master1"
STANDBY_MASTER="master2"
SHARED_STORAGE="/nfs/jenkins-home"
echo "=== HA 故障转移恢复流程 ==="
# Step 1: 确认Standby Master已成为Active
echo "[1/4] 检查当前Active节点..."
ACTIVE_STATUS=$(curl -sf "http://$STANDBY_MASTER/api/json?tree=nodeDescription" | grep -o '"nodeDescription":"[^"]*"')
echo " 当前Active: $ACTIVE_STATUS"
# Step 2: 修复原Master
echo "[2/4] 修复原Master ($ACTIVE_MASTER)..."
ssh $ACTIVE_MASTER "
# 检查Jenkins进程
if pgrep -f jenkins.war > /dev/null; then
echo ' Jenkins仍在运行,强制停止'
pkill -9 -f jenkins.war
fi
# 检查共享存储挂载
mount | grep $SHARED_STORAGE > /dev/null
if [ \$? -ne 0 ]; then
echo ' 重新挂载共享存储'
mount -t nfs nfs-server:$SHARED_STORAGE $SHARED_STORAGE
fi
# 检查磁盘空间
df -h $SHARED_STORAGE
"
# Step 3: 启动原Master作为Standby
echo "[3/4] 启动原Master作为Standby..."
ssh $ACTIVE_MASTER "
export JENKINS_HOME=$SHARED_STORAGE
nohup java -jar jenkins.war --httpPort=8080 > /var/log/jenkins.log 2>&1 &
sleep 30
curl -sf http://localhost:8080/api/json > /dev/null && echo ' ✓ Jenkins启动成功' || echo ' ✗ 启动失败'
"
# Step 4: 更新负载均衡
echo "[4/4] 更新负载均衡配置..."
# 这里根据你的LB类型执行相应命令
# 例如 Nginx:
# ssh lb-server "nginx -s reload"
echo " LB配置已更新(请根据实际情况执行)"
echo ""
echo "✓ 故障转移恢复完成!"
echo " Active: $STANDBY_MASTER"
echo " Standby: $ACTIVE_MASTER"
九、最佳实践与检查清单
9.1 日常维护检查清单
每日检查:
□ 检查Jenkins服务状态(systemctl status jenkins)
□ 检查磁盘空间使用率(df -h)
□ 检查是否有失败的构建
□ 查看系统日志有无异常(journalctl -u jenkins --since today)
每周检查:
□ 验证备份是否成功执行
□ 检查备份文件完整性
□ 清理旧构建(释放磁盘空间)
□ 检查插件更新
每月检查:
□ 执行一次完整恢复演练
□ 审查备份保留策略
□ 检查凭据有效期
□ 性能评估和优化
9.2 停机维护检查清单
停机前(提前1天):
□ 发送停机通知给所有团队成员
□ 确认没有重要的定时构建在停机窗口内
□ 检查当前正在运行的长时构建
□ 确认备份任务已成功完成
□ 准备好回滚方案
停机时:
□ 进入Prepare for Shutdown模式
□ 等待构建完成或手动取消非关键构建
□ 执行备份(如果还没做)
□ 停止Jenkins服务
□ 执行维护操作(升级/迁移/修复)
停机后:
□ 启动Jenkins服务
□ 验证所有Job配置正确
□ 测试几个关键构建
□ 检查Agent连接状态
□ 发送恢复通知
9.3 备份策略最佳实践
✅ 最佳实践:
1. 3-2-1 备份原则
├── 3份数据副本(原始 + 2个备份)
├── 2种不同介质(本地 + 远程/云)
└── 1份异地备份(防火灾/地震等)
2. 备份加密
├── 对敏感文件(credentials.xml, secrets/)加密存储
└── 使用GPG或云存储的加密功能
3. 备份验证
├── 定期执行恢复演练(至少每季度一次)
├── 自动化备份完整性校验
└── 记录恢复时间和步骤
4. 版本控制Jenkins配置
├── 将config.xml纳入Git管理
├── 使用Configuration as Code (JCasC)
└── Job配置使用Pipeline as Code
❌ 常见错误:
1. 只备份不验证 → 恢复时发现备份损坏
2. 备份到同一磁盘 → 磁盘损坏时备份也丢失
3. 不备份secrets/ → 凭据无法解密
4. 备份workspace → 浪费空间且拖慢速度
5. 从不演练恢复 → 真正需要时手忙脚乱
9.4 灾难恢复计划模板
Jenkins 灾难恢复计划 (DRP)
=========================
1. 灾难定义
- Jenkins Master完全不可用
- 共享存储损坏或不可访问
- 数据丢失或损坏
2. RTO/RPO目标
- RTO (恢复时间目标): 4小时
- RPO (恢复点目标): 24小时
3. 恢复优先级
P0 (立即):
- 恢复Jenkins Master服务
- 恢复核心Job配置
P1 (4小时内):
- 恢复所有Job配置
- 恢复用户和权限
- 恢复凭据
P2 (24小时内):
- 恢复构建历史(最近7天)
- 恢复插件配置
- 验证所有功能正常
4. 联系人
- 一线运维: xxx@company.com / 电话
- 二线支持: yyy@company.com / 电话
- 管理层: zzz@company.com / 电话
5. 恢复步骤
(参见第六章:数据恢复)
6. 演练计划
- 季度演练: 完整恢复流程
- 半年度演练: 灾难场景模拟
附录:常用命令速查
A. 停机相关命令
bash
# 进入准备关机模式
curl -X POST http://localhost:8080/prepareShutdown --user admin:password
# 取消关机模式
curl -X POST http://localhost:8080/cancelShutdown --user admin:password
# 安全关闭(等待构建完成)
curl -X POST http://localhost:8080/safeShutdown --user admin:password
# 立即关闭
curl -X POST http://localhost:8080/shutdown --user admin:password
# 检查运行状态
curl -s http://localhost:8080/api/json?tree=busyExecutors,totalExecutors --user admin:password
# CLI方式
java -jar jenkins-cli.jar -s http://localhost:8080 prepare-shutdown --username admin --password password
java -jar jenkins-cli.jar -s http://localhost:8080 safe-shutdown --username admin --password password
java -jar jenkins-cli.jar -s http://localhost:8080 cancel-shutdown --username admin --password password
B. 备份相关命令
bash
# 快速备份核心配置(一行命令)
tar -czf jenkins-core-backup-$(date +%Y%m%d).tar.gz \
-C /var/lib/jenkins \
config.xml credentials.xml secrets/ users/
# 快速统计JENKINS_HOME大小
du -sh /var/lib/jenkins/*
du -sh /var/lib/jenkins/jobs/*/builds/ 2>/dev/null | sort -rh | head -20
# 查找大文件(清理前先查看)
find /var/lib/jenkins -type f -size +100M -exec ls -lh {} \; 2>/dev/null | sort -k5 -rh
# 备份到远程
rsync -avz --delete /var/lib/jenkins/ user@backup:/backup/jenkins/
# 备份到S3
aws s sync /var/lib/jenkins/ s3://bucket/jenkins-backup/ --exclude "*" --include "config.xml" --include "credentials.xml" --include "secrets/**" --include "users/**" --include "jobs/*/config.xml"
C. 恢复相关命令
bash
# 快速检查备份内容
tar -tzf backup.tar.gz | head -50
# 恢复单个文件
tar -xzf backup.tar.gz -C /var/lib/jenkins config.xml
# 回滚到备份前状态
mv /var/lib/jenkins /var/lib/jenkins.failed
mv /var/lib/jenkins.bak.timestamp /var/lib/jenkins
systemctl restart jenkins
# 修复权限
chown -R jenkins:jenkins /var/lib/jenkins
chmod -R 755 /var/lib/jenkins