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
相关推荐
威迪斯特2 小时前
Gorilla框架:Go语言生态中的模块化开发利器
运维·开发语言·后端·golang·web框架·维护·gorilla
liuyao_xianhui2 小时前
Linux开发工具结尾 _make
linux·运维·服务器·数据结构·哈希算法·宽度优先·1024程序员节
MXsoft6182 小时前
【无标题】
运维·自动化
天疆说2 小时前
在 Ubuntu 22.04 上安装 Ghostty 终端
linux·运维·ubuntu
buhuizhiyuci2 小时前
熟练使用Linux编译工具(gcc, g++, make, makefile)
linux·运维·服务器
健康平安的活着2 小时前
使用tsf分析服务器的内存使用情况【经典版】
运维·服务器
wechatbot8882 小时前
企业微信 iPad 协议客服机器人自动化管理平台开发指南
java·运维·微信·自动化·企业微信·ipad
草莓熊Lotso2 小时前
从 LLM 底层原理到 LangChain 全链路打通:大模型应用开发新征程
linux·运维·服务器·人工智能·langchain
cyber_两只龙宝2 小时前
【Oracle】Oracle数据库的登录验证
linux·运维·数据库·sql·云原生·oracle