Jenkins Master 停机与备份

目录

  1. 概述
  2. [JENKINS_HOME 目录结构](#JENKINS_HOME 目录结构)
  3. [Master 安全停机](#Master 安全停机)
  4. 数据备份方案
    • 4.1 备份策略选择
    • 4.2 推荐备份方案
    • 4.3 备份时是否需要停机?(核心问题)
    • 4.4 备份时的注意事项(详细清单)
    • 4.5 备份内容详细说明
  5. Linux实时同步技术详解 ⭐新增
    • 5.1 技术选型对比
    • 5.2 rsync基础同步
    • 5.3 inotify+rsync实时同步
    • 5.4 lsyncd实时同步守护进程(推荐)
    • 5.5 条件过滤与高级配置
    • 5.6 实时同步+定时备份完整方案
  6. 自动化备份脚本
  7. 数据恢复
  8. 高可用场景下的维护
  9. 最佳实践与检查清单

一、概述

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
相关推荐
乘云数字DATABUFF4 天前
5分钟部署开源APM Databuff:OpenTelemetry全链路追踪入门实战
运维·后端
荣--6 天前
一键部署不是为了省时间 —— 它是把"买来的 PaaS"变成"自己的平台"的拐点
运维·zabbix·工程化·一键部署·平台化·边界设计
江华森6 天前
动手实战学 Docker — 从零到集群编排完全指南
运维
Avan_菜菜7 天前
FRP 内网穿透完整实战:从 HTTP 映射到 HTTPS 自签代理
运维·nginx·https
SelectDB8 天前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
XIAOHEZIcode9 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户03284722207010 天前
如何搭建本地yum源(上)
运维
大树8813 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠13 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质13 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务